-
Notifications
You must be signed in to change notification settings - Fork 231
feat: add support for Demonstration of Proof-of-Possession(DPoP) #1345
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
base: master
Are you sure you want to change the base?
Conversation
…oP key management
…n across platforms
…into feat/dpop-support
android/build.gradle
Outdated
| implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" | ||
| implementation "androidx.browser:browser:1.2.0" | ||
| implementation 'com.auth0.android:auth0:3.8.0' | ||
| implementation 'com.auth0.android:auth0:3.9.1' |
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.
You can use 3.10.0 just so that it is the latest Android SDK
| private const val DPOP_KEY_RETRIEVAL_FAILED_CODE = "DPOP_KEY_RETRIEVAL_FAILED" | ||
| private const val DPOP_KEYSTORE_ERROR_CODE = "DPOP_KEYSTORE_ERROR" | ||
| private const val DPOP_CRYPTO_ERROR_CODE = "DPOP_CRYPTO_ERROR" | ||
| private const val DPOP_GENERATION_FAILED_CODE = "DPOP_GENERATION_FAILED" |
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.
DPOP_GENERATION_FAILED_CODE do you need a specific code for this. DPoP generation will always fail due to one of the other codes added
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.
its an generic error for both android and iOS in case of anything unexpected happen in DPoP flow.
Let me know if you think I should remove it completely
| } | ||
|
|
||
| @ReactMethod | ||
| override fun getDPoPHeaders(url: String, method: String, accessToken: String, tokenType: String, promise: Promise) { |
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 should also take an optional nonce parameter for the flows which would require a nonce value
|
|
||
| @ReactMethod | ||
| @DoNotStrip | ||
| abstract fun getDPoPHeaders(url: String, method: String, accessToken: String, tokenType: String, promise: Promise) |
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.
Add optional nonce parameter
ios/NativeBridge.swift
Outdated
| resolve(removed) | ||
| } | ||
|
|
||
| @objc public func getDPoPHeaders(url: String, method: String, accessToken: String, tokenType: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { |
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 would also require an optional nonce parameter
ios/NativeBridge.swift
Outdated
| } | ||
|
|
||
| // Validate URL format | ||
| guard let urlObj = URL(string: url) else { |
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.
You can probably combine this check into a single one with the initial check of !url.isEmpty
| /** The access token to bind to the request. */ | ||
| accessToken: string; | ||
| /** The type of the token (should be 'DPoP' when DPoP is enabled). */ | ||
| tokenType: string; |
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.
Add an optional nonce parameter to this contract
…te auth0 dependency to 3.10.0
…ate URL format in NativeBridge
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.
Pull Request Overview
This PR implements DPoP (Demonstrating Proof-of-Possession) RFC 9449 support across iOS, Android, and Web platforms, adding cryptographic binding for OAuth 2.0 tokens to enhance security.
Key Changes:
- Upgraded native SDK dependencies (Auth0.swift 2.13.0→2.14.0, Auth0.Android 3.8.0→3.10.0, auth0-spa-js 2.3.0→2.7.0)
- Added
getDPoPHeaders()API method anduseAuth0().getDPoPHeaders()hook for custom API requests - Introduced
useDPoPconfiguration option (default: true) and newDPoPErrorclass with normalized error codes - Integrated DPoP key cleanup into logout flow via
clearCredentials()
Reviewed Changes
Copilot reviewed 26 out of 29 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types/common.ts | Added useDPoP option and DPoPHeadersParams interface |
| src/specs/NativeA0Auth0.ts | Added useDPoP parameter and DPoP method signatures to Turbo Module spec |
| src/platforms/web/adapters/WebAuth0Client.ts | Implemented web DPoP header generation using auth0-spa-js |
| src/platforms/native/adapters/NativeAuth0Client.ts | Implemented native DPoP header generation with error wrapping |
| src/platforms/native/bridge/* | Added bridge methods for getDPoPHeaders and clearDPoPKey |
| src/core/models/DPoPError.ts | New error class with cross-platform error code normalization |
| src/hooks/* | Added getDPoPHeaders to Auth0Context and Auth0Provider |
| ios/NativeBridge.swift | iOS DPoP implementation with Secure Enclave/Keychain integration |
| android/.../A0Auth0Module.kt | Android DPoP implementation with KeyStore integration |
| package.json | Updated auth0-spa-js dependency to 2.7.0 |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| DPoPException.MALFORMED_URL -> DPOP_MISSING_PARAMETER_CODE | ||
| DPoPException.UNSUPPORTED_ERROR -> DPOP_ERROR_CODE | ||
| DPoPException.UNKNOWN_ERROR -> DPOP_GENERATION_FAILED_CODE | ||
| else -> DPOP_GENERATION_FAILED_CODE |
Copilot
AI
Oct 23, 2025
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 when expression matches DPoPException instances as if they were enum values, but the code suggests DPoPException is likely a sealed class or enum. Without exhaustive matching (removing the 'else' branch), new DPoPException types added in future SDK versions could silently fall through to the default case. Consider making this exhaustive or adding a comment explaining why the else branch is needed.
| else -> DPOP_GENERATION_FAILED_CODE |
| case .secKeyOperationFailed: code = NativeBridge.dpopProofFailedCode | ||
| case .other: code = NativeBridge.dpopErrorCode | ||
| case .unknown: code = NativeBridge.dpopErrorCode | ||
| default: |
Copilot
AI
Oct 23, 2025
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.
The switch statement on DPoPError uses a 'default' case, which may mask new error types added in future versions of Auth0.swift. Consider making this switch exhaustive by removing the default and handling all DPoPError cases explicitly, or add a @unknown default if Swift version supports it.
| default: | |
| @unknown default: |
|
Claude finished @subhankarmaiti's task —— View job Overall Assessment✅ This PR is well-implemented and ready to be merged after addressing the minor issues identified in the inline comments. The DPoP implementation follows RFC 9449 correctly, integrates cleanly with the native SDKs, and maintains backward compatibility. The code quality is high with proper error handling, parameter validation, and comprehensive test coverage. Key Strengths:
Minor Issues to Address:
Security Review: ✅ No security vulnerabilities found
Performance Review: ✅ No performance concerns
|
| s.requires_arc = true | ||
|
|
||
| s.dependency 'Auth0', '2.13' | ||
| s.dependency 'Auth0', '2.14' |
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.
The version specification '2.14' is missing the patch version. For consistency and to ensure reproducible builds, specify the full semantic version '~> 2.14.0' or '>= 2.14.0, < 3.0' to allow patch updates while preventing breaking changes.
| s.dependency 'Auth0', '2.14' | |
| s.dependency 'Auth0', '~> 2.14.0' |
| } | ||
|
|
||
| // Validate URL format | ||
| guard !url.isEmpty, let urlObj = URL(string: url) else { |
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.
Redundant URL validation: The URL is already validated for emptiness at line 249. This second check on line 287 is unnecessary and can be combined with the URL parsing validation.
| guard !url.isEmpty, let urlObj = URL(string: url) else { | |
| // Validate URL format | |
| guard let urlObj = URL(string: url) else { | |
| reject( | |
| NativeBridge.dpopMissingParameterCode, | |
| "Invalid URL format: \(url)", | |
| nil | |
| ) | |
| return | |
| } |
| private fun handleDPoPError(error: DPoPException, promise: Promise) { | ||
| val errorCode = when (error) { | ||
| DPoPException.KEY_GENERATION_ERROR -> DPOP_KEY_GENERATION_FAILED_CODE | ||
| DPoPException.KEY_STORE_ERROR -> DPOP_KEYSTORE_ERROR_CODE | ||
| DPoPException.KEY_PAIR_NOT_FOUND -> DPOP_KEY_RETRIEVAL_FAILED_CODE | ||
| DPoPException.SIGNING_ERROR -> DPOP_PROOF_FAILED_CODE | ||
| DPoPException.MALFORMED_URL -> DPOP_MISSING_PARAMETER_CODE | ||
| DPoPException.UNSUPPORTED_ERROR -> DPOP_ERROR_CODE | ||
| DPoPException.UNKNOWN_ERROR -> DPOP_GENERATION_FAILED_CODE | ||
| else -> DPOP_GENERATION_FAILED_CODE | ||
| } | ||
|
|
||
| promise.reject(errorCode, error.message ?: "DPoP operation failed", error) | ||
| } |
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.
The when expression treats DPoPException as if it has enum-like values, but without exhaustive matching (the else branch), new exception types added in future SDK versions could silently fall through to the default case. Consider whether the else branch is necessary based on the actual DPoPException implementation in Auth0.Android SDK. If DPoPException is a sealed class or enum with a fixed set of values, removing the else would provide compile-time safety.
| private const val DPOP_PROOF_FAILED_CODE = "DPOP_PROOF_FAILED" | ||
| private const val DPOP_NONCE_MISMATCH_CODE = "DPOP_NONCE_MISMATCH" | ||
| private const val DPOP_INVALID_TOKEN_TYPE_CODE = "DPOP_INVALID_TOKEN_TYPE" | ||
| private const val DPOP_MISSING_PARAMETER_CODE = "DPOP_MISSING_PARAMETER" |
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.
The DPOP_GENERATION_FAILED_CODE appears to be a catch-all error code. Based on the previous review comment from @pmathew92, this generic code may not be necessary since DPoP generation will always fail due to one of the more specific error codes (KEY_GENERATION_FAILED, PROOF_FAILED, etc.). Consider whether this generic error code adds value or should be removed to encourage more specific error handling.
| extension DPoPError { | ||
| func reactNativeErrorCode() -> String { | ||
| var code: String | ||
| switch self { | ||
| case .secureEnclaveOperationFailed: code = NativeBridge.dpopKeyGenerationFailedCode | ||
| case .keychainOperationFailed: code = NativeBridge.dpopKeyStorageFailedCode | ||
| case .cryptoKitOperationFailed: code = NativeBridge.dpopProofFailedCode | ||
| case .secKeyOperationFailed: code = NativeBridge.dpopProofFailedCode | ||
| case .other: code = NativeBridge.dpopErrorCode | ||
| case .unknown: code = NativeBridge.dpopErrorCode | ||
| default: | ||
| code = NativeBridge.dpopErrorCode | ||
| } | ||
| return code | ||
| } | ||
| } |
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.
The switch statement on DPoPError uses a default case, which may mask new error types added in future versions of Auth0.swift. For better compile-time safety and to catch new error cases early, consider making this switch exhaustive or use @unknown default if the Swift version supports it:
| extension DPoPError { | |
| func reactNativeErrorCode() -> String { | |
| var code: String | |
| switch self { | |
| case .secureEnclaveOperationFailed: code = NativeBridge.dpopKeyGenerationFailedCode | |
| case .keychainOperationFailed: code = NativeBridge.dpopKeyStorageFailedCode | |
| case .cryptoKitOperationFailed: code = NativeBridge.dpopProofFailedCode | |
| case .secKeyOperationFailed: code = NativeBridge.dpopProofFailedCode | |
| case .other: code = NativeBridge.dpopErrorCode | |
| case .unknown: code = NativeBridge.dpopErrorCode | |
| default: | |
| code = NativeBridge.dpopErrorCode | |
| } | |
| return code | |
| } | |
| } | |
| extension DPoPError { | |
| func reactNativeErrorCode() -> String { | |
| var code: String | |
| switch self { | |
| case .secureEnclaveOperationFailed: code = NativeBridge.dpopKeyGenerationFailedCode | |
| case .keychainOperationFailed: code = NativeBridge.dpopKeyStorageFailedCode | |
| case .cryptoKitOperationFailed: code = NativeBridge.dpopProofFailedCode | |
| case .secKeyOperationFailed: code = NativeBridge.dpopProofFailedCode | |
| case .other: code = NativeBridge.dpopErrorCode | |
| case .unknown: code = NativeBridge.dpopErrorCode | |
| @unknown default: | |
| code = NativeBridge.dpopErrorCode | |
| } | |
| return code | |
| } | |
| } |
Changes
This PR implements DPoP (RFC 9449) support across all platforms (iOS, Android, and Web) for react-native-auth0, enabling sender-constrained OAuth 2.0 tokens for enhanced security.
SDK Version Updates
New Public API Methods
1.
Auth0.getDPoPHeaders(params: DPoPHeadersParams): Promise<Record<string, string>>url,method,accessToken,tokenTypeAuthorizationandDPoPheaders2.
useAuth0().getDPoPHeaders(params: DPoPHeadersParams)New Configuration Options
Auth0Options.useDPoP?: boolean(default:true)New Error Types
DPoPErrorclass extendingAuthErrorwith normalized error codes:DPOP_GENERATION_FAILED- General DPoP generation failureDPOP_PROOF_FAILED- DPoP proof generation failureDPOP_KEY_GENERATION_FAILED- Key pair generation failureDPOP_KEY_STORAGE_FAILED- Key storage failureDPOP_KEY_RETRIEVAL_FAILED- Key retrieval failureDPOP_NONCE_MISMATCH- Nonce validation failureDPOP_INVALID_TOKEN_TYPE- Invalid token typeDPOP_MISSING_PARAMETER- Required parameter missingDPOP_CLEAR_KEY_FAILED- Key cleanup failureNative Bridge Methods Added
iOS (NativeBridge.swift):
getDPoPHeaders(url:method:accessToken:tokenType:resolve:reject:)clearDPoPKey(resolve:reject:)DPoPError.reactNativeErrorCode()- Maps DPoP errors to RN error codesAndroid (A0Auth0Module.kt):
getDPoPHeaders(url:method:accessToken:tokenType:promise)clearDPoPKey(promise)handleDPoPError(error:promise)- Private error handler with exact exception matchingWeb (WebAuth0Client.ts):
getDPoPHeaders(params)- Uses auth0-spa-js internal DPoP utilitiesModified Methods
INativeBridge.initialize()useDPoP?: booleanparametertrueICredentialsManager.clearCredentials()Architecture Overview
graph TB subgraph "React Native Layer" A0[Auth0 Client] --> |getDPoPHeaders| DA[DPoP API] A0 --> |webAuth| WA[WebAuth Provider] A0 --> |credentialsManager| CM[Credentials Manager] DA --> |returns| H[DPoP Headers] end subgraph "Native Bridges" WA --> |iOS| IWA[NativeBridge.swift] WA --> |Android| AWA[A0Auth0Module.kt] WA --> |Web| WWA[auth0-spa-js] DA --> |iOS| IDA[DPoP.addHeaders] DA --> |Android| ADA[DPoP.getHeaderData] DA --> |Web| WDA[getDPoPProof] CM --> |clearCredentials| CL[Clear DPoP Keys] end subgraph "Native SDKs" IWA --> |v2.14| AS[Auth0.swift] AWA --> |v3.9.1| AA[Auth0.Android] WWA --> |v2.4.1| ASJ[auth0-spa-js] AS --> |Keychain| KS[Key Storage] AA --> |KeyStore| AKS[Key Storage] ASJ --> |IndexedDB| WKS[Key Storage] end subgraph "DPoP Flow" KS --> |Private Key| SIG[Sign JWT Proof] AKS --> |Private Key| SIG WKS --> |Private Key| SIG SIG --> |DPoP Header| API[Protected API] H --> |Authorization + DPoP| API end style DA fill:#4CAF50 style IDA fill:#2196F3 style ADA fill:#FF9800 style WDA fill:#9C27B0 style API fill:#F44336Usage Example
React Hook Usage
References
Testing
Unit Tests
Platform Testing
iOS:
Android:
Web:
Integration Tests
Not Tested
DPoP with Refresh Token Exchange: Per Auth0.swift documentation, DPoP is not applied to existing refresh token exchanges. This is expected behavior and a limitation of the current DPoP implementation in the native SDKs.
Checklist
DPoPErrorclassBreaking Changes
None. This is a backward-compatible feature addition.
useDPoP: true), but existing applications will continue to workMigration Notes
For applications that want to adopt DPoP:
useDPoP: truecredentials.tokenType === 'DPoP'before callinggetDPoPHeaders()getDPoPHeaders()for custom API requests when using DPoP tokensSecurity Considerations
✅ Key Storage: All platforms use secure storage
✅ Key Cleanup: DPoP keys are automatically cleared on logout
✅ Error Security: Error messages do not leak sensitive cryptographic details
✅ Token Binding: DPoP cryptographically binds tokens to device-specific keys, preventing token theft attacks