-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from ral-facilities/implement-login-endpoint-#6
Implement a login endpoint
- Loading branch information
Showing
24 changed files
with
568 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
ldap_jwt_auth/logging.ini | ||
keys/ | ||
/keys/ | ||
|
||
# Byte-compiled / optimized / DLL files | ||
__pycache__/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,89 @@ | ||
# LDAP-JWT Authentication Service | ||
This is a Python microservice 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 | ||
- Private and public key pair (must be OpenSSH encoded) for encrypting and decrypting the JWTs | ||
- 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. | ||
|
||
#### Using Docker Compose File | ||
1. Create a `.env` file alongside the `.env.example` file. Use the example file as a reference and modify the values | ||
accordingly. | ||
|
||
2. Create a `logging.ini` file alongside the `logging.example.ini` file. Use the example file as a reference and modify | ||
it accordingly. | ||
|
||
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 "" | ||
``` | ||
|
||
4. 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 | ||
1. Build an image using the `Dockerfile` from the root of the project directory: | ||
```bash | ||
docker build -f Dockerfile -t ldap_jwt_auth_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 | ||
``` | ||
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. | ||
|
||
1. Create a Python virtual environment and activate it in the root of the project directory: | ||
```bash | ||
python -m venv venv | ||
source venv/bin/activate | ||
``` | ||
|
||
2. Install the software packages required to build `python-ldap` on your local system, more | ||
info [here](https://www.python-ldap.org/en/python-ldap-3.3.0/installing.html). | ||
|
||
3. Install the required dependencies using pip: | ||
```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. | ||
|
||
5. Create a `logging.ini` file alongside the `logging.example.ini` file. Use the example file as a reference and modify | ||
it accordingly. | ||
|
||
6. Start the microservice using Uvicorn: | ||
```bash | ||
uvicorn ldap_jwt_auth.main:app --log-config ldap_jwt_auth/logging.ini --reload | ||
``` | ||
The microservice should now be running locally at http://localhost:8000. The Swagger UI could be accessed | ||
at http://localhost:8000/docs. | ||
|
||
7. To run the unit tests, run : | ||
```bash | ||
pytest test/unit/ | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# pylint: disable=no-member | ||
""" | ||
Module for providing a class for managing authentication. | ||
""" | ||
import logging | ||
|
||
import ldap | ||
|
||
from ldap_jwt_auth.core.config import config | ||
from ldap_jwt_auth.core.exceptions import InvalidCredentialsError, LDAPServerError | ||
from ldap_jwt_auth.core.models import UserCredentials | ||
|
||
logger = logging.getLogger() | ||
|
||
|
||
class Authentication: | ||
""" | ||
Class for managing authentication against an LDAP server. | ||
""" | ||
|
||
def authenticate(self, user_credentials: UserCredentials) -> None: | ||
""" | ||
Authenticate a user against an LDAP server based on the provided user credentials. | ||
:param user_credentials: The credentials of the user. | ||
:raises InvalidCredentialsError: If the user credentials are empty or invalid. | ||
:raises LDAPServerError: If there is a problem with the LDAP server. | ||
""" | ||
username = user_credentials.username | ||
password = user_credentials.password | ||
logger.info("Authenticating a user") | ||
logger.debug("Username provided is '%s'", username) | ||
|
||
if not username or not password: | ||
raise InvalidCredentialsError("Empty username or password") | ||
|
||
try: | ||
connection = ldap.initialize(config.ldap_server.url) | ||
ldap.set_option(ldap.OPT_PROTOCOL_VERSION, 3) | ||
ldap.set_option(ldap.OPT_X_TLS_DEMAND, True) | ||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) | ||
ldap.set_option(ldap.OPT_DEBUG_LEVEL, 0) | ||
connection.start_tls_s() | ||
connection.simple_bind_s(f"{username}@{config.ldap_server.realm}", user_credentials.password) | ||
logger.info("Authentication successful") | ||
connection.unbind() | ||
except ldap.INVALID_CREDENTIALS as exc: | ||
message = "Invalid username or password" | ||
logger.exception(message) | ||
connection.unbind() | ||
raise InvalidCredentialsError(message) from exc | ||
except Exception as exc: | ||
message = "Problem with LDAP server" | ||
logger.exception(message) | ||
raise LDAPServerError(message) from exc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
""" | ||
Module for providing a class for handling JWTs. | ||
""" | ||
import logging | ||
from datetime import datetime, timezone, timedelta | ||
|
||
import jwt | ||
from cryptography.hazmat.primitives import serialization | ||
|
||
from ldap_jwt_auth.core.config import config | ||
from ldap_jwt_auth.core.constants import PRIVATE_KEY | ||
|
||
logger = logging.getLogger() | ||
|
||
|
||
class JWTHandler: | ||
""" | ||
Class for handling JWTs. | ||
""" | ||
|
||
def get_access_token(self, username: str) -> str: | ||
""" | ||
Generates a payload and returns a signed JWT access token. | ||
:param username: The username of the user. | ||
:return: The signed JWT access token | ||
""" | ||
logger.info("Getting an access token") | ||
payload = { | ||
"username": username, | ||
"exp": datetime.now(timezone.utc) + timedelta(minutes=config.authentication.access_token_validity_minutes), | ||
} | ||
return self._pack_jwt(payload) | ||
|
||
def get_refresh_token(self) -> str: | ||
""" | ||
Generates a payload and returns a signed JWT refresh token. | ||
:return: The signed JWT refresh token. | ||
""" | ||
logger.info("Getting a refresh token") | ||
payload = { | ||
"exp": datetime.now(timezone.utc) + timedelta(days=config.authentication.refresh_token_validity_days) | ||
} | ||
return self._pack_jwt(payload) | ||
|
||
def _pack_jwt(self, payload: dict) -> str: | ||
""" | ||
Packs the provided payload into a JWT token and signs it. | ||
:param payload: The payload to be packed. | ||
:return: The encoded and signed JWT. | ||
""" | ||
logger.debug("Packing payload into a JWT") | ||
bytes_key = bytes(PRIVATE_KEY, encoding="utf8") | ||
loaded_private_key = serialization.load_ssh_private_key(bytes_key, password=None) | ||
token = jwt.encode(payload, loaded_private_key, algorithm=config.authentication.jwt_algorithm) | ||
return token |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
""" | ||
Module for custom exception classes. | ||
""" | ||
|
||
|
||
class LDAPServerError(Exception): | ||
""" | ||
Exception raised when there is problem with the LDAP server. | ||
""" | ||
|
||
|
||
class InvalidCredentialsError(Exception): | ||
""" | ||
Exception raised when invalid credentials are provided. | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
""" | ||
Model for defining the API schema models. | ||
""" | ||
|
||
from pydantic import BaseModel | ||
|
||
|
||
class UserCredentials(BaseModel): | ||
""" | ||
Model for the user credentials. | ||
""" | ||
|
||
username: str | ||
password: str |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Oops, something went wrong.