Skip to content

Commit

Permalink
added auth plus docs
Browse files Browse the repository at this point in the history
  • Loading branch information
witlox committed Nov 25, 2024
1 parent c5fc122 commit 5673c91
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 24 deletions.
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ The docker way, either use the `devcontainer` or run with `docker-compose`:
## Settings

The settings are stored in a `.env` file. The default selected by poetry is `.env` which is configured for development.
The following settings are available:
- DEBUG: boolean, default=False; set to True to enable debug mode
- UI: boolean, default=False; set to True to enable the UI for the API
- CORS: string, default="*"; set to the allowed origins for CORS
- PEER_SECRET: string, default=""; set the secret for authenticating peers
- CLOCK_OFFSET: float, default=0.0; set the allowed clock offset for synchronization
- REDIS_URL: string, default="redis://redis:6379/0"; set the URL for the Redis database
The following settings can be configured:
```dotenv
DEBUG: False #boolean, default=False; set to True to enable debug mode, very chatty
UI: False #boolean, default=False; set to True to enable the UI for the API
CORS: * #string, default=*; set to the allowed origins for CORS
PEER_SECRET: "abracadabra" #string, default=""; set the secret for authenticating peers
PEERS: "a,b,c" #string, default=""; set the comma seperated list of peers to sync with
CLOCK_OFFSET: 0.0 #float, default=0.0; set the allowed clock offset for synchronization
REDIS_URL: redis://localhost:6379/0 #string, default="redis://redis:6379/0"; set the URL for the Redis database
```

For Telemetry environment variables, see [Telemetry](docs/Telemetry.md).

44 changes: 44 additions & 0 deletions docs/Authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Authentication mechanisms for HORAO

There are 3 configurations that need to be applied, the first is peer authentication using a shared secret.
The second and third are user authentication that need to be configured for regular users and administrators.
Both users and administrators will need to use the same Open ID Connect provider.
Basic information requested is only email, identification of administrators is done by the roles custom claim (so the OIDC provider needs to support this, and needs to be configured, like https://claims.idp.example.com/role).

## Peer authentication
All instances of `HORAO` that form one cluster need to be able to authenticate each other. This is done using a shared secret that is stored in the `.env` file. The shared secret is used to sign the messages that are exchanged between the instances. The shared secret is stored in the `.env` file as follows:
```dotenv
PEER_SECRET=abracadabra
```
Peer synchronization is done using the `PEERS` environment variable. This is a comma separated list of peers that need to be synchronized. The peers are identified by their IP address. The `PEERS` environment variable is stored in the `.env` file as follows:
```dotenv
PEERS=10.0.0.1,some.host.somewhere
```
These are comma separated values that are used to identify the peers that need to be synchronized.
The synchronization happens over the 'synchronize' endpoint on the API.

There is a 'PEER_STRICT' that defaults to 'True'. This means that the peers origin needs to be matched to the value supplied in the 'PEERS' environment variable. If 'PEER_STRICT' is set to 'False' then the origin of the peer is not checked.

## Open ID Connect parts
The following variables need to be set in the `.env` file:
```dotenv
OAUTH_NAME=openidc
OAUTH_CLIENT_ID=client_id
OAUTH_CLIENT_SECRET=client_secret
OAUTH_SERVER_METADATA_URL=https://idp.example.com/.well-known/openid-configuration
OAUTH_BASE_URL=https://idp.example.com
OAUTH_AUTHORIZE_URL=https://idp.example.com/authorize
OAUTH_AUTHORIZE_PARAMS={}
OAUTH_ACCESS_TOKEN_URL=https://idp.example.com/token
OAUTH_REQUEST_TOKEN_URL=None
OAUTH_ROLE_URI=https://claims.idp.example.com/role
```
The `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are the client id and client secret that are provided by the Open ID Connect provider.

### Administrators
Administrators are identified by the roles custom claim. The roles custom claim is used to identify the administrators. The roles custom claim is stored in the `.env` file as follows:
```dotenv
ADMINISTRATOR_ROLE=administrator
```
### Users
If the roles custom claim is not present, or the user does not have the administrator role, then the user is considered a regular user.
2 changes: 2 additions & 0 deletions horao/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ def init(authorization: Optional[AuthenticationBackend] = None) -> Starlette:
logger.warning("CORS is set to *")
routes = [
Route("/ping", endpoint=horao.api.alive_controller.is_alive, methods=["GET"]),
Route("/login", endpoint=horao.api.authenticate.login, methods=["POST"]),
Route("/logout", endpoint=horao.api.authenticate.logout, methods=["POST"]),
Route(
"/synchronize",
endpoint=horao.api.synchronization.synchronize,
Expand Down
4 changes: 3 additions & 1 deletion horao/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-#
"""API definitions."""
from horao.api.alive_controller import is_alive
from .alive_controller import is_alive
from .authenticate import login, logout
from .synchronization import synchronize
35 changes: 35 additions & 0 deletions horao/api/authenticate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import os

from authlib.integrations.starlette_client import OAuth # type: ignore
from starlette.requests import Request
from starlette.responses import RedirectResponse


async def login(request: Request):
redirect_uri = request.url_for("auth")
oauth_role_uri = os.getenv("OAUTH_ROLE_URI", "role")
oauth_settings = {
"name": os.getenv("OATH_NAME", "openidc"),
"client_id": os.getenv("OAUTH_CLIENT_ID"),
"client_secret": os.getenv("OAUTH_CLIENT_SECRET"),
"server_metadata_url": os.getenv("OAUTH_SERVER_METADATA_URL", None),
"api_base_url": os.getenv("OAUTH_API_BASE_URL", None),
"authorize_url": os.getenv("OAUTH_AUTHORIZE_URL", None),
"authorize_params": os.getenv("OAUTH_AUTHORIZE_PARAMS", None),
"access_token_url": os.getenv("OAUTH_ACCESS_TOKEN_URL", None),
"access_token_params": os.getenv("OAUTH_ACCESS_TOKEN_PARAMS", None),
"request_token_url": os.getenv("OAUTH_REFRESH_TOKEN_URL", None),
"request_token_params": os.getenv("OAUTH_REFRESH_TOKEN_PARAMS", None),
"client_kwargs": os.getenv(
"OAUTH_CLIENT_KWARGS", {"scope": f"openid email {oauth_role_uri}"}
),
}
oauth = OAuth()
filtered_settings = {k: v for k, v in oauth_settings.items() if v is not None}
client = oauth.register(filtered_settings)
return await client.authorize_redirect(request, redirect_uri)


async def logout(request: Request):
request.session.pop("user", None)
return RedirectResponse(url="/")
67 changes: 53 additions & 14 deletions horao/auth/multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import Tuple, Union

import jwt
from authlib.integrations.starlette_client import OAuth # type: ignore
from starlette.authentication import (
AuthCredentials,
AuthenticationBackend,
Expand All @@ -17,12 +18,30 @@
)
from starlette.requests import HTTPConnection

from horao.auth.roles import Peer
from horao.auth.roles import Administrator, Peer, User


class MultiAuthBackend(AuthenticationBackend):
logger = logging.getLogger(__name__)

oauth_role_uri = os.getenv("OAUTH_ROLE_URI", "role")
oauth_settings = {
"name": os.getenv("OATH_NAME", "openidc"),
"client_id": os.getenv("OAUTH_CLIENT_ID"),
"client_secret": os.getenv("OAUTH_CLIENT_SECRET"),
"server_metadata_url": os.getenv("OAUTH_SERVER_METADATA_URL", None),
"api_base_url": os.getenv("OAUTH_API_BASE_URL", None),
"authorize_url": os.getenv("OAUTH_AUTHORIZE_URL", None),
"authorize_params": os.getenv("OAUTH_AUTHORIZE_PARAMS", None),
"access_token_url": os.getenv("OAUTH_ACCESS_TOKEN_URL", None),
"access_token_params": os.getenv("OAUTH_ACCESS_TOKEN_PARAMS", None),
"request_token_url": os.getenv("OAUTH_REFRESH_TOKEN_URL", None),
"request_token_params": os.getenv("OAUTH_REFRESH_TOKEN_PARAMS", None),
"client_kwargs": os.getenv(
"OAUTH_CLIENT_KWARGS", {"scope": f"openid email {oauth_role_uri}"}
),
}

def digest_authentication(
self, conn: HTTPConnection, token: str
) -> Union[None, Tuple[AuthCredentials, BaseUser]]:
Expand All @@ -43,6 +62,23 @@ def digest_authentication(
origin=host,
)

async def oauth_authentication(
self, conn: HTTPConnection
) -> Union[None, Tuple[AuthCredentials, BaseUser]]:
oauth = OAuth()
filtered_settings = {
k: v for k, v in self.oauth_settings.items() if v is not None
}
client = oauth.register(filtered_settings)
token = conn.headers["Authorization"]
user = await client.authorize_access_token(token)
if not user:
raise AuthenticationError(f"Authentication failed for {conn.client.host}") # type: ignore
role = user.get(self.oauth_role_uri, "user")
if not role or os.getenv("ADMINISTRATOR_ROLE", "administrator") not in role:
return AuthCredentials(["authenticated"]), User(user["email"])
return AuthCredentials(["authenticated"]), Administrator(user["email"])

async def authenticate(
self, conn: HTTPConnection
) -> Union[None, Tuple[AuthCredentials, BaseUser]]:
Expand All @@ -54,16 +90,19 @@ async def authenticate(
return None

auth = conn.headers["Authorization"]
try:
scheme, token = auth.split()
if scheme.lower() != "bearer":
return None
return self.digest_authentication(conn, token)
except (
ValueError,
UnicodeDecodeError,
jwt.InvalidTokenError,
binascii.Error,
) as exc:
self.logger.error(f"Invalid token for peer ({exc})")
raise AuthenticationError(f"access not allowed for {conn.client.host}") # type: ignore
if "Peer" in conn.headers:
try:
scheme, token = auth.split()
if scheme.lower() != "bearer":
return None
return self.digest_authentication(conn, token)
except (
ValueError,
UnicodeDecodeError,
jwt.InvalidTokenError,
binascii.Error,
) as exc:
self.logger.error(f"Invalid token for peer ({exc})")
raise AuthenticationError(f"access not allowed for {conn.client.host}") # type: ignore
else:
return await self.oauth_authentication(conn)
Loading

0 comments on commit 5673c91

Please sign in to comment.