diff --git a/EXAMPLES.md b/EXAMPLES.md index efde66aa..5da255ea 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -8,6 +8,7 @@ - [Call an API](#call-an-api) - [Handling errors](#handling-errors) - [Organizations](#organizations) +- [Device-bound tokens with DPoP](#device-bound-tokens-with-dpop) - [Standalone Components and a more functional approach](#standalone-components-and-a-more-functional-approach) - [Connect Accounts for using Token Vault](#connect-accounts-for-using-token-vault) @@ -381,6 +382,321 @@ export class AppComponent { } ``` +## Device-bound tokens with DPoP + +**Demonstrating Proof-of-Possession** β€”or simply **DPoP**β€” is a recent OAuth 2.0 extension defined in [RFC9449](https://datatracker.ietf.org/doc/html/rfc9449). + +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.** + +To support DPoP in `auth0-angular`, some APIs available in modern browsers are required: + +- [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. + +- [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). + +The following OAuth 2.0 flows are currently supported by `auth0-angular`: + +- [Authorization Code Flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow) (`authorization_code`). + +- [Refresh Token Flow](https://auth0.com/docs/secure/tokens/refresh-tokens) (`refresh_token`). + +> [!IMPORTANT] +> Currently, only the `ES256` algorithm is supported. + +### Enabling DPoP + +DPoP is disabled by default. To enable it, set the `useDpop` option to `true` when configuring the SDK. For example: + +```ts +import { NgModule } from '@angular/core'; +import { AuthModule } from '@auth0/auth0-angular'; + +@NgModule({ + imports: [ + AuthModule.forRoot({ + domain: 'YOUR_AUTH0_DOMAIN', + clientId: 'YOUR_AUTH0_CLIENT_ID', + useDpop: true, // πŸ‘ˆ + authorizationParams: { + redirect_uri: window.location.origin, + }, + }), + ], +}) +export class AppModule {} +``` + +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**. + +> [!IMPORTANT] +> 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. +> +> You decide how to handle this transition. For example, you might require users to log in again the next time they use your application. + +> [!NOTE] +> Using DPoP requires storing some temporary data in the user's browser. When you log the user out with `logout()`, this data is deleted. + +> [!TIP] +> 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. + +### Using DPoP in your own requests + +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 ` header instead of the usual `Authorization: Bearer `. + +For internal requests sent by `auth0-angular` to Auth0, simply enable the `useDpop` option and **every interaction with Auth0 will be protected**. + +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: + +- `getDpopNonce()` +- `setDpopNonce()` +- `generateDpopProof()` + +However, due to the nature of how DPoP works, **this is not a trivial task**: + +- When a nonce is missing or expired, the request may need to be retried. +- Received nonces must be stored and managed. +- DPoP headers must be generated and included in every request, and regenerated for retries. + +Because of this, we recommend using the provided `createFetcher()` method with `fetchWithAuth()`, which **handles all of this for you**. + +#### Simple usage + +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. + +For example, if you had this code: + +```ts +const response = await fetch('https://api.example.com/foo', { + method: 'GET', + headers: { 'user-agent': 'My Client 1.0' }, +}); + +console.log(response.status); +console.log(response.headers); +console.log(await response.json()); +``` + +You would change it as follows: + +```ts +import { Component } from '@angular/core'; +import { AuthService } from '@auth0/auth0-angular'; + +@Component({ + selector: 'app-data', + template: `...`, +}) +export class DataComponent { + constructor(private auth: AuthService) {} + + async fetchData() { + const fetcher = this.auth.createFetcher({ + dpopNonceId: 'my_api_request', + }); + + const response = await fetcher.fetchWithAuth('https://api.example.com/foo', { + method: 'GET', + headers: { 'user-agent': 'My Client 1.0' }, + }); + + console.log(response.status); + console.log(response.headers); + console.log(await response.json()); + } +} +``` + +When using `fetchWithAuth()`, the following will be handled for you automatically: + +- Use `getAccessTokenSilently()` to get the access token to inject in the headers. +- Generate and inject DPoP headers when needed. +- Store and update any DPoP nonces. +- Handle retries caused by a rejected nonce. + +> [!IMPORTANT] +> 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. + +#### Advanced usage + +If you need something more complex than the example above, you can provide a custom implementation in the `fetch` property. + +However, since `auth0-angular` needs to make decisions based on HTTP responses, your implementation **must return an object with _at least_ two properties**: + +1. `status`: the response status code as a number. +2. `headers`: the response headers as a plain object or as a Fetch API's Headers-like interface. + +Whatever it returns, it will be passed as the output of the `fetchWithAuth()` method. + +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. + +##### Having a base URL + +If you need to make requests to different endpoints of the same API, passing a `baseUrl` to `createFetcher()` can be useful: + +```ts +import { Injectable } from '@angular/core'; +import { AuthService, Fetcher } from '@auth0/auth0-angular'; + +@Injectable({ providedIn: 'root' }) +export class ApiService { + private fetcher: Fetcher; + + constructor(private auth: AuthService) { + this.fetcher = this.auth.createFetcher({ + dpopNonceId: 'my-api', + baseUrl: 'https://api.example.com', + }); + } + + async getFoo() { + return this.fetcher.fetchWithAuth('/foo'); // => https://api.example.com/foo + } + + async getBar() { + return this.fetcher.fetchWithAuth('/bar'); // => https://api.example.com/bar + } + + async getXyz() { + return this.fetcher.fetchWithAuth('/xyz'); // => https://api.example.com/xyz + } + + async getFromOtherApi() { + // If the passed URL is absolute, `baseUrl` will be ignored for convenience: + return this.fetcher.fetchWithAuth('https://other-api.example.com/foo'); + } +} +``` + +##### Multiple API endpoints + +When working with multiple APIs, create separate fetchers for each. Each fetcher manages its own nonces independently: + +```ts +import { Injectable } from '@angular/core'; +import { AuthService, Fetcher } from '@auth0/auth0-angular'; + +@Injectable({ providedIn: 'root' }) +export class MultiApiService { + 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 getInternalData() { + const response = await this.internalApi.fetchWithAuth('/data'); + return response.json(); + } + + async getPartnerResources() { + const response = await this.partnerApi.fetchWithAuth('/resources'); + return response.json(); + } + + async getAllData() { + const [internal, partner] = await Promise.all([this.getInternalData(), this.getPartnerResources()]); + return { internal, partner }; + } +} +``` + +##### Manual DPoP management + +For scenarios requiring full control over DPoP proof generation and nonce management, you can use the low-level methods: + +```ts +import { Component } from '@angular/core'; +import { AuthService, UseDpopNonceError } from '@auth0/auth0-angular'; +import { firstValueFrom } from 'rxjs'; + +@Component({ + selector: 'app-advanced', + template: ``, +}) +export class AdvancedComponent { + constructor(private auth: AuthService) {} + + async makeRequest() { + try { + // 1. Get access token + const token = await firstValueFrom(this.auth.getAccessTokenSilently()); + + // 2. Get current DPoP nonce for the API + const nonce = await firstValueFrom(this.auth.getDpopNonce('my-api')); + + // 3. Generate DPoP proof + const proof = await firstValueFrom( + this.auth.generateDpopProof({ + url: 'https://api.example.com/data', + method: 'POST', + accessToken: token!, + nonce, + }) + ); + + // 4. Make the API request + 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' }), + }); + + // 5. Update nonce if server provides a new one + const newNonce = response.headers.get('DPoP-Nonce'); + if (newNonce) { + await firstValueFrom(this.auth.setDpopNonce(newNonce, 'my-api')); + } + + const data = await response.json(); + console.log('Success:', data); + } catch (error) { + if (error instanceof UseDpopNonceError) { + console.error('DPoP nonce error:', error.message); + } else { + console.error('Request failed:', error); + } + } + } +} +``` + +### Standalone Components with DPoP + +When using standalone components, enable DPoP in your `provideAuth0` configuration: + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideAuth0 } from '@auth0/auth0-angular'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, { + providers: [ + provideAuth0({ + domain: 'YOUR_AUTH0_DOMAIN', + clientId: 'YOUR_AUTH0_CLIENT_ID', + useDpop: true, // πŸ‘ˆ + authorizationParams: { + redirect_uri: window.location.origin, + }, + }), + ], +}); +``` + ## Standalone components and a more functional approach 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.