auth0-angular
auth0-angular copied to clipboard
feat: add DPoP support with fetcher API
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-jsto v2.10.0- Adds DPoP (Demonstrating Proof-of-Possession) support
- Includes new authentication methods and security features
-
๐ง Update
handleRedirectCallbackreturn type- Added
ConnectAccountRedirectResultto support account linking flows - Fully backward compatible - existing code works unchanged
- Added
-
๐งช Add
cross-fetchpolyfill- 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 optionsFetcher- Fetcher instance typeCustomFetchMinimalOutput- 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
Authorizationheaders (DPoP <token>orBearer <token>) - โ
Generates and includes DPoP proofs in the
DPoPheader - โ 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
- โ
handleRedirectCallbackreturn 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
- Depends on:
@auth0/auth0-spa-js>= 2.10.0 (included) - Related: auth0-react #869
- Spec: RFC 9449 - OAuth 2.0 DPoP
Checklist
- [x] Tests pass
- [x] Types exported
- [x] Documentation added
- [x] No breaking changes
- [x] Backward compatible