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

1 participant