Skip to main content

ID Austria identification for Django via OpenID Connect

Project description

django-idaustria

A reusable Django app for ID Austria identification via OpenID Connect.

It deliberately separates identification (proving a user corresponds to a real Austrian citizen) from authentication (logging in). A typical use case is a signup flow where the site needs to be sure a new account belongs to a specific person; after identification, the user continues to log in with their regular credentials.

Features

  • OIDC Authorization Code Flow against the ID Austria reference and production IdP
  • JWT ID token validation via the IdP's JWKS
  • Optional PKCE support
  • Typed IDAustriaIdentity view over the claims (bPK, names, birthdate, address, eIDAS level, …) with automatic decoding of the Base64-JSON mainAddress
  • identification_completed signal fired on every outcome (success and error)
  • Session-based "pending identification" decoupled from user auth — bind to a user later with bind_pending_identification()
  • Structured AuthorizationError for IdP-reported failures (e.g. access_denied)
  • Vollmacht / Vertretung (USP) — typed MandateInfo for representation logins (both juristic and natural mandators); optional MANDATE_REQUIRED strict mode

Installation

pip install django-idaustria
# settings.py
INSTALLED_APPS = [
    # ...
    "idaustria",
]

# Mandatory — from your IDA-SP registration
IDAUSTRIA_CLIENT_ID = "https://your.app.identifier"        # a URL, not a random string
IDAUSTRIA_CLIENT_SECRET = "<client-secret>"
IDAUSTRIA_REDIRECT_URI = "https://your.site/idaustria/callback/"  # must match IDA-SPR verbatim

# Optional
IDAUSTRIA_ENV = "ref"                # or "prod" (default: "ref")
IDAUSTRIA_SCOPES = ("openid", "profile")
IDAUSTRIA_USE_PKCE = False
IDAUSTRIA_TIMEOUT = 10.0

Wire the URLs:

# urls.py
from django.urls import include, path

urlpatterns = [
    # ...
    path("idaustria/", include(("idaustria.urls", "idaustria"), namespace="idaustria")),
]

Migrate the database:

python manage.py migrate

The django-idaustria app only provides one Django model: Identification. It keeps a record of successful ID Austria identifications as a 1:n relation to your application's user model (it uses settings.AUTH_USER_MODEL).

Usage

1. Trigger the flow

Render a link or button pointing to reverse("idaustria:start"). The user is redirected to the ID Austria IdP and, after completion, back to idaustria:callback. The callback endpoint:

  1. exchanges the authorization code for tokens (client_secret_post)
  2. validates the id_token against the IdP's JWKS
  3. stores the result in the session under a random pending_id
  4. fires the identification_completed signal
  5. returns JSON: {"ok": true, "claims": {...}, "pending_id": "..."}

On failure the callback returns a structured error, e.g. {"ok": false, "error": "access_denied", "error_description": "..."}.

2. Consume the identification

from django.dispatch import receiver
from idaustria import IDAustriaIdentity, bind_pending_identification
from idaustria.signals import identification_completed


@receiver(identification_completed)
def on_idaustria_completed(sender, request, success, identity, tokens, error, **kwargs):
    if not success:
        # error is an IDAustriaError subclass (AuthorizationError, StateMismatchError, …)
        return

    # identity is an IDAustriaIdentity — typed view over the claims
    bpk = identity.bpk                    # stable person identifier
    name = identity.full_name             # "Alice Example"
    birthdate = identity.birthdate        # datetime.date | None
    address = identity.main_address       # MainAddress | None (auto-decoded)
    is_adult = identity.age_over(18)      # bool | None
    raw = identity.claims                 # raw dict, if you need a claim not surfaced

    # You probably want to persist the identification. Do it later, when
    # you know which user to bind it to (e.g. after signup completes):
# later, in your signup finalization view:
def finalize_signup(request):
    user = ...  # the user you just created or logged in
    identification = bind_pending_identification(request, user)
    # identification is now an Identification row, or None if nothing was pending

3. Use bpk, not sub

At ID Austria the OIDC sub claim is transient — it changes on every login. The only stable identifier for a person is the bPK (bereichsspezifisches Personenkennzeichen), exposed as identity.bpk (underlying claim urn:pvpgvat:oidc.bpk). Use bpk for recognizing returning users.

4. Vollmacht / Vertretung (USP)

When the application is registered with mandate profiles in the IDA-SPR, users can sign in on behalf of another entity — typically a company via USP, but also a natural person. The id_token then carries the representative's personal claims (bpk, name, …) PLUS the mandator's data as separate claims.

@receiver(identification_completed)
def on_idaustria_completed(sender, success, identity, **kwargs):
    if not success:
        return
    if identity.is_mandated:
        m = identity.mandate
        # m.kind == "legal"  -> company (USP); identifier = KUR/Stammzahl
        # m.kind == "natural" -> private person; identifier = bPK
        print(f"{identity.full_name} acting on behalf of {m.name} ({m.mandate_type})")
    else:
        print(f"personal identification: {identity.full_name}")

After bind_pending_identification(), the same fields are denormalised on the model: identification.is_mandated, identification.mandator_kind, identification.mandator_identifier, identification.mandator_name, identification.mandate_type. The full raw mandate claims remain available via identification.claims.

See docs/Mandate.md for the full claim catalogue and the IDAUSTRIA_REQUEST_MANDATE_CLAIMS / IDAUSTRIA_MANDATE_REQUIRED settings.

Notes

  • ID Austria supports only the Authorization Code Flow and requires a client_secret.
  • client_id must be a URL inside your application (this is an IDA-SPR convention, not an OAuth requirement).
  • Attributes (name, address, age-over, …) are not selected via OIDC scopes — they are configured per service provider in IDA-SPR. Request only openid profile for compatibility.
  • The callback endpoint does not require authentication — it deliberately runs anonymously so sign-up flows work.
  • Up to 5 pending identifications are retained per session; older entries are pruned.

Development

See CLAUDE.md for architecture notes and the demo/ project for a runnable example.

cd demo
uv run python manage.py migrate
uv run python manage.py runserver

Tests:

uv run python -m pytest

License

MIT — see LICENSE.md.

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

django_idaustria-0.2.3.tar.gz (27.8 kB view details)

Uploaded Source

Built Distribution

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

django_idaustria-0.2.3-py3-none-any.whl (25.6 kB view details)

Uploaded Python 3

File details

Details for the file django_idaustria-0.2.3.tar.gz.

File metadata

  • Download URL: django_idaustria-0.2.3.tar.gz
  • Upload date:
  • Size: 27.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for django_idaustria-0.2.3.tar.gz
Algorithm Hash digest
SHA256 cb6ce968d8c9836517120db4b4c65542cbb30349995b175380b4d2250349489c
MD5 2b2218e3c53a19105619c87e5fbcd53d
BLAKE2b-256 70a340bc83e0914c0c835bfe4360c76edac8063755553b1fc24514b45c2d9ba0

See more details on using hashes here.

File details

Details for the file django_idaustria-0.2.3-py3-none-any.whl.

File metadata

  • Download URL: django_idaustria-0.2.3-py3-none-any.whl
  • Upload date:
  • Size: 25.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for django_idaustria-0.2.3-py3-none-any.whl
Algorithm Hash digest
SHA256 219fb6ab765fa48c88bb1a1d625a1a6d57d546309da4c8865ad13caab690b9b8
MD5 43dac0ab0fc4e939c66635fd732a1ada
BLAKE2b-256 f3816a3bf8bd9180546fe72d6d1e7015592ea41b4e339aa8278da61bf74ea511

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