|
8 | 8 | - [Protecting a route in a Next.js app (in SPA mode)](#protecting-a-route-in-a-nextjs-app-in-spa-mode) |
9 | 9 | - [Use with Auth0 organizations](#use-with-auth0-organizations) |
10 | 10 | - [Protecting a route with a claims check](#protecting-a-route-with-a-claims-check) |
| 11 | +- [Device-bound tokens with DPoP](#device-bound-tokens-with-dpop) |
11 | 12 |
|
12 | 13 | ## Use with a Class Component |
13 | 14 |
|
@@ -325,17 +326,181 @@ In order to protect a route with a claims check alongside an authentication requ |
325 | 326 |
|
326 | 327 | ```jsx |
327 | 328 | const withClaimCheck = (Component, myClaimCheckFunction, returnTo) => { |
328 | | - const { user } = useAuth0(); |
| 329 | + const { user } = useAuth0(); |
329 | 330 | if (myClaimCheckFunction(user)) { |
330 | | - return <Component /> |
| 331 | + return <Component />; |
331 | 332 | } |
332 | 333 | Router.push(returnTo); |
333 | | -} |
| 334 | +}; |
334 | 335 |
|
335 | 336 | const checkClaims = (claim?: User) => claim?.['https://my.app.io/jwt/claims']?.ROLE?.includes('ADMIN'); |
336 | 337 |
|
337 | 338 | // Usage |
338 | 339 | const Page = withAuthenticationRequired( |
339 | | - withClaimCheck(Component, checkClaims, '/missing-roles' ) |
| 340 | + withClaimCheck(Component, checkClaims, '/missing-roles') |
340 | 341 | ); |
341 | 342 | ``` |
| 343 | +
|
| 344 | +## Device-bound tokens with DPoP |
| 345 | +
|
| 346 | +**Demonstrating Proof-of-Possession** –or just **DPoP**– is an OAuth 2.0 extension defined in [RFC9449](https://datatracker.ietf.org/doc/html/rfc9449). |
| 347 | +
|
| 348 | +It defines a mechanism for securely binding tokens to a specific device by means of cryptographic signatures. Without it, **a token leak caused by XSS or other vulnerability could result in an attacker impersonating the real user.** |
| 349 | +
|
| 350 | +In order to support DPoP in `auth0-spa-js`, we require some APIs found in modern browsers: |
| 351 | +
|
| 352 | +- [Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Crypto): it allows to create and use cryptographic keys that will be used for creating the proofs (i.e. signatures) used in DPoP. |
| 353 | +
|
| 354 | +- [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API): it allows to use cryptographic keys [without giving access to the private material](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto#storing_keys). |
| 355 | +
|
| 356 | +The following OAuth 2.0 flows are currently supported by `auth0-spa-js`: |
| 357 | +
|
| 358 | +- [Authorization Code Flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow) (`authorization_code`). |
| 359 | +
|
| 360 | +- [Refresh Token Flow](https://auth0.com/docs/secure/tokens/refresh-tokens) (`refresh_token`). |
| 361 | +
|
| 362 | +- [Custom Token Exchange Flow](https://auth0.com/docs/authenticate/custom-token-exchange) (`urn:ietf:params:oauth:grant-type:token-exchange`). |
| 363 | +
|
| 364 | +Currently, only the `ES256` algorithm is supported. |
| 365 | +
|
| 366 | +### Enabling DPoP |
| 367 | +
|
| 368 | +Currently, DPoP is disabled by default. To enable it, set the `useDpop` option to `true` when invoking the provider. For example: |
| 369 | +
|
| 370 | +```jsx |
| 371 | +<Auth0Provider |
| 372 | + domain="YOUR_AUTH0_DOMAIN" |
| 373 | + clientId="YOUR_AUTH0_CLIENT_ID" |
| 374 | + useDpop={true} // 👈 |
| 375 | + authorizationParams={{ redirect_uri: window.location.origin }} |
| 376 | +> |
| 377 | +``` |
| 378 | +
|
| 379 | +After enabling DPoP, supported OAuth 2.0 flows in Auth0 will start transparently issuing tokens that will be cryptographically bound to the current browser. |
| 380 | +
|
| 381 | +Note that a DPoP token will have to be sent to a resource server with an `Authorization: DPoP <token>` header instead of `Authorization: Bearer <token>` as usual. |
| 382 | +
|
| 383 | +If you're using both types at the same time, you can use the `detailedResponse` option in `getAccessTokenSilently()` to get access to the `token_type` property and know what kind of token you got: |
| 384 | +
|
| 385 | +```js |
| 386 | +const headers = { |
| 387 | + Authorization: `${token.token_type} ${token.access_token}`, |
| 388 | +}; |
| 389 | +``` |
| 390 | +
|
| 391 | +If all your clients are already using DPoP, you may want to increase security and make Auth0 reject non-DPoP interactions by enabling the "Require Token Sender-Constraining" option in your Auth0's application settings. Check [the docs](https://auth0.com/docs/get-started/applications/configure-sender-constraining) for details. |
| 392 | +
|
| 393 | +### Clearing DPoP data |
| 394 | +
|
| 395 | +When using DPoP some temporary data is stored in the user's browser. When you log the user out with `logout()`, it will be deleted. |
| 396 | +
|
| 397 | +### Using DPoP in your own requests |
| 398 | +
|
| 399 | +Enabling `useDpop` **protects every internal request that the SDK sends to Auth0** (i.e. the authorization server). |
| 400 | +
|
| 401 | +However, if you want to use a DPoP access token to authenticate against a custom API (i.e. a resource server), some extra work is required. `Auth0Provider` has some methods that will provide the needed pieces: |
| 402 | +
|
| 403 | +- `getDpopNonce()` |
| 404 | +- `setDpopNonce()` |
| 405 | +- `generateDpopProof()` |
| 406 | +
|
| 407 | +This example shows how these coould be used: |
| 408 | +
|
| 409 | +```jsx |
| 410 | +import { useEffect, useState } from 'react'; |
| 411 | +import { useAuth0 } from '@auth0/auth0-react'; |
| 412 | + |
| 413 | +const Posts = () => { |
| 414 | + const { |
| 415 | + getAccessTokenSilently, |
| 416 | + getDpopNonce, |
| 417 | + setDpopNonce, |
| 418 | + generateDpopProof, |
| 419 | + } = useAuth0(); |
| 420 | + |
| 421 | + const [posts, setPosts] = useState(null); |
| 422 | + |
| 423 | + useEffect(() => { |
| 424 | + (async () => { |
| 425 | + // Define an identifier that the SDK will use to reference the nonces. |
| 426 | + const nonceId = 'my_api_request'; |
| 427 | + |
| 428 | + // Get an access token as usual. |
| 429 | + const accessToken = await getAccessTokenSilently(); |
| 430 | + |
| 431 | + // Get the current DPoP nonce (if any) and do the request with it. |
| 432 | + const nonce = await getDpopNonce(nonceId); |
| 433 | + |
| 434 | + const response = await fetchWithDpop({ |
| 435 | + url: 'https://api.example.com/posts', |
| 436 | + method: 'GET', |
| 437 | + accessToken, |
| 438 | + nonce, |
| 439 | + }); |
| 440 | + |
| 441 | + setPosts(await response.json()); |
| 442 | + |
| 443 | + async function fetchWithDpop({ |
| 444 | + url, |
| 445 | + method, |
| 446 | + body, |
| 447 | + accessToken, |
| 448 | + nonce, |
| 449 | + isDpopNonceRetry, |
| 450 | + }) { |
| 451 | + const headers = { |
| 452 | + // A DPoP access token has the type `DPoP` and not `Bearer`. |
| 453 | + Authorization: `DPoP ${accessToken}`, |
| 454 | + |
| 455 | + // Include the DPoP proof, which is cryptographic evidence that we |
| 456 | + // are in possession of the same key that was used to get the token. |
| 457 | + DPoP: await generateDpopProof({ url, method, nonce, accessToken }), |
| 458 | + }; |
| 459 | + |
| 460 | + // Make the request. |
| 461 | + const response = await fetch(url, { method, headers, body }); |
| 462 | + |
| 463 | + // If there was a nonce in the response, save it. |
| 464 | + const newNonce = response.headers.get('dpop-nonce'); |
| 465 | + |
| 466 | + if (newNonce) { |
| 467 | + setDpopNonce(newNonce, nonceId); |
| 468 | + } |
| 469 | + |
| 470 | + // If the server rejects the DPoP nonce but it provides a new one, try |
| 471 | + // the request one last time with the correct nonce. |
| 472 | + if ( |
| 473 | + response.status === 401 && |
| 474 | + response.headers.get('www-authenticate')?.includes('use_dpop_nonce') |
| 475 | + ) { |
| 476 | + if (isDpopNonceRetry) { |
| 477 | + throw new Error('DPoP nonce was rejected twice, giving up'); |
| 478 | + } |
| 479 | + |
| 480 | + return fetchWithDpop({ |
| 481 | + ...params, |
| 482 | + nonce: newNonce ?? nonce, |
| 483 | + isDpopNonceRetry: true, |
| 484 | + }); |
| 485 | + } |
| 486 | + |
| 487 | + return response; |
| 488 | + } |
| 489 | + })(); |
| 490 | + }, []); |
| 491 | + |
| 492 | + if (!posts) { |
| 493 | + return <div>Loading...</div>; |
| 494 | + } |
| 495 | + |
| 496 | + return ( |
| 497 | + <ul> |
| 498 | + {posts.map((post, index) => { |
| 499 | + return <li key={index}>{post}</li>; |
| 500 | + })} |
| 501 | + </ul> |
| 502 | + ); |
| 503 | +}; |
| 504 | + |
| 505 | +export default Posts; |
| 506 | +``` |
0 commit comments