Add Dynamic Client Registration behind initial access tokens#536
Open
jdelic wants to merge 3 commits into
Open
Add Dynamic Client Registration behind initial access tokens#536jdelic wants to merge 3 commits into
jdelic wants to merge 3 commits into
Conversation
Implements RFC 7591/7592 Dynamic Client Registration per .agent-docs/dcr-cimd-authz-iss/implementation-plan.rst section 3: registration at /o2/register/ is gated by an operator-minted "authserver:dcr-initial" bearer token (mailauth/dcr.py, InitialAccessTokenDCRPermission), managed via the new `dcrtoken` management command (create/list/revoke). Registered applications are bound to the Domain that signs for the request Host, so every DCR client can issue OIDC ID tokens; hosts without a signing domain are rejected with 400. RFC 7592 self-management (GET/PUT/DELETE) is mounted at the upstream-expected 'dcr-register-management' URL name. Deviates from the plan's reference code in mailauth/dcr.py: the plan assigns `domain` to the application only after upstream's post() saves it, assuming application.full_clean() tolerates domain=None. In reality MNApplication.domain is null=True but not blank=True, so full_clean() always rejected a domain-less application before upstream could save anything. Since touching mailauth/models.py was out of scope, MNDynamicClientRegistrationView.post() inlines upstream's post() body (reusing its private helpers) and sets `domain` on the Application before full_clean() runs, instead of calling super().post(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AgWqnQ8ExYjdZofXbvJZEz
- check the initial access token before resolving the request host in MNDynamicClientRegistrationView, so an unauthenticated caller can no longer probe which vhosts have a signing domain (401 for all of them) - require TLS on both registration endpoints (RFC 7591 section 5) and rate limit them by IP, like the other credential-carrying APIs in mailauth/views.py. The RFC 7592 endpoint gets its own subclass for that - add `iss` to authorization *error* redirects, which django-oauth-toolkit builds in the view and which therefore never pass through MNOAuthLibCore - stop advertising authorization_response_iss_parameter_supported in the RFC 8414 document served at the server root: it publishes issuer "https://<host>", which is not the "https://<host>/o2" we send as `iss` - advertise the DCR registration endpoint in the OpenID Connect discovery document as well, upstream only publishes it in the RFC 8414 metadata - rewrite the MNDynamicClientRegistrationView docstring: keep why the view vendors upstream's post() (MNApplication.domain isn't blank=True), drop the retelling of how the implementation plan got there - dcrtoken: reject negative --expires-days, drop microseconds from the timestamps in `list` so the columns line up, document that a token is reusable until it expires - tests: token-endpoint auth for a DCR-registered confidential client, a user access token rejected as an initial access token, plaintext and rate limit rejections, discovery documents, negative --expires-days - smoke test dcrtoken create/list/revoke in the CI workflow Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V17VQLeK8vFDVLZ8uyiEGC
The document at the bare /.well-known/oauth-authorization-server named "https://<host>" as its issuer while every endpoint in it lives under /o2 and both `iss` and the ID token `iss` claim say "https://<host>/o2". The path-component route made it worse: <path:issuer_path> answered for any path, so /.well-known/oauth-authorization-server/anything claimed to be metadata for the issuer "https://<host>/anything". - pin the path-insertion route to the one issuer path that exists, so every other path 404s like an unknown issuer should - redirect the bare well-known URL to that document instead of inventing an issuer for it. Clients that don't implement RFC 8414 issuer paths still discover the server, and now learn its real issuer - also serve the document in the issuer-suffix form under /o2, which is how OpenID Connect Discovery style clients build the URL. Registering that route inside the oauth2_provider namespace additionally makes django-oauth-toolkit's own RFC 9207 issuer helper resolve correctly - with every document naming the same issuer, the discovery mixin can go back to advertising RFC 9207 support unconditionally - the o2 mount path is a constant now, the metadata routes have to agree with it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V17VQLeK8vFDVLZ8uyiEGC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements RFC 7591/7592 Dynamic Client Registration per .agent-docs/dcr-cimd-authz-iss/implementation-plan.rst section 3: registration at /o2/register/ is gated by an operator-minted "authserver:dcr-initial" bearer token (mailauth/dcr.py, InitialAccessTokenDCRPermission), managed via the new
dcrtokenmanagement command (create/list/revoke). Registered applications are bound to the Domain that signs for the request Host, so every DCR client can issue OIDC ID tokens; hosts without a signing domain are rejected with 400. RFC 7592 self-management (GET/PUT/DELETE) is mounted at the upstream-expected 'dcr-register-management' URL name.Deviates from the plan's reference code in mailauth/dcr.py: the plan assigns
domainto the application only after upstream's post() saves it, assuming application.full_clean() tolerates domain=None. In reality MNApplication.domain is null=True but not blank=True, so full_clean() always rejected a domain-less application before upstream could save anything. Since touching mailauth/models.py was out of scope, MNDynamicClientRegistrationView.post() inlines upstream's post() body (reusing its private helpers) and setsdomainon the Application before full_clean() runs, instead of calling super().post().Claude-Session: https://claude.ai/code/session_01AgWqnQ8ExYjdZofXbvJZEz