Skip to content

Commit 0dd77e9

Browse files
docs: Restructure DPoP examples to match auth0-react pattern
- Change section title to 'Device-bound tokens with DPoP' - Add detailed intro explaining DPoP security benefits - Document browser API requirements (Crypto API, IndexedDB) - List supported OAuth 2.0 flows - Add important callouts about ES256, new sessions, and sender constraining - Reorganize content to match auth0-react structure - Move manual DPoP management under Advanced usage subsection - Add clearer progression from simple to advanced usage
1 parent 16173d0 commit 0dd77e9

File tree

1 file changed

+147
-90
lines changed

1 file changed

+147
-90
lines changed

EXAMPLES.md

Lines changed: 147 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
- [Call an API](#call-an-api)
99
- [Handling errors](#handling-errors)
1010
- [Organizations](#organizations)
11-
- [DPoP (Demonstrating Proof-of-Possession)](#dpop-demonstrating-proof-of-possession)
11+
- [Device-bound tokens with DPoP](#device-bound-tokens-with-dpop)
1212
- [Standalone Components and a more functional approach](#standalone-components-and-a-more-functional-approach)
1313
- [Connect Accounts for using Token Vault](#connect-accounts-for-using-token-vault)
1414

@@ -382,17 +382,30 @@ export class AppComponent {
382382
}
383383
```
384384
385-
## DPoP (Demonstrating Proof-of-Possession)
385+
## Device-bound tokens with DPoP
386386
387-
[DPoP](https://datatracker.ietf.org/doc/html/rfc9449) is a security mechanism that cryptographically binds access tokens to clients, providing protection against:
387+
**Demonstrating Proof-of-Possession** —or simply **DPoP**— is a recent OAuth 2.0 extension defined in [RFC9449](https://datatracker.ietf.org/doc/html/rfc9449).
388388
389-
- **Token Theft** - Stolen tokens are cryptographically bound and unusable by attackers
390-
- **Replay Attacks** - Tokens are tied to specific HTTP requests
391-
- **Token Exfiltration** - Tokens require the client's private key to use
389+
It defines a mechanism for securely binding tokens to a specific device using cryptographic signatures. Without it, **a token leak caused by XSS or other vulnerabilities could allow an attacker to impersonate the real user.**
392390
393-
### Enable DPoP
391+
To support DPoP in `auth0-angular`, some APIs available in modern browsers are required:
394392
395-
To enable DPoP support, set `useDpop: true` in your Auth0 configuration:
393+
- [Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Crypto): allows to create and use cryptographic keys, which are used to generate the proofs (i.e. signatures) required for DPoP.
394+
395+
- [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API): enables the use of cryptographic keys [without exposing the private material](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto#storing_keys).
396+
397+
The following OAuth 2.0 flows are currently supported by `auth0-angular`:
398+
399+
- [Authorization Code Flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow) (`authorization_code`).
400+
401+
- [Refresh Token Flow](https://auth0.com/docs/secure/tokens/refresh-tokens) (`refresh_token`).
402+
403+
> [!IMPORTANT]
404+
> Currently, only the `ES256` algorithm is supported.
405+
406+
### Enabling DPoP
407+
408+
DPoP is disabled by default. To enable it, set the `useDpop` option to `true` when configuring the SDK. For example:
396409
397410
```ts
398411
import { NgModule } from '@angular/core';
@@ -403,82 +416,167 @@ import { AuthModule } from '@auth0/auth0-angular';
403416
AuthModule.forRoot({
404417
domain: 'YOUR_AUTH0_DOMAIN',
405418
clientId: 'YOUR_AUTH0_CLIENT_ID',
419+
useDpop: true, // 👈
406420
authorizationParams: {
407421
redirect_uri: window.location.origin,
408-
audience: 'https://api.example.com',
409422
},
410-
useDpop: true, // Enable DPoP
411423
}),
412424
],
413425
})
414426
export class AppModule {}
415427
```
416428
417-
### Using createFetcher (Recommended)
429+
After enabling DPoP, **every new session using a supported OAuth 2.0 flow in Auth0 will begin transparently to use tokens that are cryptographically bound to the current browser**.
430+
431+
> [!IMPORTANT]
432+
> DPoP will only be used for new user sessions created after enabling it. Any previously existing sessions will continue using non-DPoP tokens until the user logs in again.
433+
>
434+
> You decide how to handle this transition. For example, you might require users to log in again the next time they use your application.
435+
436+
> [!NOTE]
437+
> Using DPoP requires storing some temporary data in the user's browser. When you log the user out with `logout()`, this data is deleted.
438+
439+
> [!TIP]
440+
> If all your clients are already using DPoP, you may want to increase security by making Auth0 reject any non-DPoP interactions. See [the docs on Sender Constraining](https://auth0.com/docs/secure/sender-constraining/configure-sender-constraining) for details.
441+
442+
### Using DPoP in your own requests
418443

419-
The simplest way to make authenticated API calls with DPoP is using the `createFetcher` method. It automatically handles tokens, DPoP proofs, and nonce management:
444+
You use a DPoP token the same way as a "traditional" access token, except it must be sent to the server with an `Authorization: DPoP <token>` header instead of the usual `Authorization: Bearer <token>`.
445+
446+
For internal requests sent by `auth0-angular` to Auth0, simply enable the `useDpop` option and **every interaction with Auth0 will be protected**.
447+
448+
However, **to use DPoP with a custom, external API, some additional work is required**. The `AuthService` provides some low-level methods to help with this:
449+
450+
- `getDpopNonce()`
451+
- `setDpopNonce()`
452+
- `generateDpopProof()`
453+
454+
However, due to the nature of how DPoP works, **this is not a trivial task**:
455+
456+
- When a nonce is missing or expired, the request may need to be retried.
457+
- Received nonces must be stored and managed.
458+
- DPoP headers must be generated and included in every request, and regenerated for retries.
459+
460+
Because of this, we recommend using the provided `createFetcher()` method with `fetchWithAuth()`, which **handles all of this for you**.
461+
462+
#### Simple usage
463+
464+
The `fetchWithAuth()` method is a drop-in replacement for the native `fetch()` function from the Fetch API, so if you're already using it, the change will be minimal.
465+
466+
For example, if you had this code:
467+
468+
```ts
469+
const response = await fetch('https://api.example.com/foo', {
470+
method: 'GET',
471+
headers: { 'user-agent': 'My Client 1.0' },
472+
});
473+
474+
console.log(response.status);
475+
console.log(response.headers);
476+
console.log(await response.json());
477+
```
478+
479+
You would change it as follows:
420480

421481
```ts
422482
import { Component } from '@angular/core';
423483
import { AuthService } from '@auth0/auth0-angular';
424484
425485
@Component({
426486
selector: 'app-data',
427-
template: `
428-
<div *ngIf="loading">Loading...</div>
429-
<div *ngIf="data">{{ data | json }}</div>
430-
<div *ngIf="error">{{ error }}</div>
431-
`,
487+
template: `...`,
432488
})
433489
export class DataComponent {
434-
data: any;
435-
loading = false;
436-
error: string | null = null;
437-
438490
constructor(private auth: AuthService) {}
439491
440492
async fetchData() {
441-
this.loading = true;
442-
this.error = null;
493+
const fetcher = this.auth.createFetcher({
494+
dpopNonceId: 'my_api_request',
495+
});
443496
444-
try {
445-
// Create fetcher - handles tokens, DPoP proofs, and nonces automatically
446-
const fetcher = this.auth.createFetcher({
447-
dpopNonceId: 'my-api',
448-
baseUrl: 'https://api.example.com',
449-
});
497+
const response = await fetcher.fetchWithAuth('https://api.example.com/foo', {
498+
method: 'GET',
499+
headers: { 'user-agent': 'My Client 1.0' },
500+
});
450501
451-
const response = await fetcher.fetchWithAuth('/protected-data');
452-
this.data = await response.json();
453-
} catch (err) {
454-
this.error = 'Failed to fetch data';
455-
console.error(err);
456-
} finally {
457-
this.loading = false;
458-
}
502+
console.log(response.status);
503+
console.log(response.headers);
504+
console.log(await response.json());
459505
}
460506
}
461507
```
462508

463-
The `createFetcher` method automatically:
509+
When using `fetchWithAuth()`, the following will be handled for you automatically:
464510

465-
- Retrieves access tokens
466-
- Adds proper `Authorization` headers (`DPoP <token>` or `Bearer <token>`)
467-
- Generates and includes DPoP proofs in the `DPoP` header
468-
- Manages DPoP nonces per API endpoint
469-
- Automatically retries on nonce errors
470-
- Handles token refreshing
511+
- Use `getAccessTokenSilently()` to get the access token to inject in the headers.
512+
- Generate and inject DPoP headers when needed.
513+
- Store and update any DPoP nonces.
514+
- Handle retries caused by a rejected nonce.
471515

472-
### Multiple API Endpoints
516+
> [!IMPORTANT]
517+
> If DPoP is enabled, a `dpopNonceId` **must** be present in the `createFetcher()` parameters, since it's used to keep track of the DPoP nonces for each request.
473518

474-
When working with multiple APIs, create separate fetchers for each. Each fetcher manages its own nonces independently:
519+
#### Advanced usage
520+
521+
If you need something more complex than the example above, you can provide a custom implementation in the `fetch` property.
522+
523+
However, since `auth0-angular` needs to make decisions based on HTTP responses, your implementation **must return an object with _at least_ two properties**:
524+
525+
1. `status`: the response status code as a number.
526+
2. `headers`: the response headers as a plain object or as a Fetch API's Headers-like interface.
527+
528+
Whatever it returns, it will be passed as the output of the `fetchWithAuth()` method.
529+
530+
Your implementation will be called with a standard, ready-to-use [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object, which will contain any headers needed for authorization and DPoP usage (if enabled). Depending on your needs, you can use this object directly or treat it as a container with everything required to make the request your own way.
531+
532+
##### Having a base URL
533+
534+
If you need to make requests to different endpoints of the same API, passing a `baseUrl` to `createFetcher()` can be useful:
475535

476536
```ts
477537
import { Injectable } from '@angular/core';
478538
import { AuthService, Fetcher } from '@auth0/auth0-angular';
479539
480540
@Injectable({ providedIn: 'root' })
481541
export class ApiService {
542+
private fetcher: Fetcher;
543+
544+
constructor(private auth: AuthService) {
545+
this.fetcher = this.auth.createFetcher({
546+
dpopNonceId: 'my-api',
547+
baseUrl: 'https://api.example.com',
548+
});
549+
}
550+
551+
async getFoo() {
552+
return this.fetcher.fetchWithAuth('/foo'); // => https://api.example.com/foo
553+
}
554+
555+
async getBar() {
556+
return this.fetcher.fetchWithAuth('/bar'); // => https://api.example.com/bar
557+
}
558+
559+
async getXyz() {
560+
return this.fetcher.fetchWithAuth('/xyz'); // => https://api.example.com/xyz
561+
}
562+
563+
async getFromOtherApi() {
564+
// If the passed URL is absolute, `baseUrl` will be ignored for convenience:
565+
return this.fetcher.fetchWithAuth('https://other-api.example.com/foo');
566+
}
567+
}
568+
```
569+
570+
##### Multiple API endpoints
571+
572+
When working with multiple APIs, create separate fetchers for each. Each fetcher manages its own nonces independently:
573+
574+
```ts
575+
import { Injectable } from '@angular/core';
576+
import { AuthService, Fetcher } from '@auth0/auth0-angular';
577+
578+
@Injectable({ providedIn: 'root' })
579+
export class MultiApiService {
482580
private internalApi: Fetcher;
483581
private partnerApi: Fetcher;
484582
@@ -512,9 +610,9 @@ export class ApiService {
512610
}
513611
```
514612

515-
### Advanced: Manual DPoP Management
613+
##### Manual DPoP management
516614

517-
For scenarios requiring full control over DPoP proof generation and nonce management:
615+
For scenarios requiring full control over DPoP proof generation and nonce management, you can use the low-level methods:
518616

519617
```ts
520618
import { Component } from '@angular/core';
@@ -576,46 +674,6 @@ export class AdvancedComponent {
576674
}
577675
```
578676

579-
### Error Handling
580-
581-
Handle DPoP-specific errors using the `UseDpopNonceError` class:
582-
583-
```ts
584-
import { Component } from '@angular/core';
585-
import { AuthService, UseDpopNonceError } from '@auth0/auth0-angular';
586-
587-
@Component({
588-
selector: 'app-error-handling',
589-
template: `...`,
590-
})
591-
export class ErrorHandlingComponent {
592-
constructor(private auth: AuthService) {}
593-
594-
async fetchWithErrorHandling() {
595-
try {
596-
const fetcher = this.auth.createFetcher({
597-
dpopNonceId: 'my-api',
598-
baseUrl: 'https://api.example.com',
599-
});
600-
601-
const response = await fetcher.fetchWithAuth('/data');
602-
const data = await response.json();
603-
return data;
604-
} catch (error) {
605-
if (error instanceof UseDpopNonceError) {
606-
// DPoP nonce validation failed
607-
console.error('DPoP nonce error:', error.message);
608-
// The fetcher automatically retries, so this error means
609-
// the retry also failed
610-
} else {
611-
console.error('Other error:', error);
612-
}
613-
throw error;
614-
}
615-
}
616-
}
617-
```
618-
619677
### Standalone Components with DPoP
620678

621679
When using standalone components, enable DPoP in your `provideAuth0` configuration:
@@ -630,11 +688,10 @@ bootstrapApplication(AppComponent, {
630688
provideAuth0({
631689
domain: 'YOUR_AUTH0_DOMAIN',
632690
clientId: 'YOUR_AUTH0_CLIENT_ID',
691+
useDpop: true, // 👈
633692
authorizationParams: {
634693
redirect_uri: window.location.origin,
635-
audience: 'https://api.example.com',
636694
},
637-
useDpop: true, // Enable DPoP
638695
}),
639696
],
640697
});

0 commit comments

Comments
 (0)