|
19 | 19 | - [Organizations](#organizations) |
20 | 20 | - [Log in to an organization](#log-in-to-an-organization) |
21 | 21 | - [Accept user invitations](#accept-user-invitations) |
| 22 | +- [DPoP (Demonstrating Proof-of-Possession)](#dpop-demonstrating-proof-of-possession) |
| 23 | + - [Enabling DPoP](#enabling-dpop) |
| 24 | + - [Making API calls with DPoP](#making-api-calls-with-dpop) |
| 25 | + - [Handling DPoP token migration](#handling-dpop-token-migration) |
| 26 | + - [Checking token type](#checking-token-type) |
| 27 | + - [Handling nonce errors](#handling-nonce-errors) |
22 | 28 | - [Bot Protection](#bot-protection) |
23 | 29 | - [Domain Switching](#domain-switching) |
24 | 30 | - [Android](#android) |
@@ -441,3 +447,353 @@ If you want to support multiple domains, you would have to pass an array of obje |
441 | 447 | ``` |
442 | 448 |
|
443 | 449 | You can skip sending the `customScheme` property if you do not want to customize it. |
| 450 | + |
| 451 | +## DPoP (Demonstrating Proof-of-Possession) |
| 452 | + |
| 453 | +[DPoP](https://datatracker.ietf.org/doc/html/rfc9449) (Demonstrating Proof-of-Possession) is an OAuth 2.0 extension that cryptographically binds access and refresh tokens to a client-specific key pair. This prevents token theft and replay attacks by ensuring that even if a token is intercepted, it cannot be used from a different device. |
| 454 | + |
| 455 | +> **Note**: This feature is currently available in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). Please reach out to Auth0 support to get it enabled for your tenant. |
| 456 | +
|
| 457 | +### Enabling DPoP |
| 458 | + |
| 459 | +DPoP is enabled by default (`useDPoP: true`) when you initialize the Auth0 client: |
| 460 | + |
| 461 | +```js |
| 462 | +import Auth0 from 'react-native-auth0'; |
| 463 | + |
| 464 | +// DPoP is enabled by default |
| 465 | +const auth0 = new Auth0({ |
| 466 | + domain: 'YOUR_AUTH0_DOMAIN', |
| 467 | + clientId: 'YOUR_AUTH0_CLIENT_ID', |
| 468 | +}); |
| 469 | + |
| 470 | +// Or explicitly enable it |
| 471 | +const auth0 = new Auth0({ |
| 472 | + domain: 'YOUR_AUTH0_DOMAIN', |
| 473 | + clientId: 'YOUR_AUTH0_CLIENT_ID', |
| 474 | + useDPoP: true, // Explicitly enable DPoP |
| 475 | +}); |
| 476 | +``` |
| 477 | + |
| 478 | +**Using Auth0Provider (React Hooks):** |
| 479 | + |
| 480 | +```js |
| 481 | +import { Auth0Provider } from 'react-native-auth0'; |
| 482 | + |
| 483 | +function App() { |
| 484 | + return ( |
| 485 | + <Auth0Provider |
| 486 | + domain="YOUR_AUTH0_DOMAIN" |
| 487 | + clientId="YOUR_AUTH0_CLIENT_ID" |
| 488 | + // DPoP is enabled by default |
| 489 | + > |
| 490 | + {/* Your app components */} |
| 491 | + </Auth0Provider> |
| 492 | + ); |
| 493 | +} |
| 494 | +``` |
| 495 | + |
| 496 | +> **Important**: DPoP will only be used for **new user sessions** created after enabling it. Existing sessions with Bearer tokens will continue to work until the user logs in again. See [Handling DPoP token migration](#handling-dpop-token-migration) for how to handle this transition. |
| 497 | +
|
| 498 | +### Making API calls with DPoP |
| 499 | + |
| 500 | +When calling your own APIs with DPoP-bound tokens, you need to include both the `Authorization` header and the `DPoP` proof header. The SDK provides a `getDPoPHeaders()` method to generate these headers: |
| 501 | + |
| 502 | +```js |
| 503 | +import Auth0 from 'react-native-auth0'; |
| 504 | + |
| 505 | +const auth0 = new Auth0({ |
| 506 | + domain: 'YOUR_AUTH0_DOMAIN', |
| 507 | + clientId: 'YOUR_AUTH0_CLIENT_ID', |
| 508 | + useDPoP: true, |
| 509 | +}); |
| 510 | + |
| 511 | +async function callApi() { |
| 512 | + try { |
| 513 | + // Get credentials |
| 514 | + const credentials = await auth0.credentialsManager.getCredentials(); |
| 515 | + |
| 516 | + // Generate DPoP headers for your API request |
| 517 | + const headers = await auth0.getDPoPHeaders({ |
| 518 | + url: 'https://api.example.com/data', |
| 519 | + method: 'GET', |
| 520 | + accessToken: credentials.accessToken, |
| 521 | + tokenType: credentials.tokenType, |
| 522 | + }); |
| 523 | + |
| 524 | + // Make the API call with the headers |
| 525 | + const response = await fetch('https://api.example.com/data', { |
| 526 | + method: 'GET', |
| 527 | + headers: { |
| 528 | + ...headers, |
| 529 | + 'Content-Type': 'application/json', |
| 530 | + }, |
| 531 | + }); |
| 532 | + |
| 533 | + const data = await response.json(); |
| 534 | + console.log(data); |
| 535 | + } catch (error) { |
| 536 | + console.error('API call failed:', error); |
| 537 | + } |
| 538 | +} |
| 539 | +``` |
| 540 | + |
| 541 | +**Using React Hooks:** |
| 542 | + |
| 543 | +```js |
| 544 | +import { useAuth0 } from 'react-native-auth0'; |
| 545 | + |
| 546 | +function MyComponent() { |
| 547 | + const { getCredentials, getDPoPHeaders } = useAuth0(); |
| 548 | + |
| 549 | + const callApi = async () => { |
| 550 | + try { |
| 551 | + const credentials = await getCredentials(); |
| 552 | + |
| 553 | + const headers = await getDPoPHeaders({ |
| 554 | + url: 'https://api.example.com/data', |
| 555 | + method: 'POST', |
| 556 | + accessToken: credentials.accessToken, |
| 557 | + tokenType: credentials.tokenType, |
| 558 | + }); |
| 559 | + |
| 560 | + const response = await fetch('https://api.example.com/data', { |
| 561 | + method: 'POST', |
| 562 | + headers: { |
| 563 | + ...headers, |
| 564 | + 'Content-Type': 'application/json', |
| 565 | + }, |
| 566 | + body: JSON.stringify({ message: 'Hello' }), |
| 567 | + }); |
| 568 | + |
| 569 | + return await response.json(); |
| 570 | + } catch (error) { |
| 571 | + console.error('API call failed:', error); |
| 572 | + } |
| 573 | + }; |
| 574 | + |
| 575 | + return <Button title="Call API" onPress={callApi} />; |
| 576 | +} |
| 577 | +``` |
| 578 | + |
| 579 | +### Handling DPoP token migration |
| 580 | + |
| 581 | +When you enable DPoP in your app, existing users will still have Bearer tokens until they log in again. You should implement logic to detect old tokens and prompt users to re-authenticate: |
| 582 | + |
| 583 | +```js |
| 584 | +import Auth0 from 'react-native-auth0'; |
| 585 | + |
| 586 | +const auth0 = new Auth0({ |
| 587 | + domain: 'YOUR_AUTH0_DOMAIN', |
| 588 | + clientId: 'YOUR_AUTH0_CLIENT_ID', |
| 589 | + useDPoP: true, |
| 590 | +}); |
| 591 | + |
| 592 | +async function ensureDPoPTokens() { |
| 593 | + try { |
| 594 | + // Check if user has credentials |
| 595 | + const hasCredentials = await auth0.credentialsManager.hasValidCredentials(); |
| 596 | + |
| 597 | + if (!hasCredentials) { |
| 598 | + // No credentials, user needs to log in |
| 599 | + return await auth0.webAuth.authorize(); |
| 600 | + } |
| 601 | + |
| 602 | + // Get existing credentials |
| 603 | + const credentials = await auth0.credentialsManager.getCredentials(); |
| 604 | + |
| 605 | + // Check if the token is DPoP |
| 606 | + if (credentials.tokenType !== 'DPoP') { |
| 607 | + console.log( |
| 608 | + 'User has old Bearer token, clearing and re-authenticating...' |
| 609 | + ); |
| 610 | + |
| 611 | + // Clear old credentials |
| 612 | + await auth0.credentialsManager.clearCredentials(); |
| 613 | + |
| 614 | + // Prompt user to log in again with DPoP |
| 615 | + return await auth0.webAuth.authorize(); |
| 616 | + } |
| 617 | + |
| 618 | + console.log('User already has DPoP token'); |
| 619 | + return credentials; |
| 620 | + } catch (error) { |
| 621 | + console.error('Token migration failed:', error); |
| 622 | + throw error; |
| 623 | + } |
| 624 | +} |
| 625 | + |
| 626 | +// Call this when your app starts or when accessing protected resources |
| 627 | +ensureDPoPTokens() |
| 628 | + .then((credentials) => console.log('Ready with DPoP tokens:', credentials)) |
| 629 | + .catch((error) => console.error('Failed to ensure DPoP tokens:', error)); |
| 630 | +``` |
| 631 | + |
| 632 | +**Using React Hooks:** |
| 633 | + |
| 634 | +```js |
| 635 | +import { useAuth0 } from 'react-native-auth0'; |
| 636 | +import { useEffect, useState } from 'react'; |
| 637 | + |
| 638 | +function App() { |
| 639 | + const { authorize, getCredentials, clearSession, hasValidCredentials } = |
| 640 | + useAuth0(); |
| 641 | + const [isReady, setIsReady] = useState(false); |
| 642 | + const [needsMigration, setNeedsMigration] = useState(false); |
| 643 | + |
| 644 | + useEffect(() => { |
| 645 | + checkAndMigrateToDPoP(); |
| 646 | + }, []); |
| 647 | + |
| 648 | + const checkAndMigrateToDPoP = async () => { |
| 649 | + try { |
| 650 | + const hasValid = await hasValidCredentials(); |
| 651 | + |
| 652 | + if (!hasValid) { |
| 653 | + setIsReady(true); |
| 654 | + return; |
| 655 | + } |
| 656 | + |
| 657 | + const credentials = await getCredentials(); |
| 658 | + |
| 659 | + if (credentials.tokenType !== 'DPoP') { |
| 660 | + setNeedsMigration(true); |
| 661 | + // Optionally auto-clear or wait for user action |
| 662 | + // await clearSession(); |
| 663 | + // await authorize(); |
| 664 | + } |
| 665 | + |
| 666 | + setIsReady(true); |
| 667 | + } catch (error) { |
| 668 | + console.error('Migration check failed:', error); |
| 669 | + setIsReady(true); |
| 670 | + } |
| 671 | + }; |
| 672 | + |
| 673 | + const handleMigration = async () => { |
| 674 | + try { |
| 675 | + await clearSession(); |
| 676 | + await authorize(); |
| 677 | + setNeedsMigration(false); |
| 678 | + } catch (error) { |
| 679 | + console.error('Migration failed:', error); |
| 680 | + } |
| 681 | + }; |
| 682 | + |
| 683 | + if (!isReady) { |
| 684 | + return <LoadingScreen />; |
| 685 | + } |
| 686 | + |
| 687 | + if (needsMigration) { |
| 688 | + return ( |
| 689 | + <View> |
| 690 | + <Text>Security Update Required</Text> |
| 691 | + <Text> |
| 692 | + Please log in again to enhance your account security with DPoP. |
| 693 | + </Text> |
| 694 | + <Button title="Log In Again" onPress={handleMigration} /> |
| 695 | + </View> |
| 696 | + ); |
| 697 | + } |
| 698 | + |
| 699 | + return <YourApp />; |
| 700 | +} |
| 701 | +``` |
| 702 | + |
| 703 | +### Checking token type |
| 704 | + |
| 705 | +You can check whether credentials use DPoP or Bearer tokens: |
| 706 | + |
| 707 | +```js |
| 708 | +const credentials = await auth0.credentialsManager.getCredentials(); |
| 709 | + |
| 710 | +if (credentials.tokenType === 'DPoP') { |
| 711 | + console.log('Using DPoP token - enhanced security enabled'); |
| 712 | + |
| 713 | + // Generate DPoP headers for API calls |
| 714 | + const headers = await auth0.getDPoPHeaders({ |
| 715 | + url: 'https://api.example.com/data', |
| 716 | + method: 'GET', |
| 717 | + accessToken: credentials.accessToken, |
| 718 | + tokenType: credentials.tokenType, |
| 719 | + }); |
| 720 | +} else { |
| 721 | + console.log('Using Bearer token - consider migrating to DPoP'); |
| 722 | + |
| 723 | + // Standard Bearer authorization |
| 724 | + const headers = { |
| 725 | + Authorization: `Bearer ${credentials.accessToken}`, |
| 726 | + }; |
| 727 | +} |
| 728 | +``` |
| 729 | + |
| 730 | +### Handling nonce errors |
| 731 | + |
| 732 | +Some APIs may require DPoP nonces to prevent replay attacks. If your API responds with a `use_dpop_nonce` error, you can retry the request with the nonce: |
| 733 | + |
| 734 | +```js |
| 735 | +async function callApiWithNonce(url, method, credentials, retryCount = 0) { |
| 736 | + try { |
| 737 | + // Generate headers (initially without nonce) |
| 738 | + const headers = await auth0.getDPoPHeaders({ |
| 739 | + url, |
| 740 | + method, |
| 741 | + accessToken: credentials.accessToken, |
| 742 | + tokenType: credentials.tokenType, |
| 743 | + }); |
| 744 | + |
| 745 | + const response = await fetch(url, { |
| 746 | + method, |
| 747 | + headers: { |
| 748 | + ...headers, |
| 749 | + 'Content-Type': 'application/json', |
| 750 | + }, |
| 751 | + }); |
| 752 | + |
| 753 | + // Check if nonce is required |
| 754 | + if (response.status === 401 && retryCount === 0) { |
| 755 | + const authHeader = response.headers.get('WWW-Authenticate'); |
| 756 | + |
| 757 | + if (authHeader && authHeader.includes('use_dpop_nonce')) { |
| 758 | + // Extract nonce from response |
| 759 | + const nonce = response.headers.get('DPoP-Nonce'); |
| 760 | + |
| 761 | + if (nonce) { |
| 762 | + console.log('Retrying with DPoP nonce...'); |
| 763 | + |
| 764 | + // Retry with nonce |
| 765 | + const headersWithNonce = await auth0.getDPoPHeaders({ |
| 766 | + url, |
| 767 | + method, |
| 768 | + accessToken: credentials.accessToken, |
| 769 | + tokenType: credentials.tokenType, |
| 770 | + nonce, |
| 771 | + }); |
| 772 | + |
| 773 | + return await fetch(url, { |
| 774 | + method, |
| 775 | + headers: { |
| 776 | + ...headersWithNonce, |
| 777 | + 'Content-Type': 'application/json', |
| 778 | + }, |
| 779 | + }); |
| 780 | + } |
| 781 | + } |
| 782 | + } |
| 783 | + |
| 784 | + return response; |
| 785 | + } catch (error) { |
| 786 | + console.error('API call with nonce failed:', error); |
| 787 | + throw error; |
| 788 | + } |
| 789 | +} |
| 790 | + |
| 791 | +// Usage |
| 792 | +const credentials = await auth0.credentialsManager.getCredentials(); |
| 793 | +const response = await callApiWithNonce( |
| 794 | + 'https://api.example.com/data', |
| 795 | + 'GET', |
| 796 | + credentials |
| 797 | +); |
| 798 | +const data = await response.json(); |
| 799 | +``` |
0 commit comments