Skip to main content

Alliance Auth OIDC Provider.

Project description

allianceauth_oidc

Fork of Solar-Helix-Independent-Transport/allianceauth-oidc-provider maintained at 6RUN0/allianceauth-oidc-provider — adds wire-level integration tests, an OIDC Conformance Suite harness, operator CLI commands, EVE-specific claims, runtime localisation (en / ru / uk), and a Russian-language README.ru.md.

A thin policy / auditing layer on top of django-oauth-toolkit that turns an Alliance Auth installation into an OpenID Connect / OAuth2 provider.

Overview

DOT does the OAuth / OIDC protocol work; this app adds Alliance-Auth-specific access control, claim mapping, safe logging, and a custom Application model with state / group whitelists.

Every authorization-code exchange runs through three independent gates. Each layer is intentional; removing any of them opens a hole, which is why the regression tests exercise each layer separately.

sequenceDiagram
    participant RP as Relying Party
    participant Auth as /o/authorize/
    participant DOT as django-oauth-toolkit
    participant Token as /o/token/
    participant Validator as AllianceAuthOAuth2Validator

    RP->>Auth: GET/POST authorize, response_type=code
    Note over Auth: Layer 1 — dispatch() runs global access_oidc + state/group whitelist
    Auth->>DOT: forward (if policy passes)
    DOT-->>RP: 302 redirect with auth code
    RP->>Token: POST code + client_secret
    Token->>Validator: validate_code(code, request)
    Note over Validator: Layer 2 — re-checks state/group on exchange
    Validator-->>Token: ok / invalid_grant
    Token->>Validator: save_bearer_token(...)
    Note over Validator: Layer 3 — last guard, PermissionDenied becomes invalid_grant
    Validator-->>RP: 200 access_token + id_token

Install

The plugin slots into a standard Alliance Auth project tree. AA's auth-helper generates this layout (myauth is the project name you picked at auth-helper init time — substitute it everywhere below):

myauth/
├── manage.py
└── myauth/
    ├── settings/
    │   ├── base.py        # AA-shipped, do not edit
    │   └── local.py       # YOUR overrides — every setting in this guide goes here
    ├── urls.py            # YOUR project URL patterns
    └── ...

If you have a different layout, the file names are what matters: locate the file that holds INSTALLED_APPS (settings) and the file that holds urlpatterns (URL conf), and apply the edits below to those.

  1. Install the fork from PyPI. The fork is published under allianceauth-oidc-provider-eveo7 to avoid colliding with the upstream allianceauth-oidc-provider release; the import path (allianceauth_oidc) is unchanged, so settings/imports stay drop-in compatible:

    pip install allianceauth-oidc-provider-eveo7
    

    Do not install both allianceauth-oidc-provider and allianceauth-oidc-provider-eveo7 into the same environment — both ship into the allianceauth_oidc/ directory and pip will refuse the second install with a file conflict. Remove the upstream package first if it is present.

    Tracking main directly is also supported:

    pip install "git+https://github.com/6RUN0/allianceauth-oidc-provider.git@current"
    
  2. In myauth/settings/local.py, append to INSTALLED_APPS:

    INSTALLED_APPS += [
        "allianceauth_oidc",
        "oauth2_provider",
    ]
    
  3. In the same myauth/settings/local.py, append the DOT + policy-validator configuration. The whole block is wrapped in an if "allianceauth_oidc" in INSTALLED_APPS … guard so the file stays valid even if the plugin is uninstalled later. Each key is explained in Configuration → OAUTH2_PROVIDER keys; paste this verbatim and tune to taste:

    from pathlib import Path
    
    if (
        "allianceauth_oidc" in INSTALLED_APPS
        and "oauth2_provider" in INSTALLED_APPS
    ):
        OAUTH2_PROVIDER_APPLICATION_MODEL = "allianceauth_oidc.AllianceAuthApplication"
        OAUTH2_PROVIDER = {
            "OIDC_ENABLED": True,
            "OIDC_RSA_PRIVATE_KEY": Path("/path/to/key").read_text(),
            "OAUTH2_VALIDATOR_CLASS": "allianceauth_oidc.auth_provider.AllianceAuthOAuth2Validator",
            "APPLICATION_ADMIN_CLASS": "allianceauth_oidc.admin.ApplicationAdmin",
            "SCOPES": {
                "openid": "User Profile",
                "email": "Registered email",
                "profile": "Main Character affiliation and Auth groups",
            },
            "PKCE_REQUIRED": True,
            "ROTATE_REFRESH_TOKEN": True,
            "REFRESH_TOKEN_REUSE_PROTECTION": True,
            "ACCESS_TOKEN_EXPIRE_SECONDS": 60,
            "REFRESH_TOKEN_EXPIRE_SECONDS": 24 * 60 * 60,
        }
    
  4. In myauth/urls.py, mount the OIDC URL conf under /o/:

    from .settings.local import INSTALLED_APPS
    
    if "allianceauth_oidc" in INSTALLED_APPS and "oauth2_provider" in INSTALLED_APPS:
        urlpatterns.append(
            path(
                "o/",
                include("allianceauth_oidc.urls", namespace="oauth2_provider"),
            )
        )
    
  5. From the project root (myauth/), run the migrations and restart Auth:

    python manage.py migrate
    supervisorctl restart myauth:    # or your process supervisor's equivalent
    

[!NOTE] If you customise the public login template (authentication/templates/public/login.html), keep the SSO link's next parameter URL-encoded — without it the OAuth flow drops query parameters after redirect (e.g. client_id is lost):

<a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{ request.GET.next | urlencode }}{% endif %}"></a>

Configuration

The previous section already shows the paste-ready settings block. This section is the per-key reference for tuning. Two surfaces:

  • DOT's OAUTH2_PROVIDER dict — required for the protocol to work.
  • Our optional ALLIANCEAUTH_OIDC_* Django settings — for logging / claim shape / portrait template. All of them have sensible defaults.

Both go into myauth/settings/local.py next to the install snippet.

OAUTH2_PROVIDER keys

Setting Recommended value Why
OAUTH2_PROVIDER_APPLICATION_MODEL "allianceauth_oidc.AllianceAuthApplication" Required. Without this the state / group access policy is silently bypassed. Set as a top-level Django setting, not inside OAUTH2_PROVIDER.
OIDC_ENABLED True Required. Turns on DOT's OIDC layer (discovery, JWKS, id_token signing).
OIDC_RSA_PRIVATE_KEY Path("/path/to/key").read_text() Required. RSA key DOT uses to sign id_tokens. See DOT docs for generation.
OAUTH2_VALIDATOR_CLASS "allianceauth_oidc.auth_provider.AllianceAuthOAuth2Validator" Required. Implements the three-layer policy and AA-specific claims.
APPLICATION_ADMIN_CLASS "allianceauth_oidc.admin.ApplicationAdmin" Required. AA-aware admin for the custom Application model.
SCOPES {"openid": "...", "email": "...", "profile": "..."} Required. Scopes shown on the consent screen. Strings are user-facing labels.
PKCE_REQUIRED True Recommended per RFC 9700. Disable only if you control all clients and they support PKCE.
ROTATE_REFRESH_TOKEN True Recommended. Mints a fresh refresh token on every use; old one is invalidated.
REFRESH_TOKEN_REUSE_PROTECTION True Recommended. Replay-defence per RFC 6819 §5.2.2.3 — a refresh token presented twice revokes the entire token family.
ACCESS_TOKEN_EXPIRE_SECONDS 60 Trade-off: shorter access-token TTL forces RPs to refresh more often (faster reaction to revocation, more token-endpoint round-trips); longer means slower revocation propagation but lighter traffic.
REFRESH_TOKEN_EXPIRE_SECONDS 24*60*60 Per-deployment risk tolerance.

Custom settings (ALLIANCEAUTH_OIDC_*)

Setting Default Effect
ALLIANCEAUTH_OIDC_LOG_MASKED_SECRETS False Replace <redacted> with masked fragments (he…il) in app debug logs. Enable only if log storage is restricted.
ALLIANCEAUTH_OIDC_LOG_MASK_HEAD 2 Visible characters at the start of a masked secret.
ALLIANCEAUTH_OIDC_LOG_MASK_TAIL 2 Visible characters at the end.
ALLIANCEAUTH_OIDC_EVE_CLAIM_PREFIX "eve_" Prefix for the EVE-specific claims. "" removes the prefix (collision risk); any other value namespaces them.
ALLIANCEAUTH_OIDC_EVE_CLAIM_SCOPE "profile" OIDC scope that gates the EVE claims. Class-level binding — changing it requires an Auth restart.
ALLIANCEAUTH_OIDC_PORTRAIT_URL_TEMPLATE "https://images.evetech.net/characters/{character_id}/portrait?size={size}" URL template for the picture claim. Both {character_id} and {size} placeholders are required; a malformed template skips the claim with a warning.
ALLIANCEAUTH_OIDC_PORTRAIT_SIZE 128 Pixel size requested from the portrait service. EVE supports 32 / 64 / 128 / 256 / 512 / 1024.

Periodic cleanup of expired tokens (Celery Beat)

The clear_expired_tokens task is shipped but not scheduled by default — operators add it to CELERYBEAT_SCHEDULE in myauth/settings/local.py:

from celery.schedules import crontab

CELERYBEAT_SCHEDULE["allianceauth_oidc_clear_expired_tokens"] = {
    "task": "allianceauth_oidc.clear_expired_tokens",
    "schedule": crontab(minute=0, hour="*/2"),  # every 2 hours
}

The task is idempotent (deletes only already-expired rows); broker authentication is the defence against unauthorised re-runs.

Reference

Endpoints

Endpoint Path Notes
Authorization /o/authorize/ Policy-aware (three-layer gate). Overridden in this app.
Token /o/token/ Audit signal + safe debug logging. Overridden in this app.
UserInfo /o/userinfo/ DOT default.
Discovery /o/.well-known/openid-configuration/ DOT default.
JWKS /o/.well-known/jwks.json DOT default.
Token revocation /o/revoke_token/ RFC 7009. DOT default.
Token introspection /o/introspect/ RFC 7662. DOT default.
RP-initiated logout /o/logout/ DOT default.
Issuer (iss claim) https://your.host/o/ Whatever your discovery URL resolves to.

Claims

Every standard OIDC claim is emitted under the scope conventionally associated with it; the groups and eve_* claims are AA-specific and ride the profile scope by default so RPs that already request openid profile get them without extra setup.

Claim Source Scope
sub User.pk (DOT default) openid
email user.email email
name user.profile.main_character.character_name profile
picture Portrait URL for the main character (see ALLIANCEAUTH_OIDC_PORTRAIT_URL_TEMPLATE) profile
groups user.groups[*].name, with user.profile.state.name appended profile
locale user.profile.language profile
eve_character_id main_character.character_id profile (set by ALLIANCEAUTH_OIDC_EVE_CLAIM_SCOPE)
eve_corporation_id / _name / _ticker main_character.corporation_* same
eve_alliance_id / _name / _ticker main_character.alliance_* (omitted for NPC corps without an alliance) same

The eve_* prefix is configurable. Empty values are omitted from the payload, not emitted as null, so RPs that key off claim in payload behave consistently.

The groups claim is capped at 256 entries to keep id_tokens under the typical 8 KB header / cookie limit. The state name is appended after truncation so consumers that rely on the state being present don't lose it silently. Override the cap by subclassing AllianceAuthOAuth2Validator and overriding the MAX_GROUPS_IN_CLAIM class attribute.

Audit signal

Every successful token-issuance fires the oidc_token_issued Django signal (allianceauth_oidc.signals). The default receiver writes a redacted audit log entry; connect your own receiver to forward to a SIEM, write to a separate audit table, or push into an alerting pipeline:

from django.dispatch import receiver
from allianceauth_oidc.signals import oidc_token_issued

@receiver(oidc_token_issued)
def forward_to_siem(sender, *, app, user, request, body, **kwargs):
    # `body` is already redacted (build_oidc_debug_meta); never re-add raw secrets.
    ...

Don't extend TokenView to do this — the signal is the documented integration point and survives DOT version bumps that change view internals.

Operations

Operator commands

Four manage.py commands cover the day-2 operational tasks without opening the admin UI. All accept --format=table|json|csv; destructive commands honour --dry-run.

Command Purpose Destructive? Key flags
oidc_create_app Bootstrap a new OIDC application (CI / Ansible-friendly). Prints the raw client_secret once. yes --name, --user-id, --redirect-uri, --state, --group, --client-type, --grant-type, --debug-mode
oidc_rotate_secret Rotate client_secret on an existing app. Existing tokens stay valid until expiry. yes --client-id, --dry-run
oidc_revoke_user_tokens Revoke every active access + refresh token for a user (off-boarding, compromise response). Idempotent. yes --username, --dry-run
oidc_audit_tokens Read-only listing of active tokens. no --username, --client-id, --include-expired
python manage.py oidc_create_app \
    --name="Grafana" --user-id=1 \
    --redirect-uri="https://grafana.example/login/generic_oauth" \
    --state=Member --group=Operators --format=json

python manage.py oidc_rotate_secret --client-id=abc123 --dry-run
python manage.py oidc_revoke_user_tokens --username=alice
python manage.py oidc_audit_tokens --client-id=abc123 --format=csv

create_app writes a Django admin LogEntry on success so the action is visible in /admin/'s history without code changes; the destructive commands log at INFO / WARNING.

Debug logging

Per-application Debug Mode (toggled in the admin) escalates token-flow logs from DEBUG to INFO. Raw token values and secrets are never logged; the _LOG_MASKED_SECRETS knob (see Custom settings) controls whether they appear as <redacted> or masked fragments.

When debugging an app, look for lines like:

[01/Jan/2099 00:00:00] INFO [extensions.allianceauth_oidc.views:78] OIDC DEBUG token issued
app_id=1 client_id=abc123 user_id=42
meta={'grant_type': 'authorization_code', ..., 'access_token': '<redacted>', 'id_token': '<redacted>'}

Paste the (separately captured) id_token into https://jwt.io/ to inspect claims. The two non-obvious fields:

  • iss — issuer; must match the value the RP has configured exactly.
  • sub — the user PK; useful for "why did this user end up here?" triage.

If you need the public key to verify the signature on jwt.io and have only the private key on disk:

ssh-keygen -y -e -m pem -f /path/to/key

Operational hardening (operator responsibility)

The provider implements the OAuth2 / OIDC protocol semantics; runtime hardening below is intentionally left to the deployment so it integrates with whatever edge / infra you already operate.

  • Rate-limit /o/token/ and /o/authorize/. Neither endpoint is rate-limited by this app; brute-force defence belongs at the edge (nginx limit_req, Cloudflare, a WAF) or via django-ratelimit in your Auth deployment. Without it, a network-level attacker can probe client_secret / code / refresh_token values at line speed.
  • Authenticate the Celery broker. clear_expired_tokens is published to whichever broker your AA install uses; if that broker is reachable by untrusted parties, a malicious task submission can repeatedly invoke cleanup. The task is idempotent, but broker auth + network ACLs are the defensive layer.
  • Security headers. This app does not set CSP / HSTS / X-Frame-Options / X-Content-Type-Options; rely on Alliance Auth's middleware stack and Django's SECURE_* settings to add them globally.

Integrations

Register an application

In /admin/allianceauth_oidc/, create an Alliance Auth application:

Field Value Notes
User any (e.g. 1) Owner — passed through to DOT but not used in this app's policy.
Client type confidential Public clients are out of scope; we don't ship a public-client recipe.
Authorization grant type Authorization code The only flow this app's policy is hardened against.
Client secret auto-generated Save it before clicking save when HASH_CLIENT_SECRET is on (default).
Algorithm RSA with SHA-2 256 Matches OIDC_RSA_PRIVATE_KEY.
States / Groups whitelist Empty ⇒ open; non-empty ⇒ user must be in a listed state OR group.

Every user who logs into any registered application also needs the global allianceauth_oidc.access_oidc permission. Without it the dispatch-layer gate (Layer 1) returns PermissionDenied regardless of state / group whitelist.

Grafana

Tested without group-to-team mapping (group → team mapping requires Grafana Cloud / Enterprise and is out of scope here).

[server]
root_url = <URL of your grafana server>

[auth.generic_oauth]
enabled = true
name = <Your Auth Name>
allow_sign_up = true
client_id = <client_id>
client_secret = <unhashed client_secret>
scopes = openid,email,profile
empty_scopes = false
email_attribute_path = email
name_attribute_path = name
auth_url = https://<your.auth.url>/o/authorize/
token_url = https://<your.auth.url>/o/token/
api_url = https://<your.auth.url>/o/userinfo/

WikiJS

Pre-create the groups you want WikiJS users to land in on the AA side; WikiJS will map them at login. (Create an Administrators group to grant the wiki admin pages.)

WikiJS field Value
Skip User Profile off
Email claim email
Display Name Claim name
Map Groups on
Groups Claim groups
Allow Self Registration on

Development

Nox sessions

Session Purpose In default nox run?
lint pre-commit (ruff, mypy, basedpyright, …) yes
tests Django test suite (parallel) yes
coverage tests + term / HTML / XML coverage reports no
typecheck mypy + basedpyright (subset of lint, run separately for fast feedback) no
audit pip-audit no
markdown_lint rumdl + lychee + vale (each tool optional) no
makemessages / compilemessages i18n catalogue refresh + compile no
makemigrations generate Django migrations under test settings no
integration wire-level mock-RP via LiveServerTestCase no
conformance OIDC Conformance Suite via docker-compose no

Integration tests (nox -s integration)

tests/test_integration_mock_rp.py boots a LiveServerTestCase and walks the OIDC code flow with requests + jwcrypto, validating the id_token signature against a JWKS retrieved over the wire. This catches regressions the standard nox -s tests set cannot — Django's test client short-circuits the WSGI layer, so absolute-URL bugs in iss / jwks_uri and Bearer-header / cookie issues only surface here.

The session forces --parallel=1: LiveServerTestCase shares its DB connection with the WSGI thread, which does not survive the test runner's fork().

Conformance Suite (nox -s conformance)

Runs the OpenID Foundation Conformance Suite against the provider via Docker Compose: MongoDB + the suite + a provider container. The default plan is driven through the suite's REST API by tests/conformance/run_plan.py.

This is the level above our own integration tests — it catches spec edge cases that our regression tests wouldn't think to check. Run before tagging a release. See tests/conformance/README.md for prerequisites, the manual / iterative workflow, configuration overrides, and the list of known conformance findings to triage.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

allianceauth_oidc_provider_eveo7-0.1.0b3.tar.gz (59.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

File details

Details for the file allianceauth_oidc_provider_eveo7-0.1.0b3.tar.gz.

File metadata

  • Download URL: allianceauth_oidc_provider_eveo7-0.1.0b3.tar.gz
  • Upload date:
  • Size: 59.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.10 {"installer":{"name":"uv","version":"0.11.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for allianceauth_oidc_provider_eveo7-0.1.0b3.tar.gz
Algorithm Hash digest
SHA256 ce7df95442929e8552cef52f78bf7cd343626acc39762ed4af128ca37c83ca1f
MD5 8ab924b423cfa5c0b16ef27382047951
BLAKE2b-256 55618bf93fe7c8d4ed83053ea47e3cdf1fe5b1989dcedfe31c42598815494cf7

See more details on using hashes here.

File details

Details for the file allianceauth_oidc_provider_eveo7-0.1.0b3-py3-none-any.whl.

File metadata

  • Download URL: allianceauth_oidc_provider_eveo7-0.1.0b3-py3-none-any.whl
  • Upload date:
  • Size: 72.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.10 {"installer":{"name":"uv","version":"0.11.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for allianceauth_oidc_provider_eveo7-0.1.0b3-py3-none-any.whl
Algorithm Hash digest
SHA256 50c9c89a0988723371491bb45fe90eb9e8df2bc0f0286fb53729d7d30ed801e1
MD5 3c66466baaf53fd6ca4bd61e059f4c2e
BLAKE2b-256 dbb91154f758a09853a3c5e39268019f30eba2e36d7feca289de0161a1102d16

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page