django-idaustria 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. django_idaustria-0.1.0/LICENSE.md +25 -0
  2. django_idaustria-0.1.0/PKG-INFO +192 -0
  3. django_idaustria-0.1.0/README.md +140 -0
  4. django_idaustria-0.1.0/pyproject.toml +60 -0
  5. django_idaustria-0.1.0/setup.cfg +4 -0
  6. django_idaustria-0.1.0/src/django_idaustria.egg-info/PKG-INFO +192 -0
  7. django_idaustria-0.1.0/src/django_idaustria.egg-info/SOURCES.txt +29 -0
  8. django_idaustria-0.1.0/src/django_idaustria.egg-info/dependency_links.txt +1 -0
  9. django_idaustria-0.1.0/src/django_idaustria.egg-info/requires.txt +4 -0
  10. django_idaustria-0.1.0/src/django_idaustria.egg-info/top_level.txt +1 -0
  11. django_idaustria-0.1.0/src/idaustria/__init__.py +10 -0
  12. django_idaustria-0.1.0/src/idaustria/admin.py +18 -0
  13. django_idaustria-0.1.0/src/idaustria/apps.py +5 -0
  14. django_idaustria-0.1.0/src/idaustria/bind.py +52 -0
  15. django_idaustria-0.1.0/src/idaustria/conf.py +86 -0
  16. django_idaustria-0.1.0/src/idaustria/exceptions.py +36 -0
  17. django_idaustria-0.1.0/src/idaustria/identity.py +191 -0
  18. django_idaustria-0.1.0/src/idaustria/migrations/0001_initial.py +59 -0
  19. django_idaustria-0.1.0/src/idaustria/migrations/__init__.py +0 -0
  20. django_idaustria-0.1.0/src/idaustria/models.py +46 -0
  21. django_idaustria-0.1.0/src/idaustria/oidc.py +183 -0
  22. django_idaustria-0.1.0/src/idaustria/py.typed +0 -0
  23. django_idaustria-0.1.0/src/idaustria/session.py +66 -0
  24. django_idaustria-0.1.0/src/idaustria/signals.py +11 -0
  25. django_idaustria-0.1.0/src/idaustria/urls.py +10 -0
  26. django_idaustria-0.1.0/src/idaustria/views.py +163 -0
  27. django_idaustria-0.1.0/tests/test_bind.py +84 -0
  28. django_idaustria-0.1.0/tests/test_identity.py +188 -0
  29. django_idaustria-0.1.0/tests/test_oidc.py +194 -0
  30. django_idaustria-0.1.0/tests/test_session.py +62 -0
  31. django_idaustria-0.1.0/tests/test_views.py +224 -0
@@ -0,0 +1,25 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ Copyright © 2026 Christian González
5
+
6
+ Permission is hereby granted, free of charge, to any person
7
+ obtaining a copy of this software and associated documentation
8
+ files (the “Software”), to deal in the Software without
9
+ restriction, including without limitation the rights to use,
10
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the
12
+ Software is furnished to do so, subject to the following
13
+ conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-idaustria
3
+ Version: 0.1.0
4
+ Summary: ID Austria identification for Django via OpenID Connect
5
+ Author-email: Christian González <christian.gonzalez@nerdocs.at>
6
+ License: The MIT License (MIT)
7
+ =====================
8
+
9
+ Copyright © 2026 Christian González
10
+
11
+ Permission is hereby granted, free of charge, to any person
12
+ obtaining a copy of this software and associated documentation
13
+ files (the “Software”), to deal in the Software without
14
+ restriction, including without limitation the rights to use,
15
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the
17
+ Software is furnished to do so, subject to the following
18
+ conditions:
19
+
20
+ The above copyright notice and this permission notice shall be
21
+ included in all copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
24
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
25
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
27
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
28
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
29
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30
+ OTHER DEALINGS IN THE SOFTWARE.
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Environment :: Web Environment
33
+ Classifier: Framework :: Django
34
+ Classifier: Framework :: Django :: 5.2
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python
39
+ Classifier: Programming Language :: Python :: 3
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: Internet :: WWW/HTTP
43
+ Classifier: Topic :: Security
44
+ Requires-Python: >=3.12
45
+ Description-Content-Type: text/markdown
46
+ License-File: LICENSE.md
47
+ Requires-Dist: django>=5.2
48
+ Requires-Dist: requests>=2.32
49
+ Requires-Dist: PyJWT[crypto]>=2.9
50
+ Requires-Dist: django-environ>=0.13.0
51
+ Dynamic: license-file
52
+
53
+ # django-idaustria
54
+
55
+ A reusable Django app for [ID Austria](https://www.id-austria.gv.at/) identification via OpenID Connect.
56
+
57
+ 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.
58
+
59
+ ## Features
60
+
61
+ - OIDC Authorization Code Flow against the ID Austria reference and production IdP
62
+ - JWT ID token validation via the IdP's JWKS
63
+ - Optional PKCE support
64
+ - Typed `IDAustriaIdentity` view over the claims (bPK, names, birthdate, address, eIDAS level, …) with automatic decoding of the Base64-JSON `mainAddress`
65
+ - `identification_completed` signal fired on every outcome (success *and* error)
66
+ - Session-based "pending identification" decoupled from user auth — bind to a user later with `bind_pending_identification()`
67
+ - Structured `AuthorizationError` for IdP-reported failures (e.g. `access_denied`)
68
+
69
+ ## Installation
70
+
71
+ ```bash
72
+ pip install django-idaustria
73
+ ```
74
+
75
+ ```python
76
+ # settings.py
77
+ INSTALLED_APPS = [
78
+ # ...
79
+ "idaustria",
80
+ ]
81
+
82
+ # Mandatory — from your IDA-SP registration
83
+ IDAUSTRIA_CLIENT_ID = "https://your.app.identifier" # a URL, not a random string
84
+ IDAUSTRIA_CLIENT_SECRET = "<client-secret>"
85
+ IDAUSTRIA_REDIRECT_URI = "https://your.site/idaustria/callback/" # must match IDA-SPR verbatim
86
+
87
+ # Optional
88
+ IDAUSTRIA_ENV = "ref" # or "prod" (default: "ref")
89
+ IDAUSTRIA_SCOPES = ("openid", "profile")
90
+ IDAUSTRIA_USE_PKCE = False
91
+ IDAUSTRIA_TIMEOUT = 10.0
92
+ ```
93
+
94
+ Wire the URLs:
95
+
96
+ ```python
97
+ # urls.py
98
+ from django.urls import include, path
99
+
100
+ urlpatterns = [
101
+ # ...
102
+ path("idaustria/", include(("idaustria.urls", "idaustria"), namespace="idaustria")),
103
+ ]
104
+ ```
105
+
106
+ Migrate the database:
107
+ ```bash
108
+ python manage.py migrate
109
+ ```
110
+
111
+ The *django-idaustria* app only provides one Django model: `Identification`. It keeps a record of successful ID Austria identifications as a 1:n
112
+ relation to your application's user model (it uses `settings.AUTH_USER_MODEL`).
113
+
114
+ ## Usage
115
+
116
+ ### 1. Trigger the flow
117
+
118
+ 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:
119
+
120
+ 1. exchanges the authorization code for tokens (`client_secret_post`)
121
+ 2. validates the `id_token` against the IdP's JWKS
122
+ 3. stores the result in the session under a random `pending_id`
123
+ 4. fires the `identification_completed` signal
124
+ 5. returns JSON: `{"ok": true, "claims": {...}, "pending_id": "..."}`
125
+
126
+ On failure the callback returns a structured error, e.g. `{"ok": false, "error": "access_denied", "error_description": "..."}`.
127
+
128
+ ### 2. Consume the identification
129
+
130
+ ```python
131
+ from django.dispatch import receiver
132
+ from idaustria import IDAustriaIdentity, bind_pending_identification
133
+ from idaustria.signals import identification_completed
134
+
135
+
136
+ @receiver(identification_completed)
137
+ def on_idaustria_completed(sender, request, success, identity, tokens, error, **kwargs):
138
+ if not success:
139
+ # error is an IDAustriaError subclass (AuthorizationError, StateMismatchError, …)
140
+ return
141
+
142
+ # identity is an IDAustriaIdentity — typed view over the claims
143
+ bpk = identity.bpk # stable person identifier
144
+ name = identity.full_name # "Alice Example"
145
+ birthdate = identity.birthdate # datetime.date | None
146
+ address = identity.main_address # MainAddress | None (auto-decoded)
147
+ is_adult = identity.age_over(18) # bool | None
148
+ raw = identity.claims # raw dict, if you need a claim not surfaced
149
+
150
+ # You probably want to persist the identification. Do it later, when
151
+ # you know which user to bind it to (e.g. after signup completes):
152
+ ```
153
+
154
+ ```python
155
+ # later, in your signup finalization view:
156
+ def finalize_signup(request):
157
+ user = ... # the user you just created or logged in
158
+ identification = bind_pending_identification(request, user)
159
+ # identification is now an Identification row, or None if nothing was pending
160
+ ```
161
+
162
+ ### 3. Use `bpk`, not `sub`
163
+
164
+ 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.
165
+
166
+ ## Notes
167
+
168
+ - ID Austria supports only the Authorization Code Flow and requires a `client_secret`.
169
+ - `client_id` must be a URL inside your application (this is an IDA-SPR convention, not an OAuth requirement).
170
+ - 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.
171
+ - The callback endpoint does not require authentication — it deliberately runs anonymously so sign-up flows work.
172
+ - Up to 5 pending identifications are retained per session; older entries are pruned.
173
+
174
+ ## Development
175
+
176
+ See [`CLAUDE.md`](./CLAUDE.md) for architecture notes and the `demo/` project for a runnable example.
177
+
178
+ ```bash
179
+ cd demo
180
+ uv run python manage.py migrate
181
+ uv run python manage.py runserver
182
+ ```
183
+
184
+ Tests:
185
+
186
+ ```bash
187
+ uv run python -m pytest
188
+ ```
189
+
190
+ ## License
191
+
192
+ MIT — see [`LICENSE.md`](./LICENSE.md).
@@ -0,0 +1,140 @@
1
+ # django-idaustria
2
+
3
+ A reusable Django app for [ID Austria](https://www.id-austria.gv.at/) identification via OpenID Connect.
4
+
5
+ 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.
6
+
7
+ ## Features
8
+
9
+ - OIDC Authorization Code Flow against the ID Austria reference and production IdP
10
+ - JWT ID token validation via the IdP's JWKS
11
+ - Optional PKCE support
12
+ - Typed `IDAustriaIdentity` view over the claims (bPK, names, birthdate, address, eIDAS level, …) with automatic decoding of the Base64-JSON `mainAddress`
13
+ - `identification_completed` signal fired on every outcome (success *and* error)
14
+ - Session-based "pending identification" decoupled from user auth — bind to a user later with `bind_pending_identification()`
15
+ - Structured `AuthorizationError` for IdP-reported failures (e.g. `access_denied`)
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install django-idaustria
21
+ ```
22
+
23
+ ```python
24
+ # settings.py
25
+ INSTALLED_APPS = [
26
+ # ...
27
+ "idaustria",
28
+ ]
29
+
30
+ # Mandatory — from your IDA-SP registration
31
+ IDAUSTRIA_CLIENT_ID = "https://your.app.identifier" # a URL, not a random string
32
+ IDAUSTRIA_CLIENT_SECRET = "<client-secret>"
33
+ IDAUSTRIA_REDIRECT_URI = "https://your.site/idaustria/callback/" # must match IDA-SPR verbatim
34
+
35
+ # Optional
36
+ IDAUSTRIA_ENV = "ref" # or "prod" (default: "ref")
37
+ IDAUSTRIA_SCOPES = ("openid", "profile")
38
+ IDAUSTRIA_USE_PKCE = False
39
+ IDAUSTRIA_TIMEOUT = 10.0
40
+ ```
41
+
42
+ Wire the URLs:
43
+
44
+ ```python
45
+ # urls.py
46
+ from django.urls import include, path
47
+
48
+ urlpatterns = [
49
+ # ...
50
+ path("idaustria/", include(("idaustria.urls", "idaustria"), namespace="idaustria")),
51
+ ]
52
+ ```
53
+
54
+ Migrate the database:
55
+ ```bash
56
+ python manage.py migrate
57
+ ```
58
+
59
+ The *django-idaustria* app only provides one Django model: `Identification`. It keeps a record of successful ID Austria identifications as a 1:n
60
+ relation to your application's user model (it uses `settings.AUTH_USER_MODEL`).
61
+
62
+ ## Usage
63
+
64
+ ### 1. Trigger the flow
65
+
66
+ 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:
67
+
68
+ 1. exchanges the authorization code for tokens (`client_secret_post`)
69
+ 2. validates the `id_token` against the IdP's JWKS
70
+ 3. stores the result in the session under a random `pending_id`
71
+ 4. fires the `identification_completed` signal
72
+ 5. returns JSON: `{"ok": true, "claims": {...}, "pending_id": "..."}`
73
+
74
+ On failure the callback returns a structured error, e.g. `{"ok": false, "error": "access_denied", "error_description": "..."}`.
75
+
76
+ ### 2. Consume the identification
77
+
78
+ ```python
79
+ from django.dispatch import receiver
80
+ from idaustria import IDAustriaIdentity, bind_pending_identification
81
+ from idaustria.signals import identification_completed
82
+
83
+
84
+ @receiver(identification_completed)
85
+ def on_idaustria_completed(sender, request, success, identity, tokens, error, **kwargs):
86
+ if not success:
87
+ # error is an IDAustriaError subclass (AuthorizationError, StateMismatchError, …)
88
+ return
89
+
90
+ # identity is an IDAustriaIdentity — typed view over the claims
91
+ bpk = identity.bpk # stable person identifier
92
+ name = identity.full_name # "Alice Example"
93
+ birthdate = identity.birthdate # datetime.date | None
94
+ address = identity.main_address # MainAddress | None (auto-decoded)
95
+ is_adult = identity.age_over(18) # bool | None
96
+ raw = identity.claims # raw dict, if you need a claim not surfaced
97
+
98
+ # You probably want to persist the identification. Do it later, when
99
+ # you know which user to bind it to (e.g. after signup completes):
100
+ ```
101
+
102
+ ```python
103
+ # later, in your signup finalization view:
104
+ def finalize_signup(request):
105
+ user = ... # the user you just created or logged in
106
+ identification = bind_pending_identification(request, user)
107
+ # identification is now an Identification row, or None if nothing was pending
108
+ ```
109
+
110
+ ### 3. Use `bpk`, not `sub`
111
+
112
+ 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.
113
+
114
+ ## Notes
115
+
116
+ - ID Austria supports only the Authorization Code Flow and requires a `client_secret`.
117
+ - `client_id` must be a URL inside your application (this is an IDA-SPR convention, not an OAuth requirement).
118
+ - 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.
119
+ - The callback endpoint does not require authentication — it deliberately runs anonymously so sign-up flows work.
120
+ - Up to 5 pending identifications are retained per session; older entries are pruned.
121
+
122
+ ## Development
123
+
124
+ See [`CLAUDE.md`](./CLAUDE.md) for architecture notes and the `demo/` project for a runnable example.
125
+
126
+ ```bash
127
+ cd demo
128
+ uv run python manage.py migrate
129
+ uv run python manage.py runserver
130
+ ```
131
+
132
+ Tests:
133
+
134
+ ```bash
135
+ uv run python -m pytest
136
+ ```
137
+
138
+ ## License
139
+
140
+ MIT — see [`LICENSE.md`](./LICENSE.md).
@@ -0,0 +1,60 @@
1
+ [project]
2
+ name = "django-idaustria"
3
+ version = "0.1.0"
4
+ description = "ID Austria identification for Django via OpenID Connect"
5
+ readme = "README.md"
6
+ license = { file = "LICENSE.md" }
7
+ authors = [
8
+ { name = "Christian González", email = "christian.gonzalez@nerdocs.at" },
9
+ ]
10
+ requires-python = ">=3.12"
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Environment :: Web Environment",
14
+ "Framework :: Django",
15
+ "Framework :: Django :: 5.2",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Internet :: WWW/HTTP",
24
+ "Topic :: Security",
25
+ ]
26
+ dependencies = [
27
+ "django>=5.2",
28
+ "requests>=2.32",
29
+ "PyJWT[crypto]>=2.9",
30
+ "django-environ>=0.13.0",
31
+ ]
32
+
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "mkdocs>=1.6.1",
37
+ "mkdocs-material>=9.7",
38
+ "black>=26.3.1",
39
+ ]
40
+ test = [
41
+ "pytest>=8.3",
42
+ "pytest-django>=4.9",
43
+ "responses>=0.25",
44
+ "cryptography>=43",
45
+ ]
46
+ e2e = [
47
+ "playwright>=1.47",
48
+ "pytest-playwright>=0.5",
49
+ ]
50
+
51
+ [tool.pytest.ini_options]
52
+ DJANGO_SETTINGS_MODULE = "tests.settings"
53
+ pythonpath = ["src", "."]
54
+ testpaths = ["tests"]
55
+ python_files = ["test_*.py"]
56
+ addopts = "-ra -m 'not e2e'"
57
+ # E2E tests are opt-in (require running demo server + real IdP access).
58
+ markers = [
59
+ "e2e: end-to-end tests using Playwright against a live demo server",
60
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-idaustria
3
+ Version: 0.1.0
4
+ Summary: ID Austria identification for Django via OpenID Connect
5
+ Author-email: Christian González <christian.gonzalez@nerdocs.at>
6
+ License: The MIT License (MIT)
7
+ =====================
8
+
9
+ Copyright © 2026 Christian González
10
+
11
+ Permission is hereby granted, free of charge, to any person
12
+ obtaining a copy of this software and associated documentation
13
+ files (the “Software”), to deal in the Software without
14
+ restriction, including without limitation the rights to use,
15
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the
17
+ Software is furnished to do so, subject to the following
18
+ conditions:
19
+
20
+ The above copyright notice and this permission notice shall be
21
+ included in all copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
24
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
25
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
27
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
28
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
29
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30
+ OTHER DEALINGS IN THE SOFTWARE.
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Environment :: Web Environment
33
+ Classifier: Framework :: Django
34
+ Classifier: Framework :: Django :: 5.2
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python
39
+ Classifier: Programming Language :: Python :: 3
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: Internet :: WWW/HTTP
43
+ Classifier: Topic :: Security
44
+ Requires-Python: >=3.12
45
+ Description-Content-Type: text/markdown
46
+ License-File: LICENSE.md
47
+ Requires-Dist: django>=5.2
48
+ Requires-Dist: requests>=2.32
49
+ Requires-Dist: PyJWT[crypto]>=2.9
50
+ Requires-Dist: django-environ>=0.13.0
51
+ Dynamic: license-file
52
+
53
+ # django-idaustria
54
+
55
+ A reusable Django app for [ID Austria](https://www.id-austria.gv.at/) identification via OpenID Connect.
56
+
57
+ 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.
58
+
59
+ ## Features
60
+
61
+ - OIDC Authorization Code Flow against the ID Austria reference and production IdP
62
+ - JWT ID token validation via the IdP's JWKS
63
+ - Optional PKCE support
64
+ - Typed `IDAustriaIdentity` view over the claims (bPK, names, birthdate, address, eIDAS level, …) with automatic decoding of the Base64-JSON `mainAddress`
65
+ - `identification_completed` signal fired on every outcome (success *and* error)
66
+ - Session-based "pending identification" decoupled from user auth — bind to a user later with `bind_pending_identification()`
67
+ - Structured `AuthorizationError` for IdP-reported failures (e.g. `access_denied`)
68
+
69
+ ## Installation
70
+
71
+ ```bash
72
+ pip install django-idaustria
73
+ ```
74
+
75
+ ```python
76
+ # settings.py
77
+ INSTALLED_APPS = [
78
+ # ...
79
+ "idaustria",
80
+ ]
81
+
82
+ # Mandatory — from your IDA-SP registration
83
+ IDAUSTRIA_CLIENT_ID = "https://your.app.identifier" # a URL, not a random string
84
+ IDAUSTRIA_CLIENT_SECRET = "<client-secret>"
85
+ IDAUSTRIA_REDIRECT_URI = "https://your.site/idaustria/callback/" # must match IDA-SPR verbatim
86
+
87
+ # Optional
88
+ IDAUSTRIA_ENV = "ref" # or "prod" (default: "ref")
89
+ IDAUSTRIA_SCOPES = ("openid", "profile")
90
+ IDAUSTRIA_USE_PKCE = False
91
+ IDAUSTRIA_TIMEOUT = 10.0
92
+ ```
93
+
94
+ Wire the URLs:
95
+
96
+ ```python
97
+ # urls.py
98
+ from django.urls import include, path
99
+
100
+ urlpatterns = [
101
+ # ...
102
+ path("idaustria/", include(("idaustria.urls", "idaustria"), namespace="idaustria")),
103
+ ]
104
+ ```
105
+
106
+ Migrate the database:
107
+ ```bash
108
+ python manage.py migrate
109
+ ```
110
+
111
+ The *django-idaustria* app only provides one Django model: `Identification`. It keeps a record of successful ID Austria identifications as a 1:n
112
+ relation to your application's user model (it uses `settings.AUTH_USER_MODEL`).
113
+
114
+ ## Usage
115
+
116
+ ### 1. Trigger the flow
117
+
118
+ 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:
119
+
120
+ 1. exchanges the authorization code for tokens (`client_secret_post`)
121
+ 2. validates the `id_token` against the IdP's JWKS
122
+ 3. stores the result in the session under a random `pending_id`
123
+ 4. fires the `identification_completed` signal
124
+ 5. returns JSON: `{"ok": true, "claims": {...}, "pending_id": "..."}`
125
+
126
+ On failure the callback returns a structured error, e.g. `{"ok": false, "error": "access_denied", "error_description": "..."}`.
127
+
128
+ ### 2. Consume the identification
129
+
130
+ ```python
131
+ from django.dispatch import receiver
132
+ from idaustria import IDAustriaIdentity, bind_pending_identification
133
+ from idaustria.signals import identification_completed
134
+
135
+
136
+ @receiver(identification_completed)
137
+ def on_idaustria_completed(sender, request, success, identity, tokens, error, **kwargs):
138
+ if not success:
139
+ # error is an IDAustriaError subclass (AuthorizationError, StateMismatchError, …)
140
+ return
141
+
142
+ # identity is an IDAustriaIdentity — typed view over the claims
143
+ bpk = identity.bpk # stable person identifier
144
+ name = identity.full_name # "Alice Example"
145
+ birthdate = identity.birthdate # datetime.date | None
146
+ address = identity.main_address # MainAddress | None (auto-decoded)
147
+ is_adult = identity.age_over(18) # bool | None
148
+ raw = identity.claims # raw dict, if you need a claim not surfaced
149
+
150
+ # You probably want to persist the identification. Do it later, when
151
+ # you know which user to bind it to (e.g. after signup completes):
152
+ ```
153
+
154
+ ```python
155
+ # later, in your signup finalization view:
156
+ def finalize_signup(request):
157
+ user = ... # the user you just created or logged in
158
+ identification = bind_pending_identification(request, user)
159
+ # identification is now an Identification row, or None if nothing was pending
160
+ ```
161
+
162
+ ### 3. Use `bpk`, not `sub`
163
+
164
+ 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.
165
+
166
+ ## Notes
167
+
168
+ - ID Austria supports only the Authorization Code Flow and requires a `client_secret`.
169
+ - `client_id` must be a URL inside your application (this is an IDA-SPR convention, not an OAuth requirement).
170
+ - 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.
171
+ - The callback endpoint does not require authentication — it deliberately runs anonymously so sign-up flows work.
172
+ - Up to 5 pending identifications are retained per session; older entries are pruned.
173
+
174
+ ## Development
175
+
176
+ See [`CLAUDE.md`](./CLAUDE.md) for architecture notes and the `demo/` project for a runnable example.
177
+
178
+ ```bash
179
+ cd demo
180
+ uv run python manage.py migrate
181
+ uv run python manage.py runserver
182
+ ```
183
+
184
+ Tests:
185
+
186
+ ```bash
187
+ uv run python -m pytest
188
+ ```
189
+
190
+ ## License
191
+
192
+ MIT — see [`LICENSE.md`](./LICENSE.md).
@@ -0,0 +1,29 @@
1
+ LICENSE.md
2
+ README.md
3
+ pyproject.toml
4
+ src/django_idaustria.egg-info/PKG-INFO
5
+ src/django_idaustria.egg-info/SOURCES.txt
6
+ src/django_idaustria.egg-info/dependency_links.txt
7
+ src/django_idaustria.egg-info/requires.txt
8
+ src/django_idaustria.egg-info/top_level.txt
9
+ src/idaustria/__init__.py
10
+ src/idaustria/admin.py
11
+ src/idaustria/apps.py
12
+ src/idaustria/bind.py
13
+ src/idaustria/conf.py
14
+ src/idaustria/exceptions.py
15
+ src/idaustria/identity.py
16
+ src/idaustria/models.py
17
+ src/idaustria/oidc.py
18
+ src/idaustria/py.typed
19
+ src/idaustria/session.py
20
+ src/idaustria/signals.py
21
+ src/idaustria/urls.py
22
+ src/idaustria/views.py
23
+ src/idaustria/migrations/0001_initial.py
24
+ src/idaustria/migrations/__init__.py
25
+ tests/test_bind.py
26
+ tests/test_identity.py
27
+ tests/test_oidc.py
28
+ tests/test_session.py
29
+ tests/test_views.py
@@ -0,0 +1,4 @@
1
+ django>=5.2
2
+ requests>=2.32
3
+ PyJWT[crypto]>=2.9
4
+ django-environ>=0.13.0
@@ -0,0 +1,10 @@
1
+ from .bind import bind_pending_identification
2
+ from .identity import IDAustriaIdentity, MainAddress
3
+ from .signals import identification_completed
4
+
5
+ __all__ = [
6
+ "IDAustriaIdentity",
7
+ "MainAddress",
8
+ "bind_pending_identification",
9
+ "identification_completed",
10
+ ]
@@ -0,0 +1,18 @@
1
+ from django.contrib import admin
2
+
3
+ from .models import Identification
4
+
5
+
6
+ @admin.register(Identification)
7
+ class IdentificationAdmin(admin.ModelAdmin):
8
+ list_display = (
9
+ "id",
10
+ "user",
11
+ "sub",
12
+ "bpk",
13
+ "token_type",
14
+ "scope",
15
+ "created_at",
16
+ )
17
+ search_fields = ("sub", "bpk", "user__username", "user__email")
18
+ list_filter = ("token_type", "created_at")