allianceauth-oidc-provider-eveo7 0.1.0b3__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 (48) hide show
  1. allianceauth_oidc_provider_eveo7-0.1.0b3/LICENSE +21 -0
  2. allianceauth_oidc_provider_eveo7-0.1.0b3/PKG-INFO +484 -0
  3. allianceauth_oidc_provider_eveo7-0.1.0b3/README.md +456 -0
  4. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/__init__.py +3 -0
  5. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/admin.py +35 -0
  6. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/app_settings.py +211 -0
  7. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/apps.py +33 -0
  8. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/auth_hooks.py +1 -0
  9. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/auth_provider.py +361 -0
  10. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/constants.py +31 -0
  11. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/locale/django.pot +137 -0
  12. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/locale/en/LC_MESSAGES/django.mo +0 -0
  13. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/locale/en/LC_MESSAGES/django.po +138 -0
  14. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/locale/ru/LC_MESSAGES/django.mo +0 -0
  15. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/locale/ru/LC_MESSAGES/django.po +145 -0
  16. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/locale/uk/LC_MESSAGES/django.mo +0 -0
  17. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/locale/uk/LC_MESSAGES/django.po +148 -0
  18. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/management/__init__.py +1 -0
  19. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/management/commands/__init__.py +1 -0
  20. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/management/commands/_format.py +112 -0
  21. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/management/commands/oidc_audit_tokens.py +97 -0
  22. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/management/commands/oidc_create_app.py +196 -0
  23. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/management/commands/oidc_revoke_user_tokens.py +128 -0
  24. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/management/commands/oidc_rotate_secret.py +101 -0
  25. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/migrations/0001_initial.py +130 -0
  26. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/migrations/0002_alter_allianceauthapplication_options_and_more.py +25 -0
  27. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/migrations/0003_remove_allianceauthapplication_logo_and_more.py +37 -0
  28. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/migrations/0004_allianceauthapplication_allowed_origins_and_more.py +38 -0
  29. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/migrations/0005_alter_allianceauthapplication_authorization_grant_type.py +33 -0
  30. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/migrations/0006_alter_allianceauthapplication_debug_mode.py +23 -0
  31. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/migrations/0007_alter_allianceauthapplication_logo_url.py +22 -0
  32. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/migrations/0008_alter_allianceauthapplication_logo_url.py +28 -0
  33. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/migrations/0009_alter_allianceauthapplication_options.py +23 -0
  34. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/migrations/__init__.py +0 -0
  35. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/models.py +69 -0
  36. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/py.typed +0 -0
  37. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/security.py +287 -0
  38. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/signals.py +115 -0
  39. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/tasks.py +48 -0
  40. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/templates/allianceauth_oidc/authorize.html +81 -0
  41. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/templates/allianceauth_oidc/denied.html +77 -0
  42. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/templates/allianceauth_oidc/logout_confirm.html +37 -0
  43. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/templatetags/__init__.py +1 -0
  44. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/templatetags/oidc_tags.py +36 -0
  45. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/urls.py +55 -0
  46. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/utils.py +239 -0
  47. allianceauth_oidc_provider_eveo7-0.1.0b3/allianceauth_oidc/views.py +357 -0
  48. allianceauth_oidc_provider_eveo7-0.1.0b3/pyproject.toml +187 -0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 AaronKable
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,484 @@
1
+ Metadata-Version: 2.4
2
+ Name: allianceauth-oidc-provider-eveo7
3
+ Version: 0.1.0b3
4
+ Summary: Alliance Auth OIDC Provider.
5
+ Author-email: AaronKable <aaronkable@gmail.com>
6
+ Maintainer-email: Boris Talovikov <boris.t.66@gmail.com>
7
+ Requires-Python: >=3.10,<3.14
8
+ Description-Content-Type: text/markdown
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Framework :: Django
11
+ Classifier: Framework :: Django :: 4.2
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ License-File: LICENSE
21
+ Requires-Dist: allianceauth>=4,<5
22
+ Requires-Dist: django-oauth-toolkit>=3.2,<4
23
+ Project-URL: Homepage, https://github.com/6RUN0/allianceauth-oidc-provider
24
+ Project-URL: Source, https://github.com/6RUN0/allianceauth-oidc-provider
25
+ Project-URL: Tracker, https://github.com/6RUN0/allianceauth-oidc-provider/issues
26
+ Project-URL: Upstream, https://github.com/Solar-Helix-Independent-Transport/allianceauth-oidc-provider
27
+
28
+ # allianceauth_oidc
29
+
30
+ > Fork of
31
+ > [Solar-Helix-Independent-Transport/allianceauth-oidc-provider](https://github.com/Solar-Helix-Independent-Transport/allianceauth-oidc-provider)
32
+ > maintained at
33
+ > [6RUN0/allianceauth-oidc-provider](https://github.com/6RUN0/allianceauth-oidc-provider) — adds
34
+ > wire-level integration tests, an OIDC Conformance Suite harness, operator CLI commands,
35
+ > EVE-specific claims, runtime localisation (en / ru / uk), and a Russian-language
36
+ > [README.ru.md](README.ru.md).
37
+
38
+ A thin policy / auditing layer on top of
39
+ [`django-oauth-toolkit`](https://django-oauth-toolkit.readthedocs.io/) that turns an
40
+ [Alliance Auth](https://gitlab.com/allianceauth/allianceauth) installation into an OpenID Connect /
41
+ OAuth2 provider.
42
+
43
+ - [Overview](#overview)
44
+ - [Install](#install)
45
+ - [Configuration](#configuration)
46
+ - [Reference](#reference)
47
+ - [Operations](#operations)
48
+ - [Integrations](#integrations)
49
+ - [Development](#development)
50
+
51
+ ## Overview
52
+
53
+ DOT does the OAuth / OIDC protocol work; this app adds Alliance-Auth-specific access control,
54
+ claim mapping, safe logging, and a custom `Application` model with `state` / `group` whitelists.
55
+
56
+ Every authorization-code exchange runs through three independent gates. Each layer is intentional;
57
+ removing any of them opens a hole, which is why the regression tests exercise each layer
58
+ separately.
59
+
60
+ ```mermaid
61
+ sequenceDiagram
62
+ participant RP as Relying Party
63
+ participant Auth as /o/authorize/
64
+ participant DOT as django-oauth-toolkit
65
+ participant Token as /o/token/
66
+ participant Validator as AllianceAuthOAuth2Validator
67
+
68
+ RP->>Auth: GET/POST authorize, response_type=code
69
+ Note over Auth: Layer 1 — dispatch() runs global access_oidc + state/group whitelist
70
+ Auth->>DOT: forward (if policy passes)
71
+ DOT-->>RP: 302 redirect with auth code
72
+ RP->>Token: POST code + client_secret
73
+ Token->>Validator: validate_code(code, request)
74
+ Note over Validator: Layer 2 — re-checks state/group on exchange
75
+ Validator-->>Token: ok / invalid_grant
76
+ Token->>Validator: save_bearer_token(...)
77
+ Note over Validator: Layer 3 — last guard, PermissionDenied becomes invalid_grant
78
+ Validator-->>RP: 200 access_token + id_token
79
+ ```
80
+
81
+ ## Install
82
+
83
+ The plugin slots into a standard Alliance Auth project tree. AA's `auth-helper` generates this
84
+ layout (`myauth` is the project name you picked at `auth-helper init` time — substitute it
85
+ everywhere below):
86
+
87
+ ```text
88
+ myauth/
89
+ ├── manage.py
90
+ └── myauth/
91
+ ├── settings/
92
+ │ ├── base.py # AA-shipped, do not edit
93
+ │ └── local.py # YOUR overrides — every setting in this guide goes here
94
+ ├── urls.py # YOUR project URL patterns
95
+ └── ...
96
+ ```
97
+
98
+ If you have a different layout, the file *names* are what matters: locate the file that holds
99
+ `INSTALLED_APPS` (settings) and the file that holds `urlpatterns` (URL conf), and apply the
100
+ edits below to those.
101
+
102
+ 1. Install the fork from PyPI. The fork is published under
103
+ `allianceauth-oidc-provider-eveo7` to avoid colliding with the upstream
104
+ `allianceauth-oidc-provider` release; the import path
105
+ (`allianceauth_oidc`) is unchanged, so settings/imports stay drop-in
106
+ compatible:
107
+
108
+ ```sh
109
+ pip install allianceauth-oidc-provider-eveo7
110
+ ```
111
+
112
+ Do **not** install both `allianceauth-oidc-provider` and
113
+ `allianceauth-oidc-provider-eveo7` into the same environment — both
114
+ ship into the `allianceauth_oidc/` directory and pip will refuse the
115
+ second install with a file conflict. Remove the upstream package
116
+ first if it is present.
117
+
118
+ Tracking `main` directly is also supported:
119
+
120
+ ```sh
121
+ pip install "git+https://github.com/6RUN0/allianceauth-oidc-provider.git@current"
122
+ ```
123
+
124
+ 2. **In `myauth/settings/local.py`**, append to `INSTALLED_APPS`:
125
+
126
+ ```python
127
+ INSTALLED_APPS += [
128
+ "allianceauth_oidc",
129
+ "oauth2_provider",
130
+ ]
131
+ ```
132
+
133
+ 3. **In the same `myauth/settings/local.py`**, append the DOT + policy-validator configuration.
134
+ The whole block is wrapped in an `if "allianceauth_oidc" in INSTALLED_APPS …` guard so the
135
+ file stays valid even if the plugin is uninstalled later. Each key is explained in
136
+ [Configuration → OAUTH2_PROVIDER keys](#oauth2_provider-keys); paste this verbatim and tune
137
+ to taste:
138
+
139
+ ```python
140
+ from pathlib import Path
141
+
142
+ if (
143
+ "allianceauth_oidc" in INSTALLED_APPS
144
+ and "oauth2_provider" in INSTALLED_APPS
145
+ ):
146
+ OAUTH2_PROVIDER_APPLICATION_MODEL = "allianceauth_oidc.AllianceAuthApplication"
147
+ OAUTH2_PROVIDER = {
148
+ "OIDC_ENABLED": True,
149
+ "OIDC_RSA_PRIVATE_KEY": Path("/path/to/key").read_text(),
150
+ "OAUTH2_VALIDATOR_CLASS": "allianceauth_oidc.auth_provider.AllianceAuthOAuth2Validator",
151
+ "APPLICATION_ADMIN_CLASS": "allianceauth_oidc.admin.ApplicationAdmin",
152
+ "SCOPES": {
153
+ "openid": "User Profile",
154
+ "email": "Registered email",
155
+ "profile": "Main Character affiliation and Auth groups",
156
+ },
157
+ "PKCE_REQUIRED": True,
158
+ "ROTATE_REFRESH_TOKEN": True,
159
+ "REFRESH_TOKEN_REUSE_PROTECTION": True,
160
+ "ACCESS_TOKEN_EXPIRE_SECONDS": 60,
161
+ "REFRESH_TOKEN_EXPIRE_SECONDS": 24 * 60 * 60,
162
+ }
163
+ ```
164
+
165
+ 4. **In `myauth/urls.py`**, mount the OIDC URL conf under `/o/`:
166
+
167
+ ```python
168
+ from .settings.local import INSTALLED_APPS
169
+
170
+ if "allianceauth_oidc" in INSTALLED_APPS and "oauth2_provider" in INSTALLED_APPS:
171
+ urlpatterns.append(
172
+ path(
173
+ "o/",
174
+ include("allianceauth_oidc.urls", namespace="oauth2_provider"),
175
+ )
176
+ )
177
+ ```
178
+
179
+ 5. From the project root (`myauth/`), run the migrations and restart Auth:
180
+
181
+ ```sh
182
+ python manage.py migrate
183
+ supervisorctl restart myauth: # or your process supervisor's equivalent
184
+ ```
185
+
186
+ > [!NOTE]
187
+ > If you customise the public login template
188
+ > (`authentication/templates/public/login.html`), keep the SSO link's `next` parameter
189
+ > URL-encoded — without it the OAuth flow drops query parameters after redirect (e.g.
190
+ > `client_id` is lost):
191
+ >
192
+ > ```html
193
+ > <a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{ request.GET.next | urlencode }}{% endif %}"></a>
194
+ > ```
195
+
196
+ ## Configuration
197
+
198
+ The previous section already shows the paste-ready settings block. This section is the per-key
199
+ reference for tuning. Two surfaces:
200
+
201
+ - DOT's `OAUTH2_PROVIDER` dict — required for the protocol to work.
202
+ - Our optional `ALLIANCEAUTH_OIDC_*` Django settings — for logging / claim shape / portrait
203
+ template. All of them have sensible defaults.
204
+
205
+ Both go into `myauth/settings/local.py` next to the install snippet.
206
+
207
+ ### OAUTH2_PROVIDER keys
208
+
209
+ | Setting | Recommended value | Why |
210
+ |---|---|---|
211
+ | `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`. |
212
+ | `OIDC_ENABLED` | `True` | **Required.** Turns on DOT's OIDC layer (discovery, JWKS, id_token signing). |
213
+ | `OIDC_RSA_PRIVATE_KEY` | `Path("/path/to/key").read_text()` | **Required.** RSA key DOT uses to sign id_tokens. See [DOT docs](https://django-oauth-toolkit.readthedocs.io/en/stable/oidc.html#creating-rsa-private-key) for generation. |
214
+ | `OAUTH2_VALIDATOR_CLASS` | `"allianceauth_oidc.auth_provider.AllianceAuthOAuth2Validator"` | **Required.** Implements the three-layer policy and AA-specific claims. |
215
+ | `APPLICATION_ADMIN_CLASS` | `"allianceauth_oidc.admin.ApplicationAdmin"` | **Required.** AA-aware admin for the custom `Application` model. |
216
+ | `SCOPES` | `{"openid": "...", "email": "...", "profile": "..."}` | **Required.** Scopes shown on the consent screen. Strings are user-facing labels. |
217
+ | `PKCE_REQUIRED` | `True` | Recommended per RFC 9700. Disable only if you control all clients and they support PKCE. |
218
+ | `ROTATE_REFRESH_TOKEN` | `True` | Recommended. Mints a fresh refresh token on every use; old one is invalidated. |
219
+ | `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. |
220
+ | `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. |
221
+ | `REFRESH_TOKEN_EXPIRE_SECONDS` | `24*60*60` | Per-deployment risk tolerance. |
222
+
223
+ ### Custom settings (ALLIANCEAUTH_OIDC_*)
224
+
225
+ | Setting | Default | Effect |
226
+ |---|---|---|
227
+ | `ALLIANCEAUTH_OIDC_LOG_MASKED_SECRETS` | `False` | Replace `<redacted>` with masked fragments (`he…il`) in app debug logs. Enable only if log storage is restricted. |
228
+ | `ALLIANCEAUTH_OIDC_LOG_MASK_HEAD` | `2` | Visible characters at the start of a masked secret. |
229
+ | `ALLIANCEAUTH_OIDC_LOG_MASK_TAIL` | `2` | Visible characters at the end. |
230
+ | `ALLIANCEAUTH_OIDC_EVE_CLAIM_PREFIX` | `"eve_"` | Prefix for the EVE-specific claims. `""` removes the prefix (collision risk); any other value namespaces them. |
231
+ | `ALLIANCEAUTH_OIDC_EVE_CLAIM_SCOPE` | `"profile"` | OIDC scope that gates the EVE claims. **Class-level binding** — changing it requires an Auth restart. |
232
+ | `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. |
233
+ | `ALLIANCEAUTH_OIDC_PORTRAIT_SIZE` | `128` | Pixel size requested from the portrait service. EVE supports 32 / 64 / 128 / 256 / 512 / 1024. |
234
+
235
+ ### Periodic cleanup of expired tokens (Celery Beat)
236
+
237
+ The `clear_expired_tokens` task is shipped but **not** scheduled by default — operators add it
238
+ to `CELERYBEAT_SCHEDULE` in `myauth/settings/local.py`:
239
+
240
+ ```python
241
+ from celery.schedules import crontab
242
+
243
+ CELERYBEAT_SCHEDULE["allianceauth_oidc_clear_expired_tokens"] = {
244
+ "task": "allianceauth_oidc.clear_expired_tokens",
245
+ "schedule": crontab(minute=0, hour="*/2"), # every 2 hours
246
+ }
247
+ ```
248
+
249
+ The task is idempotent (deletes only already-expired rows); broker authentication is the defence
250
+ against unauthorised re-runs.
251
+
252
+ ## Reference
253
+
254
+ ### Endpoints
255
+
256
+ | Endpoint | Path | Notes |
257
+ |---|---|---|
258
+ | Authorization | `/o/authorize/` | Policy-aware (three-layer gate). Overridden in this app. |
259
+ | Token | `/o/token/` | Audit signal + safe debug logging. Overridden in this app. |
260
+ | UserInfo | `/o/userinfo/` | DOT default. |
261
+ | Discovery | `/o/.well-known/openid-configuration/` | DOT default. |
262
+ | JWKS | `/o/.well-known/jwks.json` | DOT default. |
263
+ | Token revocation | `/o/revoke_token/` | RFC 7009. DOT default. |
264
+ | Token introspection | `/o/introspect/` | RFC 7662. DOT default. |
265
+ | RP-initiated logout | `/o/logout/` | DOT default. |
266
+ | Issuer (`iss` claim) | `https://your.host/o/` | Whatever your discovery URL resolves to. |
267
+
268
+ ### Claims
269
+
270
+ Every standard OIDC claim is emitted under the scope conventionally associated with it; the
271
+ `groups` and `eve_*` claims are AA-specific and ride the `profile` scope by default so RPs that
272
+ already request `openid profile` get them without extra setup.
273
+
274
+ | Claim | Source | Scope |
275
+ |---|---|---|
276
+ | `sub` | `User.pk` (DOT default) | `openid` |
277
+ | `email` | `user.email` | `email` |
278
+ | `name` | `user.profile.main_character.character_name` | `profile` |
279
+ | `picture` | Portrait URL for the main character (see `ALLIANCEAUTH_OIDC_PORTRAIT_URL_TEMPLATE`) | `profile` |
280
+ | `groups` | `user.groups[*].name`, with `user.profile.state.name` appended | `profile` |
281
+ | `locale` | `user.profile.language` | `profile` |
282
+ | `eve_character_id` | `main_character.character_id` | `profile` (set by `ALLIANCEAUTH_OIDC_EVE_CLAIM_SCOPE`) |
283
+ | `eve_corporation_id` / `_name` / `_ticker` | `main_character.corporation_*` | same |
284
+ | `eve_alliance_id` / `_name` / `_ticker` | `main_character.alliance_*` (omitted for NPC corps without an alliance) | same |
285
+
286
+ The `eve_*` prefix is configurable. Empty values are **omitted** from the payload, not emitted as
287
+ `null`, so RPs that key off `claim in payload` behave consistently.
288
+
289
+ The `groups` claim is capped at **256 entries** to keep id_tokens under the typical 8 KB
290
+ header / cookie limit. The state name is appended **after** truncation so consumers that rely on
291
+ the state being present don't lose it silently. Override the cap by subclassing
292
+ `AllianceAuthOAuth2Validator` and overriding the `MAX_GROUPS_IN_CLAIM` class attribute.
293
+
294
+ ### Audit signal
295
+
296
+ Every successful token-issuance fires the `oidc_token_issued` Django signal
297
+ (`allianceauth_oidc.signals`). The default receiver writes a redacted audit log entry; connect
298
+ your own receiver to forward to a SIEM, write to a separate audit table, or push into an alerting
299
+ pipeline:
300
+
301
+ ```python
302
+ from django.dispatch import receiver
303
+ from allianceauth_oidc.signals import oidc_token_issued
304
+
305
+ @receiver(oidc_token_issued)
306
+ def forward_to_siem(sender, *, app, user, request, body, **kwargs):
307
+ # `body` is already redacted (build_oidc_debug_meta); never re-add raw secrets.
308
+ ...
309
+ ```
310
+
311
+ Don't extend `TokenView` to do this — the signal is the documented integration point and survives
312
+ DOT version bumps that change view internals.
313
+
314
+ ## Operations
315
+
316
+ ### Operator commands
317
+
318
+ Four `manage.py` commands cover the day-2 operational tasks without opening the admin UI. All
319
+ accept `--format=table|json|csv`; destructive commands honour `--dry-run`.
320
+
321
+ | Command | Purpose | Destructive? | Key flags |
322
+ |---|---|---|---|
323
+ | `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` |
324
+ | `oidc_rotate_secret` | Rotate `client_secret` on an existing app. Existing tokens stay valid until expiry. | yes | `--client-id`, `--dry-run` |
325
+ | `oidc_revoke_user_tokens` | Revoke every active access + refresh token for a user (off-boarding, compromise response). Idempotent. | yes | `--username`, `--dry-run` |
326
+ | `oidc_audit_tokens` | Read-only listing of active tokens. | no | `--username`, `--client-id`, `--include-expired` |
327
+
328
+ ```sh
329
+ python manage.py oidc_create_app \
330
+ --name="Grafana" --user-id=1 \
331
+ --redirect-uri="https://grafana.example/login/generic_oauth" \
332
+ --state=Member --group=Operators --format=json
333
+
334
+ python manage.py oidc_rotate_secret --client-id=abc123 --dry-run
335
+ python manage.py oidc_revoke_user_tokens --username=alice
336
+ python manage.py oidc_audit_tokens --client-id=abc123 --format=csv
337
+ ```
338
+
339
+ `create_app` writes a Django admin `LogEntry` on success so the action is visible in `/admin/`'s
340
+ history without code changes; the destructive commands log at `INFO` / `WARNING`.
341
+
342
+ ### Debug logging
343
+
344
+ Per-application `Debug Mode` (toggled in the admin) escalates token-flow logs from `DEBUG` to
345
+ `INFO`. Raw token values and secrets are **never** logged; the `_LOG_MASKED_SECRETS` knob (see
346
+ [Custom settings](#custom-settings-allianceauth_oidc_)) controls whether they appear as
347
+ `<redacted>` or masked fragments.
348
+
349
+ When debugging an app, look for lines like:
350
+
351
+ ```text
352
+ [01/Jan/2099 00:00:00] INFO [extensions.allianceauth_oidc.views:78] OIDC DEBUG token issued
353
+ app_id=1 client_id=abc123 user_id=42
354
+ meta={'grant_type': 'authorization_code', ..., 'access_token': '<redacted>', 'id_token': '<redacted>'}
355
+ ```
356
+
357
+ Paste the (separately captured) `id_token` into <https://jwt.io/> to inspect claims. The two
358
+ non-obvious fields:
359
+
360
+ - `iss` — issuer; must match the value the RP has configured exactly.
361
+ - `sub` — the user PK; useful for "why did this user end up here?" triage.
362
+
363
+ If you need the public key to verify the signature on jwt.io and have only the private key on
364
+ disk:
365
+
366
+ ```sh
367
+ ssh-keygen -y -e -m pem -f /path/to/key
368
+ ```
369
+
370
+ ### Operational hardening (operator responsibility)
371
+
372
+ The provider implements the OAuth2 / OIDC protocol semantics; runtime hardening below is
373
+ intentionally left to the deployment so it integrates with whatever edge / infra you already
374
+ operate.
375
+
376
+ - **Rate-limit `/o/token/` and `/o/authorize/`.** Neither endpoint is rate-limited by this app;
377
+ brute-force defence belongs at the edge (nginx `limit_req`, Cloudflare, a WAF) or via
378
+ `django-ratelimit` in your Auth deployment. Without it, a network-level attacker can probe
379
+ `client_secret` / `code` / `refresh_token` values at line speed.
380
+ - **Authenticate the Celery broker.** `clear_expired_tokens` is published to whichever broker
381
+ your AA install uses; if that broker is reachable by untrusted parties, a malicious task
382
+ submission can repeatedly invoke cleanup. The task is idempotent, but broker auth + network
383
+ ACLs are the defensive layer.
384
+ - **Security headers.** This app does not set CSP / HSTS / `X-Frame-Options` /
385
+ `X-Content-Type-Options`; rely on Alliance Auth's middleware stack and Django's `SECURE_*`
386
+ settings to add them globally.
387
+
388
+ ## Integrations
389
+
390
+ ### Register an application
391
+
392
+ In `/admin/allianceauth_oidc/`, create an `Alliance Auth application`:
393
+
394
+ | Field | Value | Notes |
395
+ |---|---|---|
396
+ | `User` | any (e.g. `1`) | Owner — passed through to DOT but not used in this app's policy. |
397
+ | `Client type` | `confidential` | Public clients are out of scope; we don't ship a public-client recipe. |
398
+ | `Authorization grant type` | `Authorization code` | The only flow this app's policy is hardened against. |
399
+ | `Client secret` | auto-generated | Save it before clicking save when `HASH_CLIENT_SECRET` is on (default). |
400
+ | `Algorithm` | `RSA with SHA-2 256` | Matches `OIDC_RSA_PRIVATE_KEY`. |
401
+ | `States` / `Groups` | whitelist | Empty ⇒ open; non-empty ⇒ user must be in a listed state OR group. |
402
+
403
+ Every user who logs into any registered application also needs the global
404
+ `allianceauth_oidc.access_oidc` permission. Without it the dispatch-layer gate (Layer 1) returns
405
+ `PermissionDenied` regardless of state / group whitelist.
406
+
407
+ ### Grafana
408
+
409
+ Tested without group-to-team mapping (group → team mapping requires Grafana Cloud / Enterprise
410
+ and is out of scope here).
411
+
412
+ ```ini
413
+ [server]
414
+ root_url = <URL of your grafana server>
415
+
416
+ [auth.generic_oauth]
417
+ enabled = true
418
+ name = <Your Auth Name>
419
+ allow_sign_up = true
420
+ client_id = <client_id>
421
+ client_secret = <unhashed client_secret>
422
+ scopes = openid,email,profile
423
+ empty_scopes = false
424
+ email_attribute_path = email
425
+ name_attribute_path = name
426
+ auth_url = https://<your.auth.url>/o/authorize/
427
+ token_url = https://<your.auth.url>/o/token/
428
+ api_url = https://<your.auth.url>/o/userinfo/
429
+ ```
430
+
431
+ ### WikiJS
432
+
433
+ Pre-create the groups you want WikiJS users to land in on the AA side; WikiJS will map them at
434
+ login. (Create an `Administrators` group to grant the wiki admin pages.)
435
+
436
+ | WikiJS field | Value |
437
+ |---|---|
438
+ | Skip User Profile | off |
439
+ | Email claim | `email` |
440
+ | Display Name Claim | `name` |
441
+ | Map Groups | on |
442
+ | Groups Claim | `groups` |
443
+ | Allow Self Registration | on |
444
+
445
+ ## Development
446
+
447
+ ### Nox sessions
448
+
449
+ | Session | Purpose | In default `nox` run? |
450
+ |---|---|---|
451
+ | `lint` | pre-commit (ruff, mypy, basedpyright, …) | yes |
452
+ | `tests` | Django test suite (parallel) | yes |
453
+ | `coverage` | tests + term / HTML / XML coverage reports | no |
454
+ | `typecheck` | mypy + basedpyright (subset of `lint`, run separately for fast feedback) | no |
455
+ | `audit` | pip-audit | no |
456
+ | `markdown_lint` | rumdl + lychee + vale (each tool optional) | no |
457
+ | `makemessages` / `compilemessages` | i18n catalogue refresh + compile | no |
458
+ | `makemigrations` | generate Django migrations under test settings | no |
459
+ | `integration` | wire-level mock-RP via `LiveServerTestCase` | no |
460
+ | `conformance` | OIDC Conformance Suite via docker-compose | no |
461
+
462
+ ### Integration tests (`nox -s integration`)
463
+
464
+ `tests/test_integration_mock_rp.py` boots a `LiveServerTestCase` and walks the OIDC code flow
465
+ with `requests` + `jwcrypto`, validating the id_token signature against a JWKS retrieved over the
466
+ wire. This catches regressions the standard `nox -s tests` set cannot — Django's test client
467
+ short-circuits the WSGI layer, so absolute-URL bugs in `iss` / `jwks_uri` and Bearer-header /
468
+ cookie issues only surface here.
469
+
470
+ The session forces `--parallel=1`: `LiveServerTestCase` shares its DB connection with the WSGI
471
+ thread, which does not survive the test runner's `fork()`.
472
+
473
+ ### Conformance Suite (`nox -s conformance`)
474
+
475
+ Runs the [OpenID Foundation Conformance Suite](https://gitlab.com/openid/conformance-suite)
476
+ against the provider via Docker Compose: MongoDB + the suite + a provider container. The default
477
+ plan is driven through the suite's REST API by `tests/conformance/run_plan.py`.
478
+
479
+ This is the level above our own integration tests — it catches spec edge cases that our
480
+ regression tests wouldn't think to check. Run before tagging a release. See
481
+ [tests/conformance/README.md](tests/conformance/README.md) for prerequisites, the manual /
482
+ iterative workflow, configuration overrides, and the list of known conformance findings to
483
+ triage.
484
+