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
IDAustriaIdentityview over the claims (bPK, names, birthdate, address, eIDAS level, …) with automatic decoding of the Base64-JSONmainAddress identification_completedsignal 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
AuthorizationErrorfor IdP-reported failures (e.g.access_denied) - Vollmacht / Vertretung (USP) — typed
MandateInfofor representation logins (both juristic and natural mandators); optionalMANDATE_REQUIREDstrict 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:
- exchanges the authorization code for tokens (
client_secret_post) - validates the
id_tokenagainst the IdP's JWKS - stores the result in the session under a random
pending_id - fires the
identification_completedsignal - 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.
Health service providers (GDA)
When the urn:eidgvat:attributes.gda attribute is approved in IDA-SPR, identity.gda returns the decoded eHVD registry record (GdaInfo: status, role/specialisation OIDs, timestamps, optional addresses) — non-mandate logins only. See docs/Gda.md.
Notes
- ID Austria supports only the Authorization Code Flow and requires a
client_secret. client_idmust 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 profilefor 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.
Metadata
Release files for django-idaustria 0.2.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| django_idaustria-0.2.5.tar.gz | 29.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_idaustria-0.2.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.7 kB
Release files / django_idaustria-0.2.5.tar.gz
| Download URL | django_idaustria-0.2.5.tar.gz |
|---|---|
| Size | 29.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
70e60cc24a365394c1a68211aea2545d38f2dc913c235d39b0abeb3f4104e633
|
|
BLAKE2b-256 checksum How to use checksums |
c71cfdde85d3a9feb423a0758e1184e5f013c9f6574301a41039ee27e00d0fea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","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}
|
Release files / django_idaustria-0.2.5-py3-none-any.whl
| Download URL | django_idaustria-0.2.5-py3-none-any.whl |
|---|---|
| Size | 26.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
33649737bfe5e15145ab4caa01f33d606c9373f1c1922af5da72a9cf648f51d2
|
|
BLAKE2b-256 checksum How to use checksums |
3201547dcac976310d563aeb13a168c55a5c85f06af05caf040e2c83233d5fc2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","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}
|
Security for django-idaustria 0.2.5
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.