Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create production container #31

Merged
merged 19 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/.ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,35 @@ jobs:

- name: Run unit tests
run: pytest -c test/pytest.ini test/unit/ --cov

docker:
# This job triggers only if all the other jobs succeed. It builds the Docker image and if successful,
# it pushes it to Harbor.
needs: [linting, unit-tests]
name: Docker
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

- name: Login to Harbor
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: harbor.stfc.ac.uk/ldap-jwt-authentication
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
with:
images: harbor.stfc.ac.uk/ldap-jwt-authentication/auth-api

- name: Build and push Docker image to Harbor
uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0
with:
context: .
file: ./Dockerfile.prod
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ldap_jwt_auth/logging.ini
ldap_jwt_auth/active_usernames.txt
/active_usernames.txt
/keys/

# Byte-compiled / optimized / DLL files
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
FROM python:3.10-alpine3.17
FROM python:3.10-alpine3.19

WORKDIR /ldap-jwt-auth-run

COPY pyproject.toml ./
COPY ldap_jwt_auth/ ldap_jwt_auth/
COPY logs/ logs/

RUN --mount=type=cache,target=/root/.cache \
set -eux; \
Expand Down
31 changes: 31 additions & 0 deletions Dockerfile.prod
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
FROM python:3.10-alpine3.19

WORKDIR /ldap-jwt-auth-run

COPY README.md pyproject.toml ./
# Copy ldap_jwt_auth source files
COPY ldap_jwt_auth/ ldap_jwt_auth/
COPY logs/ logs/

RUN set -eux; \
\
# Install python-ldap system dependencies \
apk add --no-cache build-base openldap-dev python3-dev; \
\
# Install pip dependencies \
python -m pip install --no-cache-dir .; \
\
# Create loging.ini from its .example file \
cp ldap_jwt_auth/logging.example.ini ldap_jwt_auth/logging.ini; \
\
# Create a non-root user to run as \
addgroup -g 500 -S ldap-jwt-auth; \
adduser -S -D -G ldap-jwt-auth -H -u 500 -h /ldap-jwt-auth-run ldap-jwt-auth; \
\
# Change ownership of logs/ - app will need to write log files to it \
chown -R ldap-jwt-auth:ldap-jwt-auth logs/;

USER ldap-jwt-auth

CMD ["uvicorn", "ldap_jwt_auth.main:app", "--host", "0.0.0.0", "--port", "8000"]
EXPOSE 8000
139 changes: 114 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,114 @@
# LDAP-JWT Authentication Service
This is a Python microservice that provides user authentication against an LDAP server and returns a JSON Web Token
(JWT).

This is a Python microservice created using FastAPI that provides user authentication against an LDAP server and returns
a JSON Web Token (JWT).

## How to Run
This microservice requires an LDAP server to run against.

### Prerequisites
- Docker installed (if you want to run the microservice inside Docker)
- Python 3.10 (or above) and an LDAP server to connect to
- Docker and Docker Compose installed (if you want to run the microservice inside Docker)
- LDAP server to connect to
- Python 3.10 (or above) (if you are not using Docker)
- Private and public key pair (must be OpenSSH encoded) for encrypting and decrypting the JWTs
- A list of active usernames, defining who can use this service
- This repository cloned

### Docker Setup
The easiest way to run the application with Docker for local development is using the `docker-compose.yml` file. It is
configured to start the application in a reload mode using the `Dockerfile`. You can also use the `Dockerfile` directly
to run the application in a container. Please do not use the `Dockerfile` in production.

Ensure that Docker is installed and running on your machine before proceeding.

1. Create a `.env` file alongside the `.env.example` file. Use the example file as a reference and modify the values accordingly.
1. Create a `.env` file alongside the `.env.example` file. Use the example file as a reference and modify the values
accordingly.
```bash
cp ldap_jwt_auth/.env.example ldap_jwt_auth/.env
```

2. Create a `logging.ini` file alongside the `logging.example.ini` file. Use the example file as a reference and modify it accordingly.
2. Create a `logging.ini` file alongside the `logging.example.ini` file. Use the example file as a reference and modify
it accordingly:
```bash
cp ldap_jwt_auth/logging.example.ini ldap_jwt_auth/logging.ini
```

3. Create a `keys` directory in the root of the project directory, navigate to it, and generate OpenSSH encoded private and public key pair:
3. Create a `keys` directory in the root of the project directory, navigate to it, and generate OpenSSH encoded private
and public key pair:
```bash
mkdir keys
cd keys/
ssh-keygen -b 2048 -t rsa -f jwt-key -q -N ""
ssh-keygen -b 2048 -t rsa -f keys/jwt-key -q -N "" -C ""
```

4. Create a `active_usernames.txt` file alongside the `active_usernames.example.txt` file and add all the usernames that
can use this system. The usernames are the Federal IDs and each one should be stored on a separate line.
```bash
cp active_usernames.example.txt active_usernames.txt
```

4. Create a `active_usernames.txt` file alongside the `active_usernames.example.txt` file and add all the usernames (each one on a seperate line) that are active/can access the system.
#### Using `docker-compose.yml`
The easiest way to run the application with Docker for local development is using the `docker-compose.yml` file. It is
configured to start the application in a reload mode using the `Dockerfile`.

#### Using Docker Compose File
1. Build and start the Docker container:
```bash
docker-compose up
```
The microservice should now be running inside Docker at http://localhost:8000 and its Swagger UI could be accessed
at http://localhost:8000/docs.

#### Using Dockerfile
#### Using `Dockerfile`
Use the `Dockerfile` to run just the application itself in a container. Use this only for local development (not
production)!

1. Build an image using the `Dockerfile` from the root of the project directory:
```bash
docker build -f Dockerfile -t ldap_jwt_auth_image .
docker build -f Dockerfile -t ldap_jwt_auth_api_image .
```

2. Start the container using the image built and map it to port `8000` locally:
```bash
docker run -p 8000:8000 --name ldap_jwt_auth_container ldap_jwt_auth_image
docker run -p 8000:8000 --name ldap_jwt_auth_api_container -v ./keys/jwt-key:/ldap-jwt-auth-run/keys/jwt-key -v ./keys/jwt-key.pub:/ldap-jwt-auth-run/keys/jwt-key.pub -v ./active_usernames.txt:/ldap-jwt-auth-run/active_usernames.txt -v ./logs:/ldap-jwt-auth-run/logs ldap_jwt_auth_api_image
```
or with values for the environment variables:
```bash
docker run -p 8000:8000 --name ldap_jwt_auth_api_container --env AUTHENTICATION__ACCESS_TOKEN_VALIDITY_MINUTES=10 -v ./keys/jwt-key:/ldap-jwt-auth-run/keys/jwt-key -v ./keys/jwt-key.pub:/ldap-jwt-auth-run/keys/jwt-key.pub -v ./active_usernames.txt:/ldap-jwt-auth-run/active_usernames.txt -v ./logs:/ldap-jwt-auth-run/logs ldap_jwt_auth_api_image
```
The microservice should now be running inside Docker at http://localhost:8000 and its Swagger UI could be accessed
at http://localhost:8000/docs.

#### Using `Dockerfile.prod`
Use the `Dockerfile.prod` to run just the application itself in a container. This can be used for production.

1. While in root of the project directory, change the permissions of the `logs` directory so that it is writable by
other users. This allows the container to save the application logs to it.
```bash
sudo chmod -R 0777 logs/
```

2. Private keys are only readable by the owner. Given that the private key is generated on the host machine and the
container runs with a different user, it means that the key is not readable by the user in the container because the
ownership belongs to the user on the host. This can be solved by transferring the ownership to the user in the
container and setting the permissions.
```bash
sudo chown 500:500 keys/jwt-key
sudo chmod 0400 keys/jwt-key
```

3. Build an image using the `Dockerfile.prod` from the root of the project directory:
```bash
docker build -f Dockerfile.prod -t ldap_jwt_auth_api_image .
```

4. Start the container using the image built and map it to port `8000` locally:
```bash
docker run -p 8000:8000 --name ldap_jwt_auth_api_container -v ./keys/jwt-key:/ldap-jwt-auth-run/keys/jwt-key -v ./keys/jwt-key.pub:/ldap-jwt-auth-run/keys/jwt-key.pub -v ./active_usernames.txt:/ldap-jwt-auth-run/active_usernames.txt -v ./logs:/ldap-jwt-auth-run/logs ldap_jwt_auth_api_image
```
or with values for the environment variables:
```bash
docker run -p 8000:8000 --name ldap_jwt_auth_api_container --env AUTHENTICATION__ACCESS_TOKEN_VALIDITY_MINUTES=10 -v ./keys/jwt-key:/ldap-jwt-auth-run/keys/jwt-key -v ./keys/jwt-key.pub:/ldap-jwt-auth-run/keys/jwt-key.pub -v ./active_usernames.txt:/ldap-jwt-auth-run/active_usernames.txt -v ./logs:/ldap-jwt-auth-run/logs ldap_jwt_auth_api_image
```
The microservice should now be running inside Docker at http://localhost:8000 and its Swagger UI could be accessed
at http://localhost:8000/docs.

### Local Setup
Ensure that you have an LDAP server to connect to.
Ensure that Python is installed on your machine before proceeding.

1. Create a Python virtual environment and activate it in the root of the project directory:
```bash
Expand All @@ -68,21 +123,30 @@ Ensure that you have an LDAP server to connect to.
```bash
pip install .[dev]
```

4. Create a `.env` file alongside the `.env.example` file. Use the example file as a reference and modify the values
accordingly.
```bash
cp ldap_jwt_auth/.env.example ldap_jwt_auth/.env
```

5. Create a `logging.ini` file alongside the `logging.example.ini` file. Use the example file as a reference and modify
it accordingly.
it accordingly:
```bash
cp ldap_jwt_auth/logging.example.ini ldap_jwt_auth/logging.ini
```

6. Create a `keys` directory in the root of the project directory, navigate to it, and generate OpenSSH encoded private and public key pair:
6. Create a `keys` directory in the root of the project directory, navigate to it, and generate OpenSSH encoded private
and public key pair:
```bash
mkdir keys
cd keys/
ssh-keygen -b 2048 -t rsa -f jwt-key -q -N ""
ssh-keygen -b 2048 -t rsa -f keys/jwt-key -q -N "" -C ""
```

7. Create a `active_usernames.txt` file alongside the `active_usernames.example.txt` file and add all the usernames (each one on a seperate line) that are active/can access the system.
7. Create a `active_usernames.txt` file alongside the `active_usernames.example.txt` file and add all the usernames that
can use this system. The usernames are the Federal IDs and each one should be stored on a separate line.
```bash
cp active_usernames.example.txt active_usernames.txt
```

8. Start the microservice using Uvicorn:
```bash
Expand All @@ -95,3 +159,28 @@ Ensure that you have an LDAP server to connect to.
```bash
pytest -c test/pytest.ini test/unit/
```

## Notes

### Application Configuration
The configuration for the application is handled
using [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). It allows for loading config
values from environment variables or the `.env` file. Please note that even when using the `.env` file, Pydantic will
still read environment variables as well as the `.env` file, environment variables will always take priority over
values loaded from the `.env` file.

Listed below are the environment variables supported by the application.

| Environment Variable | Description | Mandatory | Default Value |
|-------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|-----------|-----------------------------------------------------------|
| `API__TITLE` | The title of the API which is added to the generated OpenAPI. | No | `LDAP-JWT Authentication Service API` |
| `API__DESCRIPTION` | The description of the API which is added to the generated OpenAPI. | No | `This is the API for the LDAP-JWT Authentication Service` |
| `API__ROOT_PATH` | (If using a proxy) The path prefix handled by a proxy that is not seen by the app. | No | ` ` |
| `AUTHENTICATION__PRIVATE_KEY_PATH` | The path to the private key to be used for encoding JWT access and refresh tokens. | Yes | |
| `AUTHENTICATION__PUBLIC_KEY_PATH` | The path to the public key to be used for decoding JWT access and refresh tokens signed by the corresponding private key. | Yes | |
| `AUTHENTICATION__JWT_ALGORITHM` | The algorithm to use to decode the JWT access and refresh tokens. | Yes | |
| `AUTHENTICATION__ACCESS_TOKEN_VALIDITY_MINUTES` | Minutes after which the JWT access token expires. | Yes | |
| `AUTHENTICATION__REFRESH_TOKEN_VALIDITY_DAYS` | Days after which the JWT refresh token expires. | Yes | |
| `AUTHENTICATION__ACTIVE_USERNAMES_PATH` | The path to the `txt` file containing the active usernames and defining who can use this service. | Yes | |
| `LDAP_SERVER__URL` | The URL to the LDAP server to connect to. | Yes | |
| `LDAP_SERVER__REALM` | The realm for the LDAP server. | Yes | |
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ services:
volumes:
- ./ldap_jwt_auth:/ldap-jwt-auth-run/ldap_jwt_auth
- ./keys:/ldap-jwt-auth-run/keys
- ./logs:/ldap-jwt-auth-run/logs
- ./active_usernames.txt:/ldap-jwt-auth-run/active_usernames.txt
ports:
- 8000:8000
restart: on-failure
2 changes: 1 addition & 1 deletion ldap_jwt_auth/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ AUTHENTICATION__PUBLIC_KEY_PATH=./keys/jwt-key.pub
AUTHENTICATION__JWT_ALGORITHM=RS256
AUTHENTICATION__ACCESS_TOKEN_VALIDITY_MINUTES=5
AUTHENTICATION__REFRESH_TOKEN_VALIDITY_DAYS=7
AUTHENTICATION__ACTIVE_USERNAMES_PATH=./ldap_jwt_auth/active_usernames.txt
AUTHENTICATION__ACTIVE_USERNAMES_PATH=./active_usernames.txt
LDAP_SERVER__URL=ldap://ldap.example.com:389
LDAP_SERVER__REALM=LDAP.EXAMPLE.COM
2 changes: 1 addition & 1 deletion ldap_jwt_auth/logging.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ propagate=0
[handler_fileHandler]
class=logging.handlers.TimedRotatingFileHandler
formatter=fileFormatter
args=('ldap-jwt-auth.log', 'D', 1, 20,)
args=('./logs/ldap-jwt-auth.log', 'D', 1, 20,)

[handler_consoleHandler]
class=StreamHandler
Expand Down
2 changes: 1 addition & 1 deletion ldap_jwt_auth/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ldap_jwt_auth.core.logger_setup import setup_logger
from ldap_jwt_auth.routers import login, refresh, verify

app = FastAPI(title=config.api.title, description=config.api.description)
app = FastAPI(title=config.api.title, description=config.api.description, root_path=config.api.root_path)

setup_logger()
logger = logging.getLogger()
Expand Down
4 changes: 2 additions & 2 deletions ldap_jwt_auth/routers/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ def login(
max_age=config.authentication.refresh_token_validity_days * 24 * 60 * 60,
secure=True,
httponly=True,
samesite="Lax",
path="/refresh",
samesite="lax",
path=f"{config.api.root_path}/refresh",
)
return response
except (InvalidCredentialsError, UserNotActiveError) as exc:
Expand Down
Empty file added logs/.keep
Empty file.