Skip to content

Commit 70c280e

Browse files
feat: add support for Demonstration of Proof-of-Possession(DPoP) (#1345)
1 parent 6b06f1b commit 70c280e

32 files changed

+2636
-44
lines changed

A0Auth0.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
1616
s.source_files = 'ios/**/*.{h,m,mm,swift}'
1717
s.requires_arc = true
1818

19-
s.dependency 'Auth0', '2.13'
19+
s.dependency 'Auth0', '2.14'
2020

2121
install_modules_dependencies(s)
2222
end

EXAMPLES.md

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
- [Organizations](#organizations)
2020
- [Log in to an organization](#log-in-to-an-organization)
2121
- [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)
2228
- [Bot Protection](#bot-protection)
2329
- [Domain Switching](#domain-switching)
2430
- [Android](#android)
@@ -441,3 +447,353 @@ If you want to support multiple domains, you would have to pass an array of obje
441447
```
442448

443449
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

Comments
 (0)