Skip to content

Commit af7a728

Browse files
committed
Add support for connected accounts
1 parent a6c5428 commit af7a728

15 files changed

+275
-50
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,5 @@ test-results
109109
cypress/screenshots
110110
cypress/videos
111111
.npmrc
112+
113+
!auth0-auth0-spa-js-2.6.0-connected-accounts.tgz

__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',
448 KB
Binary file not shown.

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ module.exports = {
3535
statements: 100,
3636
},
3737
},
38+
setupFiles: ['./jest.setup.js']
3839
};

jest.setup.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// jest.setup.js
2+
const { TextEncoder, TextDecoder } = require('util');
3+
4+
if (typeof global.TextEncoder === 'undefined') {
5+
global.TextEncoder = TextEncoder;
6+
}
7+
if (typeof global.TextDecoder === 'undefined') {
8+
global.TextDecoder = TextDecoder;
9+
}

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,6 @@
9595
"react-dom": "^16.11.0 || ^17 || ^18 || ^19"
9696
},
9797
"dependencies": {
98-
"@auth0/auth0-spa-js": "^2.5.0"
98+
"@auth0/auth0-spa-js": "file:auth0-auth0-spa-js-2.6.0-connected-accounts.tgz"
9999
}
100100
}

scripts/oidc-provider.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ const config = {
4242
webMessageResponseMode: {
4343
enabled: true,
4444
},
45+
dPoP: {
46+
enabled: true,
47+
}
4548
},
4649
rotateRefreshToken: true,
4750
interactions: {

0 commit comments

Comments
 (0)