Compute project management for scientific HPC centers
694
This service is the Perseus Gateway (FastAPI).
pyproject.toml for the required Python version.uv for dependency management via pipx install uv or see
the documentation.uv sync to install dependencies.uv run fastapi dev app/main.py.http://localhost:8000/gateway/docs (adjust for FASTAPI_ROOT_PATH or custom
host/port).The gateway reads its configuration from environment variables (or a .env file) defined in app/core/settings.py. Set
the variables before starting the app, e.g.
export PERSEUS_API_TOKEN="real-token"
uv run fastapi dev app/main.py
Allowed PERSEUS endpoints are listed in allowed_endpoints.json in the project root, which is versioned with the
service. The file contains an endpoints array where every object defines the relative path (without a leading slash),
the permitted HTTP methods, and whether authentication is required for matching requests. Paths accept Unix shell-style
wildcards, so you can express dynamic segments such as OIDs. Use "*" in the methods array when every verb should be
allowed. Restart the gateway after updating the file so the new rules are loaded.
{
"endpoints": [
{
"path": "country",
"methods": [
"GET"
],
"require_login": false
},
{
"path": "service/UserPortal/user",
"methods": [
"GET",
"POST"
],
"require_login": true
}
]
}
Set ALLOWED_ENDPOINTS_CONFIG_PATH to point to a different JSON file if you do not want to use the checked-in config.
| Variable | Purpose | Default |
|---|---|---|
PERSEUS_API_TOKEN | Shared secret used to authenticate against the Perseus API. | perseus_api_key |
PERSEUS_BASE_URL | Base URL of the Perseus API. | http://perseus.localhost |
PERSEUS_REQUEST_TIMEOUT | Timeout in seconds for requests to the Perseus API. | 15 |
ALLOWED_ENDPOINTS_CONFIG_PATH | JSON file containing the allowed PERSEUS endpoints. | allowed_endpoints.json |
FASTAPI_ROOT_PATH | Root path FastAPI mounts the application under. | /gateway |
ANDROMEDA_URL | Base URL of the Andromeda frontend used for login and callback redirects. | http://andromeda.localhost:6173 |
ANDROMEDA_PROFILE_PATH | Profile page path used after account-link flows. | /profile |
KEYCLOAK_URL | Base URL of the Keycloak server. | http://localhost:8080 |
KEYCLOAK_REALM | Keycloak realm name. | perseus |
KEYCLOAK_CLIENT_ID | OIDC client ID for the gateway. | perseus-gateway |
KEYCLOAK_CLIENT_SECRET | OIDC client secret for the gateway. | perseus-gateway-secret |
SESSION_COOKIE_NAME | Name of the session cookie. | perseus_session |
SESSION_COOKIE_SAMESITE | SameSite policy for the session cookie. | lax |
SESSION_COOKIE_SECURE | Whether the session cookie is only sent over HTTPS. | true |
SESSION_COOKIE_DOMAIN | Domain scope for the session cookie. | unset |
SESSION_TTL_SECONDS | Session lifetime in seconds. | 86400 |
SESSION_SECRET_KEY | Secret key for signing session cookies. | perseus_session_secret |
DATABASE_URL | Connection string for the gateway's persistence layer. | sqlite:///./perseus_gateway.db |
RATE_LIMIT_PER_MINUTE | Requests allowed per minute for a client. | 5 |
CACHE_EXPIRE_SECONDS | TTL for cached responses in seconds. | 60 |
VALIDATE_CACHE_SECONDS | TTL in seconds for cached authentication validation responses. | 5 |
LOGIN_OPTIONS | Source for /auth/login-options; supported modes are KEYCLOAK and FILE. | KEYCLOAK |
LOGIN_OPTIONS_PATH | JSON file used for login options when LOGIN_OPTIONS=FILE. | login_options.json |
USERNAME_SAFE_IDP | kc_idp_hint of the identity provider trusted to provide stable, unique usernames; users authenticating via it are matched by username before falling back to email. | ldap |
The gateway currently supports Keycloak-based login for /auth/login. It uses a hybrid session model: the browser receives a signed cookie from Starlette's SessionMiddleware, but the durable authentication state lives in the gateway database. The cookie stores the generated session_id; the database record stores the PERSEUS person OID, expiry, validity flag, and optional Keycloak token data.
/auth/login-options is adjacent to this flow, but separate from it: depending on LOGIN_OPTIONS, the gateway either fetches available providers from Keycloak or serves a static list from login_options.json.
flowchart TD
A[User starts login in Andromeda] --> B[Gateway /auth/login]
B --> C[Redirect to Keycloak]
C --> D[Keycloak authenticates user]
D --> E[Gateway /auth/callback]
E --> F{Token and state valid?}
F -- No, invalid state --> G[Return HTTP 400]
F -- No, auth failed --> H[Redirect to Andromeda with error]
F -- Yes --> I[Start PERSEUS user provisioning]
I --> J[Search PERSEUS by external_id=sub]
J --> K{Person found by sub?}
K -- Yes --> L[Update firstname lastname email and optional username]
L --> M[Use existing person OID]
K -- No --> K1{kc_idp_hint matches USERNAME_SAFE_IDP?}
K1 -- Yes --> K2[Search PERSEUS by username]
K2 --> K3{Person found by username?}
K3 -- Yes --> K4[Link identity provider=keycloak external_id=sub]
K4 --> R
K1 -- No --> N
K3 -- No --> N[Search PERSEUS by email]
N --> O{Person found by email?}
O -- Yes --> P[Link identity provider=keycloak external_id=sub]
P --> Q[Update firstname lastname email and optional username]
Q --> R[Use existing person OID]
O -- No --> S[Create new PERSEUS person with keycloak identity]
S --> T{Person created?}
T -- Yes --> U[Use new person OID]
T -- No --> V[Return HTTP 500]
M --> W
R --> W
U --> W
W -- Yes --> X[Create DB session and set signed session cookie]
X --> Y[Redirect to Andromeda]
W -- No, PERSEUS unavailable --> Z[Return HTTP 503]
W -- No, provisioning failed --> AA[Return HTTP 500]
The docker/ directory contains a self-contained Docker Compose environment with Keycloak, OpenLDAP, and a mock OIDC
provider. It lets you test all authentication flows locally without connecting to any external or production system.
Warning: All credentials in this setup are hardcoded test values and the environment is for local development only. Never deploy these containers against a production system.
| Service | Image | Default port | Purpose |
|---|---|---|---|
| Keycloak | quay.io/keycloak/keycloak:26.3 | 8080 | Identity broker, PERSEUS realm |
| OpenLDAP | osixia/openldap:latest | 1389 | Local LDAP with test users |
| Mock OIDC | ghcr.io/navikt/mock-oauth2-server:latest | 8888 | Simulates GitHub and ORCID |
cd docker/
cp .env.example .env # adjust ports only if you have conflicts
docker compose up
Keycloak starts in start-dev mode and imports the PERSEUS realm from keycloak/realm-export.json on every fresh
container start. The Keycloak admin console is available at http://localhost:8080 (admin / admin).
Note: Keycloak uses an in-process H2 database (
KC_DB=dev-file). All Keycloak data is lost when the container is removed. The realm is re-imported automatically from the JSON export on the next start — this is intentional for a reproducible local environment.
The local Keycloak instance is configured with an LDAP user federation provider connected to the OpenLDAP container. The following test users are available for authentication:
| Username | Password | Notes | |
|---|---|---|---|
m.mustermann | test | [email protected] | Standard test user |
e.musterfrau | test | [email protected] | For conflict/duplicate testing |
testuser.noemail | test1234 | — | No mail attribute (incomplete profile) |
Log in via Keycloak at http://localhost:8080/realms/perseus/account or trigger the login flow from the gateway.
The mock server exposes two issuers:
| Simulated provider | OIDC discovery URL | kc_idp_hint |
|---|---|---|
| GitHub | http://localhost:8888/github/.well-known/openid-configuration | github |
| ORCID | http://localhost:8888/orcid/.well-known/openid-configuration | orcid |
To trigger a login via a specific provider, append ?kc_idp_hint=github or ?kc_idp_hint=orcid to the Keycloak login
URL.
The mock server's interactive login page (interactiveLogin: true) lets you enter any claims at login time.
Pre-configured usernames are github-user1, github-user2, orcid-user1, and orcid-user2 (see
docker/mock-oauth2/config.json).
Important networking note: The mock server reports its URLs with mock-oauth2:8080 as the issuer host (used for
Keycloak back-channel calls). The authorizationUrl in the realm import uses localhost:8888 so the browser can reach
the consent page. These two values intentionally differ — do not change them unless you understand the full flow.
All ports are configurable via docker/.env. Edit the relevant *_PORT variable and restart Compose.
If the OpenLDAP container fails to start, the LDIF import likely contains a syntax error. Validate the files with:
ldapmodify -H ldap://localhost:1389 \
-D "cn=admin,dc=ldapmock,dc=local" \
-w adminpassword \
-f docker/ldap/ldif/02-users.ldif \
-n # dry-run, no actual changes
uv run ruff check and uv run ruff format before commiting to lint and format the code.Content type
Image
Digest
sha256:8f97d3f9c…
Size
78.6 MB
Last updated
11 days ago
docker pull pc2upb/perseus-gateway