providers/oauth2: require client_secret on device_code exchange for confidential clients#21700
Open
SAY-5 wants to merge 1 commit intogoauthentik:mainfrom
Open
providers/oauth2: require client_secret on device_code exchange for confidential clients#21700SAY-5 wants to merge 1 commit intogoauthentik:mainfrom
SAY-5 wants to merge 1 commit intogoauthentik:mainfrom
Conversation
…onfidential clients
TokenParams.__post_init__ only ran the client_secret check for the
authorization_code and refresh_token grant types:
if self.grant_type in [GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_REFRESH_TOKEN]:
if self.provider.client_type == ClientTypes.CONFIDENTIAL and not compare_digest(
self.provider.client_secret, self.client_secret,
):
raise TokenError("invalid_client")
The device_code path (__post_init_device_code) then looked up the
DeviceToken solely by device_code and issued an access token if one
matched. A caller that knows the client_id and has stolen a
device_code (e.g. via the standard phishing flow: attacker starts
device authorization, sends user_code to a victim, victim completes
authorization, attacker redeems the device_code) did not have to
prove ownership of the confidential client.
RFC 6749 Section 2.3.1 requires confidential clients to authenticate
to the token endpoint, and RFC 8628 Section 3.4 inherits that: the
device_code is bearer-shaped but not a substitute for client
credentials. Keycloak and Okta both enforce client_secret on the
device token exchange for confidential clients; we didn't.
Add GRANT_TYPE_DEVICE_CODE to the list so the existing compare_digest
check runs for it too. Public clients are unaffected (the guard is
gated on ClientTypes.CONFIDENTIAL). client_credentials/password keep
their own client-auth path in __post_init_client_credentials, which
also enforces the secret (and supports client assertion).
Fixes goauthentik#20828
Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>
✅ Deploy Preview for authentik-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
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.
What
TokenParams.__post_init__inauthentik/providers/oauth2/views/token.pyonly ran theclient_secretcheck for theauthorization_codeandrefresh_tokengrant types:The device-code path (
__post_init_device_code) then looked up theDeviceTokenpurely bydevice_codeand issued an access token whenever one matched — regardless of whether the caller had proved ownership of the confidential client:Per #20828 this is exploitable via the standard device-flow phishing shape (attacker starts device authorization, sends
user_codeto a victim, victim completes authorization, attacker redeemsdevice_code): an attacker that only knowsclient_idcan redeem a stolendevice_codewithout ever proving ownership of the confidential client.RFC 6749 §2.3.1 requires confidential clients to authenticate to the token endpoint; RFC 8628 §3.4 inherits that. The
device_codeis bearer-shaped but is not a substitute for client credentials. Keycloak and Okta both enforceclient_secreton the device token exchange for confidential clients.Fix
Add
GRANT_TYPE_DEVICE_CODEto the list that already runs thecompare_digestcheck:ClientTypes.CONFIDENTIAL).client_credentials/passwordkeep their own client-auth path in__post_init_client_credentials(which also enforces the secret, plus supports client assertion).authorization_code/refresh_tokenpaths are unchanged.Test plan
main: configure an OIDC provider withclient_type: confidential, run a device flow,POST /application/o/token/with onlyclient_id=...&device_code=...&grant_type=urn:ietf:params:oauth:grant-type:device_code— gets an access token.{"error":"invalid_client"}; the request with the correctclient_secret(or a validAuthorization: Basic ...) still succeeds.client_secretstill succeeds (unchanged behaviour).Fixes #20828