auth0-angular icon indicating copy to clipboard operation
auth0-angular copied to clipboard

feat: add DPoP support with fetcher API

Open yogeshchoudhary147 opened this issue 1 month ago โ€ข 0 comments

Description

Adds DPoP (Demonstrating Proof-of-Possession) support by exposing new authentication methods from @auth0/auth0-spa-js v2.10.0. DPoP cryptographically binds access tokens to clients, preventing token theft and replay attacks.

What is DPoP?

DPoP prevents security vulnerabilities by:

  • Token Theft Protection - Stolen tokens are cryptographically bound and unusable
  • Replay Attack Prevention - Tokens are tied to specific HTTP requests
  • Token Exfiltration Mitigation - Tokens require the client's private key

Changes

1. Dependency & Compatibility Updates

  • โฌ†๏ธ Upgrade @auth0/auth0-spa-js to v2.10.0

    • Adds DPoP (Demonstrating Proof-of-Possession) support
    • Includes new authentication methods and security features
  • ๐Ÿ”ง Update handleRedirectCallback return type

    • Added ConnectAccountRedirectResult to support account linking flows
    • Fully backward compatible - existing code works unchanged
  • ๐Ÿงช Add cross-fetch polyfill

    • auth0-spa-js v2.10.0 uses native Fetch API which isn't available in Node.js test environments
    • Polyfill provides fetch, Headers, Request, Response globals for Jest tests
    • See test-setup.ts - only affects test environment, not production

2. New DPoP Methods

Method Description
getDpopNonce(id?) Retrieve DPoP nonce for an API identifier
setDpopNonce(nonce, id?) Store DPoP nonce for future requests
generateDpopProof(params) Generate cryptographic DPoP proof JWT
createFetcher(config?) Create authenticated fetcher with automatic DPoP handling โญ

3. New Exports

Class:

  • UseDpopNonceError - DPoP nonce error class

Types:

  • FetcherConfig - Fetcher configuration options
  • Fetcher - Fetcher instance type
  • CustomFetchMinimalOutput - Custom response type constraint

4. Documentation & Testing

  • โœ… Comprehensive JSDoc documentation for all methods
  • โœ… Unit tests for all 4 new DPoP methods
  • โœ… All existing tests pass

Usage

Basic Setup

import { AuthModule } from '@auth0/auth0-angular';

@NgModule({
  imports: [
    AuthModule.forRoot({
      domain: 'YOUR_DOMAIN',
      clientId: 'YOUR_CLIENT_ID',
      authorizationParams: {
        redirect_uri: window.location.origin,
        audience: 'https://api.example.com'
      },
      useDpop: true  // Enable DPoP
    })
  ]
})
export class AppModule { }

Recommended: Using createFetcher() โญ

The simplest way to make authenticated API calls with DPoP:

import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({
  selector: 'app-data',
  template: `<div>{{ data | json }}</div>`
})
export class DataComponent {
  data: any;

  constructor(private auth: AuthService) {}

  async fetchData() {
    // Create fetcher - handles tokens, DPoP proofs, and nonces automatically
    const fetcher = this.auth.createFetcher({ 
      dpopNonceId: 'my-api',
      baseUrl: 'https://api.example.com'
    });

    const response = await fetcher.fetchWithAuth('/protected-data');
    this.data = await response.json();
  }
}

What createFetcher() does automatically:

  • โœ… Retrieves access tokens via getAccessTokenSilently()
  • โœ… Adds proper Authorization headers (DPoP <token> or Bearer <token>)
  • โœ… Generates and includes DPoP proofs in the DPoP header
  • โœ… Manages DPoP nonces per API endpoint
  • โœ… Automatically retries on nonce errors
  • โœ… Handles token refreshing
  • โœ… Works with both DPoP and Bearer tokens

Multiple APIs

@Injectable({ providedIn: 'root' })
export class ApiService {
  private internalApi: Fetcher;
  private partnerApi: Fetcher;

  constructor(private auth: AuthService) {
    // Each fetcher manages its own nonces independently
    this.internalApi = this.auth.createFetcher({
      dpopNonceId: 'internal-api',
      baseUrl: 'https://internal.example.com'
    });

    this.partnerApi = this.auth.createFetcher({
      dpopNonceId: 'partner-api',
      baseUrl: 'https://partner.example.com'
    });
  }

  async getData() {
    const data1 = await this.internalApi.fetchWithAuth('/data');
    const data2 = await this.partnerApi.fetchWithAuth('/resources');
    return { internal: data1, partner: data2 };
  }
}

Advanced: Manual DPoP Management

For scenarios requiring full control:

import { Component } from '@angular/core';
import { AuthService, UseDpopNonceError } from '@auth0/auth0-angular';

@Component({
  selector: 'app-advanced',
  template: `...`
})
export class AdvancedComponent {
  constructor(private auth: AuthService) {}

  async makeRequest() {
    try {
      const token = await this.auth.getAccessTokenSilently().toPromise();
      const nonce = await this.auth.getDpopNonce('my-api').toPromise();
      
      const proof = await this.auth.generateDpopProof({
        url: 'https://api.example.com/data',
        method: 'POST',
        accessToken: token!,
        nonce
      }).toPromise();

      const response = await fetch('https://api.example.com/data', {
        method: 'POST',
        headers: {
          'Authorization': `DPoP ${token}`,
          'DPoP': proof!,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ data: 'example' })
      });

      // Update nonce if server provides new one
      const newNonce = response.headers.get('DPoP-Nonce');
      if (newNonce) {
        await this.auth.setDpopNonce(newNonce, 'my-api').toPromise();
      }

    } catch (error) {
      if (error instanceof UseDpopNonceError) {
        console.error('DPoP nonce error:', error.message);
      }
    }
  }
}

Error Handling

import { UseDpopNonceError } from '@auth0/auth0-angular';

try {
  const response = await fetcher.fetchWithAuth('/data');
} catch (error) {
  if (error instanceof UseDpopNonceError) {
    // DPoP nonce validation failed
    console.error('Nonce error:', error.message);
  }
}

Why Use createFetcher()?

Feature createFetcher() Manual
Token retrieval โœ… Automatic โŒ Manual
DPoP proof generation โœ… Automatic โŒ Manual
Nonce management โœ… Automatic โŒ Manual
Error retry โœ… Automatic โŒ Manual
Code complexity โœ… Low โŒ High

Breaking Changes

None - Fully backward compatible:

  • โœ… New methods are additive
  • โœ… handleRedirectCallback return type is a union (includes old type)
  • โœ… All existing code works without modification
  • โœ… DPoP is opt-in via useDpop: true

Migration Guide

Enable DPoP

// Add useDpop: true to your AuthModule configuration
AuthModule.forRoot({
  domain: 'YOUR_DOMAIN',
  clientId: 'YOUR_CLIENT_ID',
  useDpop: true  // Add this
})

Update API Calls (Recommended)

// Before
this.auth.getAccessTokenSilently().subscribe(token => {
  fetch('https://api.example.com/data', {
    headers: { 'Authorization': `Bearer ${token}` }
  });
});

// After - Use fetcher
const fetcher = this.auth.createFetcher({
  dpopNonceId: 'my-api',
  baseUrl: 'https://api.example.com'
});
const response = await fetcher.fetchWithAuth('/data');

Testing

  • โœ… Unit tests for getDpopNonce() with/without ID
  • โœ… Unit tests for setDpopNonce() with/without ID
  • โœ… Unit test for generateDpopProof()
  • โœ… Unit tests for createFetcher() with/without config
  • โœ… All existing tests pass

Related

Checklist

  • [x] Tests pass
  • [x] Types exported
  • [x] Documentation added
  • [x] No breaking changes
  • [x] Backward compatible

yogeshchoudhary147 avatar Dec 04 '25 05:12 yogeshchoudhary147