Skip to content

Commit 070c31e

Browse files
Merge branch 'main' into dependabot/github_actions/actions/setup-node-6
2 parents 827fb40 + 7a652a6 commit 070c31e

Some content is hidden

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

62 files changed

+603
-187
lines changed

.github/workflows/codeql.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ jobs:
4040
uses: actions/checkout@v5
4141

4242
- name: Initialize CodeQL
43-
uses: github/codeql-action/init@v3
43+
uses: github/codeql-action/init@v4
4444
with:
4545
languages: ${{ matrix.language }}
4646
queries: +security-and-quality
4747

4848
- name: Autobuild
49-
uses: github/codeql-action/autobuild@v3
49+
uses: github/codeql-action/autobuild@v4
5050

5151
- name: Perform CodeQL Analysis
52-
uses: github/codeql-action/analyze@v3
52+
uses: github/codeql-action/analyze@v4
5353
with:
5454
category: '/language:${{ matrix.language }}'

.github/workflows/test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,6 @@ jobs:
6060
run: npm run test
6161

6262
- name: Upload coverage
63-
uses: codecov/codecov-action@4fe8c5f003fae66aa5ebb77cfd3e7bfbbda0b6b0 # [email protected]
63+
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # [email protected]
64+
with:
65+
token: ${{ secrets.CODECOV_TOKEN }}

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,4 @@ test-results
108108

109109
cypress/screenshots
110110
cypress/videos
111-
.npmrc
111+
.npmrc

.version

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

CHANGELOG.md

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

3+
## [v2.8.0](https://github.com/auth0/auth0-react/tree/v2.8.0) (2025-10-17)
4+
[Full Changelog](https://github.com/auth0/auth0-react/compare/v2.7.0...v2.8.0)
5+
6+
**Added**
7+
- Bump auth0-spa-js for connected account updates [\#923](https://github.com/auth0/auth0-react/pull/923) ([adamjmcgrath](https://github.com/adamjmcgrath))
8+
9+
## [v2.7.0](https://github.com/auth0/auth0-react/tree/v2.7.0) (2025-10-15)
10+
[Full Changelog](https://github.com/auth0/auth0-react/compare/v2.6.0...v2.7.0)
11+
12+
**Added**
13+
- Add support for connected accounts [\#912](https://github.com/auth0/auth0-react/pull/912) ([adamjmcgrath](https://github.com/adamjmcgrath))
14+
315
## [v2.6.0](https://github.com/auth0/auth0-react/tree/v2.6.0) (2025-10-06)
416
[Full Changelog](https://github.com/auth0/auth0-react/compare/v2.5.0...v2.6.0)
517

EXAMPLES.md

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
- [Use with Auth0 organizations](#use-with-auth0-organizations)
1010
- [Protecting a route with a claims check](#protecting-a-route-with-a-claims-check)
1111
- [Device-bound tokens with DPoP](#device-bound-tokens-with-dpop)
12-
- [Using Multi Resource Refresh Tokens]()
12+
- [Using Multi Resource Refresh Tokens](#using-multi-resource-refresh-tokens)
13+
- [Connect Accounts for using Token Vault](#connect-accounts-for-using-token-vault)
1314

1415
## Use with a Class Component
1516

@@ -597,3 +598,88 @@ MRRT is disabled by default. To enable it, set the `useMrrt` option to `true` wh
597598
> [!IMPORTANT]
598599
> In order MRRT to work, it needs a previous configuration setting the refresh token policies.
599600
> Visit [configure and implement MRRT.](https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token/configure-and-implement-multi-resource-refresh-token)
601+
602+
## Connect Accounts for using Token Vault
603+
604+
The Connect Accounts feature uses the Auth0 My Account API to allow users to link multiple third party accounts to a single Auth0 user profile.
605+
606+
When using Connected Accounts, Auth0 acquires tokens from upstream Identity Providers (like Google) and stores them in a secure [Token Vault](https://auth0.com/docs/secure/tokens/token-vault). These tokens can then be used to access third-party APIs (like Google Calendar) on behalf of the user.
607+
608+
The tokens in the Token Vault are then accessible to [Resource Servers](https://auth0.com/docs/get-started/apis) (APIs) configured in Auth0. The SPA application can then issue requests to the API, which can retrieve the tokens from the Token Vault and use them to access the third-party APIs.
609+
610+
This is particularly useful for applications that require access to different resources on behalf of a user, like AI Agents.
611+
612+
### Configure the SDK
613+
614+
The SDK must be configured with an audience (an API Identifier) - this will be the resource server that uses the tokens from the Token Vault.
615+
616+
The SDK must also be configured to use refresh tokens and MRRT ([Multiple Resource Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token)) since we will use the refresh token grant to get Access Tokens for the My Account API in addition to the API we are calling.
617+
618+
The My Account API requires DPoP tokens, so we also need to enable DPoP.
619+
620+
```jsx
621+
<Auth0Provider
622+
domain="YOUR_AUTH0_DOMAIN"
623+
clientId="YOUR_AUTH0_CLIENT_ID"
624+
authorizationParams={{
625+
redirect_uri: window.location.origin,
626+
audience: '<AUTH0 API IDENTIFIER>' // The API that will use the tokens from the Token Vault
627+
}}
628+
useRefreshTokens={true}
629+
useMrrt={true}
630+
useDpop={true}
631+
>
632+
<App />
633+
</Auth0Provider>
634+
```
635+
636+
### Login to the application
637+
638+
Use the login methods to authenticate to the application and get a refresh and access token for the API.
639+
640+
```jsx
641+
const Login = () => {
642+
const { loginWithRedirect } = useAuth0();
643+
return <button onClick={() => loginWithRedirect({
644+
authorizationParams: {
645+
audience: '<AUTH0 API IDENTIFIER>', // The API that will use the tokens from the Token Vault
646+
scope: 'openid profile email offline_access read:calendar' // Make sure you get a Refresh Token as you're using MRRT to get access to the My Account API
647+
}
648+
})}>Login</button>;
649+
};
650+
```
651+
652+
### Connect to a third party account
653+
654+
Use the new `connectAccountWithRedirect` method to redirect the user to the third party Identity Provider to connect their account.
655+
656+
```jsx
657+
const ConnectAccount = () => {
658+
const { connectAccountWithRedirect } = useAuth0();
659+
return <button onClick={() => connectAccountWithRedirect({
660+
connection: '<CONNECTION eg, google-apps-connection>',
661+
access_type: 'offline', // You must also request a refresh token from the third party Identity Provider for it to be stored in Token Vault.
662+
authorization_params: {
663+
scope: '<SCOPE eg https://www.googleapis.com/auth/calendar.acls.readonly>'
664+
}
665+
})}>Connect Google Calendar</button>;
666+
};
667+
```
668+
669+
When the redirect completes, the user will be returned to the application and the tokens from the third party Identity Provider will be stored in the Token Vault.
670+
671+
```jsx
672+
<Auth0Provider
673+
// ...
674+
onRedirectCallback={(appState) => {
675+
if (appState.connectedAccount) {
676+
console.log(`You've connected to ${appState.connectedAccount.connection}`);
677+
}
678+
window.history.replaceState({}, document.title, '/');
679+
}}
680+
>
681+
<App />
682+
</Auth0Provider>
683+
```
684+
685+
You can now [call the API](#calling-an-api) with your access token and the API can use [Access Token Exchange with Token Vault](https://auth0.com/docs/secure/tokens/token-vault/access-token-exchange-with-token-vault) to get tokens from the Token Vault to access third party APIs on behalf of the user.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
[![npm](https://img.shields.io/npm/v/@auth0/auth0-react.svg?style=flat)](https://www.npmjs.com/package/@auth0/auth0-react)
44
[![codecov](https://img.shields.io/codecov/c/github/auth0/auth0-react/main.svg?style=flat)](https://codecov.io/gh/auth0/auth0-react)
5+
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/auth0/auth0-react)
56
![Downloads](https://img.shields.io/npm/dw/@auth0/auth0-react)
67
[![License](https://img.shields.io/:license-mit-blue.svg?style=flat)](https://opensource.org/licenses/MIT)
78
[![CircleCI](https://img.shields.io/circleci/build/github/auth0/auth0-react.svg?branch=main&style=flat)](https://circleci.com/gh/auth0/auth0-react)
@@ -181,4 +182,4 @@ Please do not report security vulnerabilities on the public GitHub issue tracker
181182
</p>
182183
<p align="center">Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout <a href="https://auth0.com/why-auth0">Why Auth0?</a></p>
183184
<p align="center">
184-
This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-react/blob/main/LICENSE"> LICENSE</a> file for more info.</p>
185+
This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-react/blob/main/LICENSE"> LICENSE</a> file for more info.</p>

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const actual = jest.requireActual('@auth0/auth0-spa-js');
2+
13
const handleRedirectCallback = jest.fn(() => ({ appState: {} }));
24
const buildLogoutUrl = jest.fn();
35
const buildAuthorizeUrl = jest.fn();
@@ -9,6 +11,7 @@ const getIdTokenClaims = jest.fn();
911
const isAuthenticated = jest.fn(() => false);
1012
const loginWithPopup = jest.fn();
1113
const loginWithRedirect = jest.fn();
14+
const connectAccountWithRedirect = jest.fn();
1215
const logout = jest.fn();
1316
const getDpopNonce = jest.fn();
1417
const setDpopNonce = jest.fn();
@@ -28,10 +31,13 @@ export const Auth0Client = jest.fn(() => {
2831
isAuthenticated,
2932
loginWithPopup,
3033
loginWithRedirect,
34+
connectAccountWithRedirect,
3135
logout,
3236
getDpopNonce,
3337
setDpopNonce,
3438
generateDpopProof,
3539
createFetcher,
3640
};
3741
});
42+
43+
export const ResponseType = actual.ResponseType;

__tests__/auth-provider.test.tsx

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
2-
Auth0Client,
2+
Auth0Client, ConnectAccountRedirectResult,
33
GetTokenSilentlyVerboseResponse,
4+
ResponseType
45
} from '@auth0/auth0-spa-js';
56
import '@testing-library/jest-dom';
67
import { act, render, renderHook, screen, waitFor } from '@testing-library/react';
@@ -192,6 +193,7 @@ describe('Auth0Provider', () => {
192193
);
193194
clientMock.handleRedirectCallback.mockResolvedValueOnce({
194195
appState: undefined,
196+
response_type: ResponseType.Code
195197
});
196198
const wrapper = createWrapper();
197199
renderHook(() => useContext(Auth0Context), {
@@ -214,6 +216,7 @@ describe('Auth0Provider', () => {
214216
);
215217
clientMock.handleRedirectCallback.mockResolvedValueOnce({
216218
appState: { returnTo: '/foo' },
219+
response_type: ResponseType.Code
217220
});
218221
const wrapper = createWrapper();
219222
renderHook(() => useContext(Auth0Context), {
@@ -257,6 +260,7 @@ describe('Auth0Provider', () => {
257260
clientMock.getUser.mockResolvedValue(user);
258261
clientMock.handleRedirectCallback.mockResolvedValue({
259262
appState: { foo: 'bar' },
263+
response_type: ResponseType.Code
260264
});
261265
const onRedirectCallback = jest.fn();
262266
const wrapper = createWrapper({
@@ -266,7 +270,43 @@ describe('Auth0Provider', () => {
266270
wrapper,
267271
});
268272
await waitFor(() => {
269-
expect(onRedirectCallback).toHaveBeenCalledWith({ foo: 'bar' }, user);
273+
expect(onRedirectCallback).toHaveBeenCalledWith({ foo: 'bar', response_type: ResponseType.Code }, user);
274+
});
275+
});
276+
277+
it('should handle connect account redirect and call a custom handler', async () => {
278+
window.history.pushState(
279+
{},
280+
document.title,
281+
'/?connect_code=__test_code__&state=__test_state__'
282+
);
283+
const user = { name: '__test_user__' };
284+
const connectedAccount = {
285+
id: 'abc123',
286+
connection: 'google-oauth2',
287+
access_type: 'offline' as ConnectAccountRedirectResult['access_type'],
288+
created_at: '2024-01-01T00:00:00.000Z',
289+
expires_at: '2024-01-02T00:00:00.000Z',
290+
}
291+
clientMock.getUser.mockResolvedValue(user);
292+
clientMock.handleRedirectCallback.mockResolvedValue({
293+
appState: { foo: 'bar' },
294+
response_type: ResponseType.ConnectCode,
295+
...connectedAccount,
296+
});
297+
const onRedirectCallback = jest.fn();
298+
const wrapper = createWrapper({
299+
onRedirectCallback,
300+
});
301+
renderHook(() => useContext(Auth0Context), {
302+
wrapper,
303+
});
304+
await waitFor(() => {
305+
expect(onRedirectCallback).toHaveBeenCalledWith({
306+
foo: 'bar',
307+
response_type: ResponseType.ConnectCode,
308+
connectedAccount
309+
}, user);
270310
});
271311
});
272312

@@ -412,6 +452,35 @@ describe('Auth0Provider', () => {
412452
expect(warn).toHaveBeenCalled();
413453
});
414454

455+
it('should provide a connectAccountWithRedirect method', async () => {
456+
const wrapper = createWrapper();
457+
const { result } = renderHook(
458+
() => useContext(Auth0Context),
459+
{ wrapper }
460+
);
461+
await waitFor(() => {
462+
expect(result.current.connectAccountWithRedirect).toBeInstanceOf(Function);
463+
});
464+
await result.current.connectAccountWithRedirect({
465+
connection: 'google-apps'
466+
});
467+
expect(clientMock.connectAccountWithRedirect).toHaveBeenCalledWith({
468+
connection: 'google-apps',
469+
});
470+
});
471+
472+
it('should handle errors from connectAccountWithRedirect', async () => {
473+
const wrapper = createWrapper();
474+
const { result } = renderHook(
475+
() => useContext(Auth0Context),
476+
{ wrapper }
477+
);
478+
clientMock.connectAccountWithRedirect.mockRejectedValue(new Error('__test_error__'));
479+
await act(async () => {
480+
await expect(result.current.connectAccountWithRedirect).rejects.toThrow('__test_error__');
481+
});
482+
});
483+
415484
it('should provide a logout method', async () => {
416485
const user = { name: '__test_user__' };
417486
clientMock.getUser.mockResolvedValue(user);
@@ -814,6 +883,7 @@ describe('Auth0Provider', () => {
814883
it('should provide a handleRedirectCallback method', async () => {
815884
clientMock.handleRedirectCallback.mockResolvedValue({
816885
appState: { redirectUri: '/' },
886+
response_type: ResponseType.Code
817887
});
818888
const wrapper = createWrapper();
819889
const { result } = renderHook(
@@ -827,6 +897,7 @@ describe('Auth0Provider', () => {
827897
appState: {
828898
redirectUri: '/',
829899
},
900+
response_type: ResponseType.Code
830901
});
831902
});
832903
expect(clientMock.handleRedirectCallback).toHaveBeenCalled();
@@ -926,6 +997,7 @@ describe('Auth0Provider', () => {
926997
appState: {
927998
redirectUri: '/',
928999
},
1000+
response_type: ResponseType.Code
9291001
});
9301002
clientMock.getUser.mockResolvedValue(undefined);
9311003
const wrapper = createWrapper();
@@ -938,6 +1010,7 @@ describe('Auth0Provider', () => {
9381010
appState: {
9391011
redirectUri: '/',
9401012
},
1013+
response_type: ResponseType.Code
9411014
});
9421015
});
9431016

@@ -1012,6 +1085,7 @@ describe('Auth0Provider', () => {
10121085
);
10131086
clientMock.handleRedirectCallback.mockResolvedValue({
10141087
appState: undefined,
1088+
response_type: ResponseType.Code
10151089
});
10161090
render(
10171091
<StrictMode>

__tests__/utils.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ describe('utils hasAuthParams', () => {
4242
].forEach((search) => expect(hasAuthParams(search)).toBeTruthy());
4343
});
4444

45+
it('should recognise the connect_code and state param', async () => {
46+
[
47+
'?connect_code=1&state=2',
48+
'?foo=1&state=2&connect_code=3',
49+
'?connect_code=1&foo=2&state=3',
50+
'?state=1&connect_code=2&foo=3',
51+
].forEach((search) => expect(hasAuthParams(search)).toBeTruthy());
52+
});
53+
4554
it('should recognise the error and state param', async () => {
4655
[
4756
'?error=1&state=2',

0 commit comments

Comments
 (0)