Skip to content

Commit 09f48ea

Browse files
docs: Add DPoP examples to EXAMPLES.md (#768)
1 parent 2f6e62c commit 09f48ea

File tree

1 file changed

+316
-0
lines changed

1 file changed

+316
-0
lines changed

EXAMPLES.md

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- [Call an API](#call-an-api)
99
- [Handling errors](#handling-errors)
1010
- [Organizations](#organizations)
11+
- [Device-bound tokens with DPoP](#device-bound-tokens-with-dpop)
1112
- [Standalone Components and a more functional approach](#standalone-components-and-a-more-functional-approach)
1213
- [Connect Accounts for using Token Vault](#connect-accounts-for-using-token-vault)
1314

@@ -381,6 +382,321 @@ export class AppComponent {
381382
}
382383
```
383384
385+
## Device-bound tokens with DPoP
386+
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).
388+
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.**
390+
391+
To support DPoP in `auth0-angular`, some APIs available in modern browsers are required:
392+
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:
409+
410+
```ts
411+
import { NgModule } from '@angular/core';
412+
import { AuthModule } from '@auth0/auth0-angular';
413+
414+
@NgModule({
415+
imports: [
416+
AuthModule.forRoot({
417+
domain: 'YOUR_AUTH0_DOMAIN',
418+
clientId: 'YOUR_AUTH0_CLIENT_ID',
419+
useDpop: true, // 👈
420+
authorizationParams: {
421+
redirect_uri: window.location.origin,
422+
},
423+
}),
424+
],
425+
})
426+
export class AppModule {}
427+
```
428+
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
443+
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:
480+
481+
```ts
482+
import { Component } from '@angular/core';
483+
import { AuthService } from '@auth0/auth0-angular';
484+
485+
@Component({
486+
selector: 'app-data',
487+
template: `...`,
488+
})
489+
export class DataComponent {
490+
constructor(private auth: AuthService) {}
491+
492+
async fetchData() {
493+
const fetcher = this.auth.createFetcher({
494+
dpopNonceId: 'my_api_request',
495+
});
496+
497+
const response = await fetcher.fetchWithAuth('https://api.example.com/foo', {
498+
method: 'GET',
499+
headers: { 'user-agent': 'My Client 1.0' },
500+
});
501+
502+
console.log(response.status);
503+
console.log(response.headers);
504+
console.log(await response.json());
505+
}
506+
}
507+
```
508+
509+
When using `fetchWithAuth()`, the following will be handled for you automatically:
510+
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.
515+
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.
518+
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:
535+
536+
```ts
537+
import { Injectable } from '@angular/core';
538+
import { AuthService, Fetcher } from '@auth0/auth0-angular';
539+
540+
@Injectable({ providedIn: 'root' })
541+
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 {
580+
private internalApi: Fetcher;
581+
private partnerApi: Fetcher;
582+
583+
constructor(private auth: AuthService) {
584+
// Each fetcher manages its own nonces independently
585+
this.internalApi = this.auth.createFetcher({
586+
dpopNonceId: 'internal-api',
587+
baseUrl: 'https://internal.example.com',
588+
});
589+
590+
this.partnerApi = this.auth.createFetcher({
591+
dpopNonceId: 'partner-api',
592+
baseUrl: 'https://partner.example.com',
593+
});
594+
}
595+
596+
async getInternalData() {
597+
const response = await this.internalApi.fetchWithAuth('/data');
598+
return response.json();
599+
}
600+
601+
async getPartnerResources() {
602+
const response = await this.partnerApi.fetchWithAuth('/resources');
603+
return response.json();
604+
}
605+
606+
async getAllData() {
607+
const [internal, partner] = await Promise.all([this.getInternalData(), this.getPartnerResources()]);
608+
return { internal, partner };
609+
}
610+
}
611+
```
612+
613+
##### Manual DPoP management
614+
615+
For scenarios requiring full control over DPoP proof generation and nonce management, you can use the low-level methods:
616+
617+
```ts
618+
import { Component } from '@angular/core';
619+
import { AuthService, UseDpopNonceError } from '@auth0/auth0-angular';
620+
import { firstValueFrom } from 'rxjs';
621+
622+
@Component({
623+
selector: 'app-advanced',
624+
template: `<button (click)="makeRequest()">Make Request</button>`,
625+
})
626+
export class AdvancedComponent {
627+
constructor(private auth: AuthService) {}
628+
629+
async makeRequest() {
630+
try {
631+
// 1. Get access token
632+
const token = await firstValueFrom(this.auth.getAccessTokenSilently());
633+
634+
// 2. Get current DPoP nonce for the API
635+
const nonce = await firstValueFrom(this.auth.getDpopNonce('my-api'));
636+
637+
// 3. Generate DPoP proof
638+
const proof = await firstValueFrom(
639+
this.auth.generateDpopProof({
640+
url: 'https://api.example.com/data',
641+
method: 'POST',
642+
accessToken: token!,
643+
nonce,
644+
})
645+
);
646+
647+
// 4. Make the API request
648+
const response = await fetch('https://api.example.com/data', {
649+
method: 'POST',
650+
headers: {
651+
Authorization: `DPoP ${token}`,
652+
DPoP: proof!,
653+
'Content-Type': 'application/json',
654+
},
655+
body: JSON.stringify({ data: 'example' }),
656+
});
657+
658+
// 5. Update nonce if server provides a new one
659+
const newNonce = response.headers.get('DPoP-Nonce');
660+
if (newNonce) {
661+
await firstValueFrom(this.auth.setDpopNonce(newNonce, 'my-api'));
662+
}
663+
664+
const data = await response.json();
665+
console.log('Success:', data);
666+
} catch (error) {
667+
if (error instanceof UseDpopNonceError) {
668+
console.error('DPoP nonce error:', error.message);
669+
} else {
670+
console.error('Request failed:', error);
671+
}
672+
}
673+
}
674+
}
675+
```
676+
677+
### Standalone Components with DPoP
678+
679+
When using standalone components, enable DPoP in your `provideAuth0` configuration:
680+
681+
```ts
682+
import { bootstrapApplication } from '@angular/platform-browser';
683+
import { provideAuth0 } from '@auth0/auth0-angular';
684+
import { AppComponent } from './app/app.component';
685+
686+
bootstrapApplication(AppComponent, {
687+
providers: [
688+
provideAuth0({
689+
domain: 'YOUR_AUTH0_DOMAIN',
690+
clientId: 'YOUR_AUTH0_CLIENT_ID',
691+
useDpop: true, // 👈
692+
authorizationParams: {
693+
redirect_uri: window.location.origin,
694+
},
695+
}),
696+
],
697+
});
698+
```
699+
384700
## Standalone components and a more functional approach
385701

386702
As of Angular 15, the Angular team is putting standalone components, as well as a more functional approach, in favor of the traditional use of NgModules and class-based approach.

0 commit comments

Comments
 (0)