Skip to content

Commit c80019d

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 f769c9c commit c80019d

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

1414
## Add login to your application
@@ -381,17 +381,30 @@ export class AppComponent {
381381
}
382382
```
383383
384-
## DPoP (Demonstrating Proof-of-Possession)
384+
## Device-bound tokens with DPoP
385385
386-
[DPoP](https://datatracker.ietf.org/doc/html/rfc9449) is a security mechanism that cryptographically binds access tokens to clients, providing protection against:
386+
**Demonstrating Proof-of-Possession** —or simply **DPoP**— is a recent OAuth 2.0 extension defined in [RFC9449](https://datatracker.ietf.org/doc/html/rfc9449).
387387
388-
- **Token Theft** - Stolen tokens are cryptographically bound and unusable by attackers
389-
- **Replay Attacks** - Tokens are tied to specific HTTP requests
390-
- **Token Exfiltration** - Tokens require the client's private key to use
388+
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.**
391389
392-
### Enable DPoP
390+
To support DPoP in `auth0-angular`, some APIs available in modern browsers are required:
393391
394-
To enable DPoP support, set `useDpop: true` in your Auth0 configuration:
392+
- [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.
393+
394+
- [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).
395+
396+
The following OAuth 2.0 flows are currently supported by `auth0-angular`:
397+
398+
- [Authorization Code Flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow) (`authorization_code`).
399+
400+
- [Refresh Token Flow](https://auth0.com/docs/secure/tokens/refresh-tokens) (`refresh_token`).
401+
402+
> [!IMPORTANT]
403+
> Currently, only the `ES256` algorithm is supported.
404+
405+
### Enabling DPoP
406+
407+
DPoP is disabled by default. To enable it, set the `useDpop` option to `true` when configuring the SDK. For example:
395408
396409
```ts
397410
import { NgModule } from '@angular/core';
@@ -402,82 +415,167 @@ import { AuthModule } from '@auth0/auth0-angular';
402415
AuthModule.forRoot({
403416
domain: 'YOUR_AUTH0_DOMAIN',
404417
clientId: 'YOUR_AUTH0_CLIENT_ID',
418+
useDpop: true, // 👈
405419
authorizationParams: {
406420
redirect_uri: window.location.origin,
407-
audience: 'https://api.example.com',
408421
},
409-
useDpop: true, // Enable DPoP
410422
}),
411423
],
412424
})
413425
export class AppModule {}
414426
```
415427
416-
### Using createFetcher (Recommended)
428+
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**.
429+
430+
> [!IMPORTANT]
431+
> 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.
432+
>
433+
> You decide how to handle this transition. For example, you might require users to log in again the next time they use your application.
434+
435+
> [!NOTE]
436+
> Using DPoP requires storing some temporary data in the user's browser. When you log the user out with `logout()`, this data is deleted.
437+
438+
> [!TIP]
439+
> 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.
440+
441+
### Using DPoP in your own requests
442+
443+
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>`.
417444

418-
The simplest way to make authenticated API calls with DPoP is using the `createFetcher` method. It automatically handles tokens, DPoP proofs, and nonce management:
445+
For internal requests sent by `auth0-angular` to Auth0, simply enable the `useDpop` option and **every interaction with Auth0 will be protected**.
446+
447+
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:
448+
449+
- `getDpopNonce()`
450+
- `setDpopNonce()`
451+
- `generateDpopProof()`
452+
453+
However, due to the nature of how DPoP works, **this is not a trivial task**:
454+
455+
- When a nonce is missing or expired, the request may need to be retried.
456+
- Received nonces must be stored and managed.
457+
- DPoP headers must be generated and included in every request, and regenerated for retries.
458+
459+
Because of this, we recommend using the provided `createFetcher()` method with `fetchWithAuth()`, which **handles all of this for you**.
460+
461+
#### Simple usage
462+
463+
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.
464+
465+
For example, if you had this code:
466+
467+
```ts
468+
const response = await fetch('https://api.example.com/foo', {
469+
method: 'GET',
470+
headers: { 'user-agent': 'My Client 1.0' },
471+
});
472+
473+
console.log(response.status);
474+
console.log(response.headers);
475+
console.log(await response.json());
476+
```
477+
478+
You would change it as follows:
419479

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

462-
The `createFetcher` method automatically:
508+
When using `fetchWithAuth()`, the following will be handled for you automatically:
463509

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

471-
### Multiple API Endpoints
515+
> [!IMPORTANT]
516+
> 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.
472517

473-
When working with multiple APIs, create separate fetchers for each. Each fetcher manages its own nonces independently:
518+
#### Advanced usage
519+
520+
If you need something more complex than the example above, you can provide a custom implementation in the `fetch` property.
521+
522+
However, since `auth0-angular` needs to make decisions based on HTTP responses, your implementation **must return an object with _at least_ two properties**:
523+
524+
1. `status`: the response status code as a number.
525+
2. `headers`: the response headers as a plain object or as a Fetch API's Headers-like interface.
526+
527+
Whatever it returns, it will be passed as the output of the `fetchWithAuth()` method.
528+
529+
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.
530+
531+
##### Having a base URL
532+
533+
If you need to make requests to different endpoints of the same API, passing a `baseUrl` to `createFetcher()` can be useful:
474534

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

514-
### Advanced: Manual DPoP Management
612+
##### Manual DPoP management
515613

516-
For scenarios requiring full control over DPoP proof generation and nonce management:
614+
For scenarios requiring full control over DPoP proof generation and nonce management, you can use the low-level methods:
517615

518616
```ts
519617
import { Component } from '@angular/core';
@@ -575,46 +673,6 @@ export class AdvancedComponent {
575673
}
576674
```
577675

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

620678
When using standalone components, enable DPoP in your `provideAuth0` configuration:
@@ -629,11 +687,10 @@ bootstrapApplication(AppComponent, {
629687
provideAuth0({
630688
domain: 'YOUR_AUTH0_DOMAIN',
631689
clientId: 'YOUR_AUTH0_CLIENT_ID',
690+
useDpop: true, // 👈
632691
authorizationParams: {
633692
redirect_uri: window.location.origin,
634-
audience: 'https://api.example.com',
635693
},
636-
useDpop: true, // Enable DPoP
637694
}),
638695
],
639696
});

0 commit comments

Comments
 (0)