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 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.

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