Skip to content

Conversation

@yogeshchoudhary147
Copy link
Contributor

@yogeshchoudhary147 yogeshchoudhary147 commented Dec 4, 2025

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

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

- Added cross-fetch polyfill to test-setup.ts to support fetch API in tests
- Updated handleRedirectCallback return type to include ConnectAccountRedirectResult
- Updated test mocks to support new return types
- Required for compatibility with @auth0/auth0-spa-js v2.10.0
- Add getDpopNonce() method to retrieve DPoP nonce for a domain
- Add setDpopNonce() method to set DPoP nonce for a domain
- Add generateDpopProof() method to generate DPoP proof JWT
- Add createFetcher() method to create authenticated HTTP fetcher
- Export DPoP-related types: Fetcher, FetcherConfig, CustomFetchMinimalOutput
- Export UseDpopNonceError for error handling
- Add comprehensive unit tests for all DPoP methods
@yogeshchoudhary147 yogeshchoudhary147 requested a review from a team as a code owner December 4, 2025 05:41
@yogeshchoudhary147 yogeshchoudhary147 changed the title feat: Add DPoP support and upgrade to @auth0/auth0-spa-js v2.10.0 feat: add DPoP support with fetcher API Dec 4, 2025
url?: string
): Observable<RedirectLoginResult<TAppState>> {
): Observable<
RedirectLoginResult<TAppState> | ConnectAccountRedirectResult<AppState>
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor pick, should this generic type be ConnectAccountRedirectResult<AppState> as ConnectAccountRedirectResult<TAppState>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will fix this

@gyaneshgouraw-okta
Copy link
Contributor

@yogeshchoudhary147 We should also update Examples.md file for this.

@yogeshchoudhary147 yogeshchoudhary147 merged commit f38b49f into main Dec 22, 2025
8 of 9 checks passed
@yogeshchoudhary147 yogeshchoudhary147 deleted the feat/dpop-support branch December 22, 2025 04:49
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.

3 participants