-
Notifications
You must be signed in to change notification settings - Fork 167
Dpop thumbprint validation #942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e2c65d4
7145784
dac4dce
3283ad2
5875c0c
97fd1b1
298b10f
464e52b
efba1e3
7e6b32e
55e8d3a
6f9839e
0b0826b
fed47bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -194,6 +194,63 @@ public abstract class BaseCredentialsManager internal constructor( | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Validates DPoP key/token alignment before attempting a refresh. | ||
| * | ||
| * Uses two signals to detect DPoP-bound credentials: | ||
| * - tokenType == "DPoP" | ||
| * - KEY_DPOP_THUMBPRINT exists | ||
| * | ||
| * @param tokenType the token_type value from storage (or decrypted credentials for migration) | ||
| * @return null if validation passes, or a CredentialsManagerException if it fails | ||
| */ | ||
| protected fun validateDPoPState(tokenType: String?): CredentialsManagerException? { | ||
| val storedThumbprint = storage.retrieveString(KEY_DPOP_THUMBPRINT) | ||
| val isDPoPBound = (tokenType?.equals("DPoP", ignoreCase = true) == true) | ||
| || (storedThumbprint != null) | ||
| if (!isDPoPBound) return null | ||
|
|
||
| // Check 1: Does the DPoP key still exist in KeyStore? | ||
| val hasKey = try { | ||
| DPoPUtil.hasKeyPair() | ||
| } catch (e: DPoPException) { | ||
| Log.e(this::class.java.simpleName, "Failed to check DPoP key existence", e) | ||
| false | ||
| } | ||
| if (!hasKey) { | ||
| Log.w(this::class.java.simpleName, "DPoP key missing from KeyStore. Clearing stale credentials.") | ||
| clearCredentials() | ||
| return CredentialsManagerException(CredentialsManagerException.Code.DPOP_KEY_MISSING) | ||
| } | ||
|
|
||
| // Check 2: Is the AuthenticationAPIClient configured with DPoP? | ||
| if (!authenticationClient.isDPoPEnabled) { | ||
| return CredentialsManagerException(CredentialsManagerException.Code.DPOP_NOT_CONFIGURED) | ||
| } | ||
|
|
||
| // Check 3: Does the current key match the one used when credentials were saved? | ||
| val currentThumbprint = try { | ||
| DPoPUtil.getPublicKeyJWK() | ||
| } catch (e: DPoPException) { | ||
| Log.e(this::class.java.simpleName, "Failed to read DPoP key thumbprint", e) | ||
| null | ||
| } | ||
|
Comment on lines
+232
to
+237
|
||
|
|
||
| if (storedThumbprint != null) { | ||
| if (currentThumbprint != storedThumbprint) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The key passed hasKeyPair() (Check 1), so it's not missing — it's a different key. Returning DPOP_KEY_MISSING here is misleading for developers debugging this path. Consider either a new code like DPOP_KEY_MISMATCH, or at minimum a distinct log message so it's distinguishable from the "key actually gone" case in Check 1.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Log is already added. I will add the new error type. Didn't add that initially as I thought it will be easier of dev side to just catch the DPOP_KEY_MISSING error and reinitiate a login flow. But a new error seems more DX friendly |
||
| Log.w(this::class.java.simpleName, "DPoP key thumbprint mismatch. The key pair has changed since credentials were saved. Clearing stale credentials.") | ||
| clearCredentials() | ||
| return CredentialsManagerException(CredentialsManagerException.Code.DPOP_KEY_MISMATCH) | ||
| } | ||
|
Comment on lines
+239
to
+244
|
||
| } else if (currentThumbprint != null) { | ||
| // Migration: existing DPoP user upgraded — no thumbprint stored yet. | ||
| // Backfill so future checks can detect key rotation. | ||
| storage.store(KEY_DPOP_THUMBPRINT, currentThumbprint) | ||
| } | ||
|
|
||
| return null | ||
| } | ||
|
|
||
| /** | ||
| * Checks if the stored scope is the same as the requested one. | ||
| * | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change introduces new DPoP validation behavior that can fail early (and in some cases clear stored credentials) during
renewAuth(...)/ssoExchange(...). There are existing unit tests for bothCredentialsManagerandSecureCredentialsManager, but none appear to cover DPoP scenarios; please add tests that exercise the new paths (key missing, thumbprint mismatch, and client not configured) and assert both the returnedCredentialsManagerExceptioncode and whether credentials are cleared/backfilled as intended.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests will be added later