Skip to content

Commit 99bfca9

Browse files
Merge branch 'main' into fix-typescript-4.6.3-compilation
2 parents d5c7dbe + 443d0fd commit 99bfca9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+3390
-2350
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,6 @@ jobs:
3737
with:
3838
node: ${{ env.NODE_VERSION }}
3939

40-
- name: Save build artifacts
41-
uses: actions/cache/save@v4
42-
with:
43-
path: .
44-
key: ${{ env.CACHE_KEY }}
45-
4640
unit:
4741
needs: build # Require build to complete before running tests
4842

@@ -59,11 +53,8 @@ jobs:
5953
node-version: ${{ env.NODE_VERSION }}
6054
cache: npm
6155

62-
- name: Restore build artifacts
63-
uses: actions/cache/restore@v4
64-
with:
65-
path: .
66-
key: ${{ env.CACHE_KEY }}
56+
- name: Install dependencies
57+
run: npm ci --include=dev
6758

6859
- name: Run tests
6960
run: npm run test

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v2.3.0
1+
v2.5.0

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Change Log
22

3+
## [v2.5.0](https://github.com/auth0/auth0-react/tree/v2.5.0) (2025-09-17)
4+
[Full Changelog](https://github.com/auth0/auth0-react/compare/v2.4.0...v2.5.0)
5+
6+
**Added**
7+
- Add support for DPoP [\#869](https://github.com/auth0/auth0-react/pull/869) ([martinml](https://github.com/martinml))
8+
9+
## [v2.4.0](https://github.com/auth0/auth0-react/tree/v2.4.0) (2025-07-22)
10+
[Full Changelog](https://github.com/auth0/auth0-react/compare/v2.3.0...v2.4.0)
11+
12+
**Added**
13+
- Bump @auth0/auth0-spa-js from 2.2.0 to 2.3.0 [\#858](https://github.com/auth0/auth0-react/pull/858) ([dependabot[bot]](https://github.com/apps/dependabot))
14+
15+
**Fixed**
16+
- Enhance type safety in Auth0Provider and reducer by introducing generic user type [\#842](https://github.com/auth0/auth0-react/pull/842) ([gyaneshgouraw-okta](https://github.com/gyaneshgouraw-okta))
17+
318
## [v2.3.0](https://github.com/auth0/auth0-react/tree/v2.3.0) (2025-01-21)
419
[Full Changelog](https://github.com/auth0/auth0-react/compare/v2.2.4...v2.3.0)
520

EXAMPLES.md

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- [Protecting a route in a Next.js app (in SPA mode)](#protecting-a-route-in-a-nextjs-app-in-spa-mode)
99
- [Use with Auth0 organizations](#use-with-auth0-organizations)
1010
- [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)
1112

1213
## Use with a Class Component
1314

@@ -339,3 +340,234 @@ const Page = withAuthenticationRequired(
339340
withClaimCheck(Component, checkClaims, '/missing-roles' )
340341
);
341342
```
343+
344+
## Device-bound tokens with DPoP
345+
346+
**Demonstrating Proof-of-Possession** —or simply **DPoP**— is a recent 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 using cryptographic signatures. Without it, **a token leak caused by XSS or other vulnerabilities could allow an attacker to impersonate the real user.**
349+
350+
To support DPoP in `auth0-react`, some APIs available in modern browsers are required:
351+
352+
- [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.
353+
354+
- [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).
355+
356+
The following OAuth 2.0 flows are currently supported by `auth0-react`:
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+
> [!IMPORTANT]
365+
> Currently, only the `ES256` algorithm is supported.
366+
367+
### Enabling DPoP
368+
369+
DPoP is disabled by default. To enable it, set the `useDpop` option to `true` when invoking the provider. For example:
370+
371+
```jsx
372+
<Auth0Provider
373+
domain="YOUR_AUTH0_DOMAIN"
374+
clientId="YOUR_AUTH0_CLIENT_ID"
375+
useDpop={true} // 👈
376+
authorizationParams={{ redirect_uri: window.location.origin }}
377+
>
378+
```
379+
380+
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**.
381+
382+
> [!IMPORTANT]
383+
> 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.
384+
>
385+
> You decide how to handle this transition. For example, you might require users to log in again the next time they use your application.
386+
387+
> [!NOTE]
388+
> Using DPoP requires storing some temporary data in the user's browser. When you log the user out with `logout()`, this data is deleted.
389+
390+
> [!TIP]
391+
> 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.
392+
393+
### Using DPoP in your own requests
394+
395+
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>`.
396+
397+
To determine the type of a token, use the `detailedResponse` option in `getAccessTokenSilently()` to access the `token_type` property, which will be either `DPoP` or `Bearer`.
398+
399+
For internal requests sent by `auth0-react` to Auth0, simply enable the `useDpop` option and **every interaction with Auth0 will be protected**.
400+
401+
However, **to use DPoP with a custom, external API, some additional work is required**. The `useAuth()` hook provides some low-level methods to help with this:
402+
403+
- `getDpopNonce()`
404+
- `setDpopNonce()`
405+
- `generateDpopProof()`
406+
407+
However, due to the nature of how DPoP works, **this is not a trivial task**:
408+
409+
- When a nonce is missing or expired, the request may need to be retried.
410+
- Received nonces must be stored and managed.
411+
- DPoP headers must be generated and included in every request, and regenerated for retries.
412+
413+
Because of this, we recommend using the provided `fetchWithAuth()` method, which **handles all of this for you**.
414+
415+
#### Simple usage
416+
417+
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.
418+
419+
For example, if you had this code:
420+
421+
```js
422+
await fetch('https://api.example.com/foo', {
423+
method: 'GET',
424+
headers: { 'user-agent': 'My Client 1.0' }
425+
});
426+
427+
console.log(response.status);
428+
console.log(response.headers);
429+
console.log(await response.json());
430+
```
431+
432+
You would change it as follows:
433+
434+
```js
435+
const { createFetcher } = useAuth0();
436+
437+
const fetcher = createFetcher({
438+
dpopNonceId: 'my_api_request'
439+
});
440+
441+
await fetcher.fetchWithAuth('https://api.example.com/foo', {
442+
method: 'GET',
443+
headers: { 'user-agent': 'My Client 1.0' }
444+
});
445+
446+
console.log(response.status);
447+
console.log(response.headers);
448+
console.log(await response.json());
449+
```
450+
451+
When using `fetchWithAuth()`, the following will be handled for you automatically:
452+
453+
- Use `getAccessTokenSilently()` to get the access token to inject in the headers.
454+
- Generate and inject DPoP headers when needed.
455+
- Store and update any DPoP nonces.
456+
- Handle retries caused by a rejected nonce.
457+
458+
> [!IMPORTANT]
459+
> If DPoP is enabled in the provider, a `dpopNonceId` **must** be present in the `createFetcher()` parameters, since it’s used to keep track of the DPoP nonces for each request.
460+
461+
#### Advanced usage
462+
463+
If you need something more complex than the example above, you can provide a custom implementation in the `fetch` property.
464+
465+
However, since `auth0-react` needs to make decisions based on HTTP responses, your implementation **must return an object with _at least_ two properties**:
466+
467+
1. `status`: the response status code as a number.
468+
2. `headers`: the response headers as a plain object or as a Fetch API’s Headers-like interface.
469+
470+
Whatever it returns, it will be passed as the output of the `fetchWithAuth()` method.
471+
472+
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.
473+
474+
##### Example with `axios`
475+
476+
```js
477+
const { createFetcher } = useAuth0();
478+
479+
const fetcher = createFetcher({
480+
dpopNonceId: 'my_api_request',
481+
fetch: (request) =>
482+
// The `Request` object has everything you need to do a request in a
483+
// different library. Make sure that your output meets the requirements
484+
// about the `status` and `headers` properties.
485+
axios.request({
486+
url: request.url,
487+
method: request.method,
488+
data: request.body,
489+
headers: Object.fromEntries(request.headers),
490+
timeout: 2000,
491+
// etc.
492+
}),
493+
},
494+
});
495+
496+
const response = await fetcher.fetchWithAuth('https://api.example.com/foo', {
497+
method: 'POST',
498+
body: JSON.stringify({ name: 'John Doe' }),
499+
headers: { 'user-agent': 'My Client 1.0' },
500+
});
501+
502+
console.log(response.status);
503+
console.log(response.headers);
504+
console.log(response.data);
505+
```
506+
507+
##### Timeouts with native `fetch()`
508+
509+
The Fetch API doesn’t support passing a timeout value directly; instead, you’re expected to use an [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). For example:
510+
511+
```js
512+
const { createFetcher } = useAuth0();
513+
514+
const fetcher = createFetcher();
515+
516+
await fetcher.fetchWithAuth('https://api.example.com/foo', {
517+
signal: AbortSignal.timeout(2000)
518+
});
519+
```
520+
521+
This works, but if you define your request parameters statically when your app starts and then call `fetchWithAuth()` after an indeterminate amount of time, you'll find that **the request will timeout immediately**. This happens because the `AbortSignal` **starts counting time as soon as it is created**.
522+
523+
To work around this, you can pass a thin wrapper over the native `fetch()` so that a new `AbortSignal` is created each time a request is made:
524+
525+
```js
526+
const { createFetcher } = useAuth0();
527+
528+
const fetcher = createFetcher({
529+
fetch: (request) => fetch(request, { signal: AbortSignal.timeout(2000) })
530+
});
531+
532+
await fetcher.fetchWithAuth('https://api.example.com/foo');
533+
```
534+
535+
##### Having a base URL
536+
537+
If you need to make requests to different endpoints of the same API, passing a `baseUrl` to `createFetcher()` can be useful:
538+
539+
```js
540+
const { createFetcher } = useAuth0();
541+
542+
const fetcher = createFetcher({
543+
baseUrl: 'https://api.example.com'
544+
});
545+
546+
await fetcher.fetchWithAuth('/foo'); // => https://api.example.com/foo
547+
await fetcher.fetchWithAuth('/bar'); // => https://api.example.com/bar
548+
await fetcher.fetchWithAuth('/xyz'); // => https://api.example.com/xyz
549+
550+
// If the passed URL is absolute, `baseUrl` will be ignored for convenience:
551+
await fetcher.fetchWithAuth('https://other-api.example.com/foo');
552+
```
553+
554+
##### Passing an access token
555+
556+
The `fetchWithAuth()` method assumes you’re using the SDK to get the access token for the request. This means that by default, it will always call `getAccessTokenSilently()` internally before making the request.
557+
558+
However, if you already have an access token or need to pass specific parameters to `getAccessTokenSilently()`, you can override this behavior with a custom access token factory, like so:
559+
560+
```js
561+
const { createFetcher, getAccessTokenSilently } = useAuth0();
562+
563+
createFetcher({
564+
getAccessToken: () =>
565+
getAccessTokenSilently({
566+
authorizationParams: {
567+
audience: '<SOME_AUDIENCE>',
568+
scope: '<SOME_SCOPE>'
569+
// etc.
570+
}
571+
})
572+
});
573+
```

FAQ.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
1. [User is not logged in after page refresh](#1-user-is-not-logged-in-after-page-refresh)
66
2. [User is not logged in after successful sign in with redirect](#2-user-is-not-logged-in-after-successful-sign-in-with-redirect)
7+
3. [Skip the Auth0 login page](#3-skip-the-auth0-login-page)
78

89
## 1. User is not logged in after page refresh
910

@@ -20,3 +21,33 @@ In this case Silent Authentication will not work because it relies on a hidden i
2021
## 2. User is not logged in after successful sign in with redirect
2122

2223
If after successfully logging in, your user returns to your SPA and is still not authenticated, do _not_ refresh the page - go to the Network tab on Chrome and confirm that the POST to `oauth/token` resulted in an error `401 Unauthorized`. If this is the case, your tenant is most likely misconfigured. Go to your **Application Properties** in your application's settings in the [Auth0 Dashboard](https://manage.auth0.com) and make sure that `Application Type` is set to `Single Page Application` and `Token Endpoint Authentication Method` is set to `None` (**Note:** there is a known issue with the Auth0 "Default App", if you are unable to set `Token Endpoint Authentication Method` to `None`, create a new Application of type `Single Page Application` or see the advice in [issues/93](https://github.com/auth0/auth0-react/issues/93#issuecomment-673431605))
24+
25+
## 3. Skip the Auth0 login page
26+
27+
When integrating with third party providers such as Google or Microsoft, being redirected to Auth0 before being redirected to the corresponding provider can be sub-optimal in terms of user-experience.
28+
If you only have a single connection enabled, or you know up front how the user wants to authenticate, you can set the `connection` parameter when calling `loginWithRedirect()` or `loginWithPopup()`:
29+
30+
```js
31+
loginWithRedirect({
32+
// ...
33+
authorizationParams: {
34+
connection: 'connection_logical_identifier'
35+
}
36+
})
37+
```
38+
39+
Doing so for connections such as Google or Microsoft, would automatically redirect you to them instead of showing the Auth0 login page first.
40+
41+
Additionally, if you are using `withAuthenticationRequired`, you may want it to pick up the same connection when it would redirect for login. To do so, you should provide the `connection` property when configuring `withAuthenticationRequired`:
42+
43+
```js
44+
withAuthenticationRequired(Component, {
45+
loginOptions: {
46+
authorizationParams: {
47+
connection: 'connection_logical_identifier'
48+
}
49+
}
50+
})
51+
```
52+
53+
ℹ️ You can find the connection's logical identifier as the **connection name** in the connection settings in the Auth0 dashboard for your tenant.

__mocks__/@auth0/auth0-spa-js.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ const isAuthenticated = jest.fn(() => false);
1010
const loginWithPopup = jest.fn();
1111
const loginWithRedirect = jest.fn();
1212
const logout = jest.fn();
13+
const getDpopNonce = jest.fn();
14+
const setDpopNonce = jest.fn();
15+
const generateDpopProof = jest.fn();
16+
const createFetcher = jest.fn();
1317

1418
export const Auth0Client = jest.fn(() => {
1519
return {
@@ -25,5 +29,9 @@ export const Auth0Client = jest.fn(() => {
2529
loginWithPopup,
2630
loginWithRedirect,
2731
logout,
32+
getDpopNonce,
33+
setDpopNonce,
34+
generateDpopProof,
35+
createFetcher,
2836
};
2937
});

0 commit comments

Comments
 (0)