Skip to content

Conversation

@subhankarmaiti
Copy link
Contributor

@subhankarmaiti subhankarmaiti commented Oct 16, 2025

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

Platform SDK Previous Version New Version
iOS Auth0.swift 2.13.0 2.14.0
Android Auth0.Android 3.8.0 3.9.1
Web auth0-spa-js 2.3.0 2.7.0

New Public API Methods

1. Auth0.getDPoPHeaders(params: DPoPHeadersParams): Promise<Record<string, string>>

  • Generates DPoP-bound headers (Authorization + DPoP) for custom API requests
  • Parameters: url, method, accessToken, tokenType
  • Returns: Object with Authorization and DPoP headers

2. useAuth0().getDPoPHeaders(params: DPoPHeadersParams)

  • React Hook version of the same functionality
  • Available in Auth0Context for use in React components

New Configuration Options

Auth0Options.useDPoP?: boolean (default: true)

  • Enables/disables DPoP for token requests
  • Applies to all platforms

New Error Types

DPoPError class extending AuthError with normalized error codes:

  • DPOP_GENERATION_FAILED - General DPoP generation failure
  • DPOP_PROOF_FAILED - DPoP proof generation failure
  • DPOP_KEY_GENERATION_FAILED - Key pair generation failure
  • DPOP_KEY_STORAGE_FAILED - Key storage failure
  • DPOP_KEY_RETRIEVAL_FAILED - Key retrieval failure
  • DPOP_NONCE_MISMATCH - Nonce validation failure
  • DPOP_INVALID_TOKEN_TYPE - Invalid token type
  • DPOP_MISSING_PARAMETER - Required parameter missing
  • DPOP_CLEAR_KEY_FAILED - Key cleanup failure

Native Bridge Methods Added

iOS (NativeBridge.swift):

  • getDPoPHeaders(url:method:accessToken:tokenType:resolve:reject:)
  • clearDPoPKey(resolve:reject:)
  • Extension: DPoPError.reactNativeErrorCode() - Maps DPoP errors to RN error codes

Android (A0Auth0Module.kt):

  • getDPoPHeaders(url:method:accessToken:tokenType:promise)
  • clearDPoPKey(promise)
  • handleDPoPError(error:promise) - Private error handler with exact exception matching

Web (WebAuth0Client.ts):

  • getDPoPHeaders(params) - Uses auth0-spa-js internal DPoP utilities

Modified Methods

INativeBridge.initialize()

  • Added useDPoP?: boolean parameter
  • Initializes DPoP during SDK setup
  • Default: true

ICredentialsManager.clearCredentials()

  • Now also clears DPoP keys on logout
  • Ensures complete cleanup of cryptographic material

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:#F44336
Loading

Usage Example

import Auth0, { DPoPError } from 'react-native-auth0';

const auth0 = new Auth0({
  domain: 'YOUR_DOMAIN',
  clientId: 'YOUR_CLIENT_ID',
  useDPoP: true // Enable DPoP (default: true)
});

// 1. Login
const credentials = await auth0.webAuth.authorize();
console.log(credentials.tokenType); // 'DPoP'

// 2. Call Custom API with DPoP
try {
  const headers = await auth0.getDPoPHeaders({
    url: 'https://api.example.com/data',
    method: 'GET',
    accessToken: credentials.accessToken,
    tokenType: credentials.tokenType
  });
  
  // headers = {
  //   Authorization: 'DPoP eyJhbGc...',
  //   DPoP: 'eyJhbGciOiJFUzI1NiIs...'
  // }
  
  const response = await fetch('https://api.example.com/data', { headers });
  const data = await response.json();
} catch (error) {
  if (error instanceof DPoPError) {
    switch (error.type) {
      case 'DPOP_KEY_GENERATION_FAILED':
        console.error('Failed to generate DPoP key');
        break;
      case 'DPOP_PROOF_FAILED':
        console.error('Failed to generate DPoP proof');
        break;
    }
  }
}

// 3. Logout (automatically clears DPoP keys)
await auth0.credentialsManager.clearCredentials();

React Hook Usage

import { useAuth0 } from 'react-native-auth0';

function MyComponent() {
  const { getDPoPHeaders, getCredentials } = useAuth0();
  
  const fetchData = async () => {
    const credentials = await getCredentials();
    
    if (credentials.tokenType === 'DPoP') {
      const headers = await getDPoPHeaders({
        url: 'https://api.example.com/data',
        method: 'GET',
        accessToken: credentials.accessToken,
        tokenType: credentials.tokenType
      });
      
      const response = await fetch('https://api.example.com/data', { headers });
      return response.json();
    }
  };
  
  // ...
}

References

Testing

Unit Tests

  • ✅ DPoP error normalization tests (TypeScript)
  • ✅ Error code mapping validation
  • ✅ Parameter validation tests

Platform Testing

iOS:

  • Tested on iOS 15+ with physical device
  • Tested DPoP key generation with Secure Enclave
  • Tested DPoP key generation with Keychain fallback
  • Verified DPoP proof generation and validation
  • Tested error handling for all DPoPError cases
  • Verified key cleanup on logout

Android:

  • Tested on Android API 23+ with physical device
  • Tested DPoP key generation with Android KeyStore
  • Verified DPoP proof generation and validation
  • Tested error handling for all DPoPException cases
  • Verified key cleanup on logout

Web:

  • Tested on Chrome, Firefox, Safari
  • Verified auth0-spa-js v2.4.1 DPoP integration
  • Tested fallback to Bearer tokens when DPoP unavailable

Integration Tests

  • End-to-end DPoP flow (login → API call → logout)
  • Token refresh with DPoP
  • DPoP nonce handling (use_dpop_nonce errors)
  • Backward compatibility (useDPoP: false)
  • Migration from non-DPoP to DPoP sessions

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

  • I have read the Auth0 general contribution guidelines
  • All existing and new tests complete without errors
  • All code follows platform-specific patterns:
    • iOS: Extension methods for error handling
    • Android: Private methods with Promise parameters
    • Web: async/await with error wrapping
  • Updated native SDK dependencies:
    • Auth0.swift: 2.13.0 → 2.14.0
    • Auth0.Android: 3.8.0 → 3.9.1
    • auth0-spa-js: 2.3.0 → 2.4.1
  • Added TypeScript type definitions
  • Added JSDoc documentation for all public APIs
  • Exported new DPoPError class
  • Updated Turbo Module specs for new architecture
  • All active GitHub checks have passed (pending CI)

Breaking Changes

None. This is a backward-compatible feature addition.

  • DPoP is enabled by default (useDPoP: true), but existing applications will continue to work
  • The SDK gracefully falls back to Bearer tokens when DPoP is not supported
  • Existing credentials are not affected (DPoP only applies to new user sessions)

Migration Notes

For applications that want to adopt DPoP:

  1. Update Dependencies: The SDK version bump is automatic via package.json
  2. Enable DPoP: DPoP is enabled by default, or explicitly set useDPoP: true
  3. Handle Token Type: Check credentials.tokenType === 'DPoP' before calling getDPoPHeaders()
  4. Update API Calls: Use getDPoPHeaders() for custom API requests when using DPoP tokens
  5. Force User Re-login (Optional): Existing sessions will continue to use Bearer tokens until users log in again
// Optional: Force re-login to enable DPoP for existing users
const credentials = await auth0.credentialsManager.getCredentials();
if (credentials.tokenType !== 'DPoP') {
  // User has an old Bearer token
  await auth0.credentialsManager.clearCredentials();
  await auth0.webAuth.authorize(); // New login will use DPoP
}

Security Considerations

Key Storage: All platforms use secure storage

  • iOS: Keychain (Secure Enclave when available)
  • Android: Android KeyStore
  • Web: IndexedDB with key rotation

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

@subhankarmaiti subhankarmaiti changed the title Feat/dpop support feat: add support for Demonstration of Proof-of-Possession(DPoP) Oct 21, 2025
@subhankarmaiti subhankarmaiti marked this pull request as ready for review October 21, 2025 05:40
@subhankarmaiti subhankarmaiti requested a review from a team as a code owner October 21, 2025 05:40
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'
Copy link
Contributor

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"
Copy link
Contributor

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

Copy link
Contributor Author

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) {
Copy link
Contributor

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add optional nonce parameter

resolve(removed)
}

@objc public func getDPoPHeaders(url: String, method: String, accessToken: String, tokenType: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
Copy link
Contributor

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

}

// Validate URL format
guard let urlObj = URL(string: url) else {
Copy link
Contributor

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;
Copy link
Contributor

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

@pmathew92 pmathew92 requested a review from Copilot October 23, 2025 13:47
Copy link

Copilot AI left a 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 and useAuth0().getDPoPHeaders() hook for custom API requests
  • Introduced useDPoP configuration option (default: true) and new DPoPError class 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
Copy link

Copilot AI Oct 23, 2025

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.

Suggested change
else -> DPOP_GENERATION_FAILED_CODE

Copilot uses AI. Check for mistakes.
case .secKeyOperationFailed: code = NativeBridge.dpopProofFailedCode
case .other: code = NativeBridge.dpopErrorCode
case .unknown: code = NativeBridge.dpopErrorCode
default:
Copy link

Copilot AI Oct 23, 2025

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.

Suggested change
default:
@unknown default:

Copilot uses AI. Check for mistakes.
@subhankarmaiti
Copy link
Contributor Author

@claude

@github-actions
Copy link

github-actions bot commented Oct 24, 2025

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:

  • Strong security implementation with proper key storage across all platforms (iOS Keychain/Secure Enclave, Android KeyStore, Web IndexedDB)
  • Excellent error normalization layer providing consistent error codes across platforms
  • Comprehensive parameter validation preventing common security mistakes
  • Backward compatible with graceful fallback to Bearer tokens
  • Well-documented with clear JSDoc comments and usage examples
  • Good test coverage including edge cases and error scenarios

Minor Issues to Address:

  • A0Auth0.podspec:19 - Use full semantic versioning (e.g., ~> 2.14.0) instead of 2.14 for reproducible builds
  • ios/NativeBridge.swift:287 - Remove redundant URL empty check (already validated at line 249)
  • ios/NativeBridge.swift:380-395 - Consider using @unknown default for better compile-time safety in DPoP error switch
  • android/A0Auth0Module.kt:430-443 - Consider exhaustive matching for DPoPException to catch new exception types at compile-time
  • android/A0Auth0Module.kt:45 - Evaluate if generic DPOP_GENERATION_FAILED_CODE is necessary given more specific error codes

Security Review: ✅ No security vulnerabilities found

  • Proper cryptographic key storage on all platforms
  • No sensitive data leakage in error messages
  • Correct implementation of RFC 9449 DPoP specification
  • Appropriate input validation and sanitization
  • Token binding correctly implemented

Performance Review: ✅ No performance concerns

  • Efficient key reuse without redundant generation
  • Minimal overhead for DPoP proof generation
  • Proper cleanup on logout preventing memory leaks
  • No blocking operations on main thread

View Job Run | Branch: feat/dpop-support

s.requires_arc = true

s.dependency 'Auth0', '2.13'
s.dependency 'Auth0', '2.14'

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.

Suggested change
s.dependency 'Auth0', '2.14'
s.dependency 'Auth0', '~> 2.14.0'

}

// Validate URL format
guard !url.isEmpty, let urlObj = URL(string: url) else {

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.

Suggested change
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
}

Comment on lines +430 to +443
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)
}

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"

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.

Comment on lines +380 to +395
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
}
}

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:

Suggested change
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
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants