@axa-fr/react-oidc 7.29.4 → 7.29.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FetchToken.d.ts.map +1 -1
- package/dist/ReactOidc.d.ts.map +1 -1
- package/dist/core/default-component/SilentLogin.component.d.ts.map +1 -1
- package/dist/core/routes/OidcRoutes.d.ts.map +1 -1
- package/dist/index.js +84 -89
- package/dist/index.umd.cjs +1 -1
- package/package.json +3 -3
- package/src/FetchToken.spec.tsx +135 -0
- package/src/FetchToken.tsx +4 -17
- package/src/OidcSecure.spec.tsx +84 -0
- package/src/ReactOidc.spec.tsx +222 -0
- package/src/ReactOidc.tsx +12 -22
- package/src/core/default-component/SilentLogin.component.spec.tsx +71 -0
- package/src/core/default-component/SilentLogin.component.tsx +1 -3
- package/src/core/routes/OidcRoutes.behavior.spec.tsx +123 -0
- package/src/core/routes/OidcRoutes.tsx +15 -21
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { Fetch } from '@axa-fr/oidc-client';
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { useOidcFetch } from './FetchToken';
|
|
6
|
+
|
|
7
|
+
const { getOrThrow, fetchWithTokens } = vi.hoisted(() => ({
|
|
8
|
+
getOrThrow: vi.fn(),
|
|
9
|
+
fetchWithTokens: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('@axa-fr/oidc-client', () => ({
|
|
13
|
+
OidcClient: { getOrThrow },
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe('useOidcFetch', () => {
|
|
17
|
+
const originalFetch = vi.fn<Fetch>();
|
|
18
|
+
const authenticatedFetch = vi.fn<Fetch>();
|
|
19
|
+
const response = new Response('response');
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.resetAllMocks();
|
|
23
|
+
getOrThrow.mockReturnValue({ fetchWithTokens });
|
|
24
|
+
fetchWithTokens.mockReturnValue(authenticatedFetch);
|
|
25
|
+
authenticatedFetch.mockResolvedValue(response);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.unstubAllGlobals();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('looks up the default configuration only when a request is made', async () => {
|
|
33
|
+
const { result } = renderHook(() => useOidcFetch(originalFetch));
|
|
34
|
+
|
|
35
|
+
expect(getOrThrow).not.toHaveBeenCalled();
|
|
36
|
+
await expect(result.current.fetch('/resource')).resolves.toBe(response);
|
|
37
|
+
expect(getOrThrow).toHaveBeenCalledExactlyOnceWith('default');
|
|
38
|
+
expect(fetchWithTokens).toHaveBeenCalledExactlyOnceWith(originalFetch, false);
|
|
39
|
+
expect(authenticatedFetch).toHaveBeenCalledExactlyOnceWith('/resource', undefined);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('defaults to the browser fetch implementation', async () => {
|
|
43
|
+
vi.stubGlobal('fetch', originalFetch);
|
|
44
|
+
const { result } = renderHook(() => useOidcFetch());
|
|
45
|
+
|
|
46
|
+
await result.current.fetch('/resource');
|
|
47
|
+
|
|
48
|
+
expect(fetchWithTokens).toHaveBeenCalledExactlyOnceWith(originalFetch, false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it.each([new URL('https://example.com/resource'), new Request('https://example.com/resource')])(
|
|
52
|
+
'forwards request objects and options unchanged: %s',
|
|
53
|
+
async input => {
|
|
54
|
+
const init: RequestInit = { method: 'POST', headers: { 'X-Custom': 'value' }, body: 'body' };
|
|
55
|
+
const { result } = renderHook(() => useOidcFetch(originalFetch, 'custom', true));
|
|
56
|
+
|
|
57
|
+
await expect(result.current.fetch(input, init)).resolves.toBe(response);
|
|
58
|
+
|
|
59
|
+
expect(getOrThrow).toHaveBeenCalledWith('custom');
|
|
60
|
+
expect(fetchWithTokens).toHaveBeenCalledWith(originalFetch, true);
|
|
61
|
+
expect(authenticatedFetch.mock.calls[0][0]).toBe(input);
|
|
62
|
+
expect(authenticatedFetch.mock.calls[0][1]).toBe(init);
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
it('uses the current client for each request rather than caching it', async () => {
|
|
67
|
+
const nextFetchWithTokens = vi.fn().mockReturnValue(authenticatedFetch);
|
|
68
|
+
const { result } = renderHook(() => useOidcFetch(originalFetch));
|
|
69
|
+
await result.current.fetch('/first');
|
|
70
|
+
|
|
71
|
+
getOrThrow.mockReturnValue({ fetchWithTokens: nextFetchWithTokens });
|
|
72
|
+
await result.current.fetch('/second');
|
|
73
|
+
|
|
74
|
+
expect(getOrThrow).toHaveBeenCalledTimes(2);
|
|
75
|
+
expect(fetchWithTokens).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(nextFetchWithTokens).toHaveBeenCalledExactlyOnceWith(originalFetch, false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('keeps the callback stable when its dependencies are unchanged', () => {
|
|
80
|
+
const { result, rerender } = renderHook(() => useOidcFetch(originalFetch));
|
|
81
|
+
const callback = result.current.fetch;
|
|
82
|
+
|
|
83
|
+
rerender();
|
|
84
|
+
|
|
85
|
+
expect(result.current.fetch).toBe(callback);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it.each([
|
|
89
|
+
{ fetch: vi.fn<Fetch>(), configurationName: 'default', isDpop: false },
|
|
90
|
+
{ fetch: originalFetch, configurationName: 'custom', isDpop: false },
|
|
91
|
+
{ fetch: originalFetch, configurationName: 'default', isDpop: true },
|
|
92
|
+
])('updates the callback when a dependency changes: %s', async nextProps => {
|
|
93
|
+
const { result, rerender } = renderHook(
|
|
94
|
+
({ fetch, configurationName, isDpop }) => useOidcFetch(fetch, configurationName, isDpop),
|
|
95
|
+
{ initialProps: { fetch: originalFetch, configurationName: 'default', isDpop: false } },
|
|
96
|
+
);
|
|
97
|
+
const callback = result.current.fetch;
|
|
98
|
+
|
|
99
|
+
rerender(nextProps);
|
|
100
|
+
expect(result.current.fetch).not.toBe(callback);
|
|
101
|
+
await result.current.fetch('/resource');
|
|
102
|
+
|
|
103
|
+
expect(getOrThrow).toHaveBeenCalledWith(nextProps.configurationName);
|
|
104
|
+
expect(fetchWithTokens).toHaveBeenCalledWith(nextProps.fetch, nextProps.isDpop);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('rejects the request rather than throwing synchronously when configuration is missing', async () => {
|
|
108
|
+
const error = new Error('Missing configuration');
|
|
109
|
+
getOrThrow.mockImplementation(() => {
|
|
110
|
+
throw error;
|
|
111
|
+
});
|
|
112
|
+
const { result } = renderHook(() => useOidcFetch(originalFetch));
|
|
113
|
+
|
|
114
|
+
await expect(result.current.fetch('/resource')).rejects.toBe(error);
|
|
115
|
+
expect(fetchWithTokens).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('preserves errors thrown while constructing the authenticated fetch', async () => {
|
|
119
|
+
const error = new Error('Cannot construct fetch');
|
|
120
|
+
fetchWithTokens.mockImplementation(() => {
|
|
121
|
+
throw error;
|
|
122
|
+
});
|
|
123
|
+
const { result } = renderHook(() => useOidcFetch(originalFetch));
|
|
124
|
+
|
|
125
|
+
await expect(result.current.fetch('/resource')).rejects.toBe(error);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('preserves rejected fetch errors', async () => {
|
|
129
|
+
const error = new Error('Network failure');
|
|
130
|
+
authenticatedFetch.mockRejectedValue(error);
|
|
131
|
+
const { result } = renderHook(() => useOidcFetch(originalFetch));
|
|
132
|
+
|
|
133
|
+
await expect(result.current.fetch('/resource')).rejects.toBe(error);
|
|
134
|
+
});
|
|
135
|
+
});
|
package/src/FetchToken.tsx
CHANGED
|
@@ -7,18 +7,6 @@ export interface ComponentWithOidcFetchProps {
|
|
|
7
7
|
|
|
8
8
|
const defaultConfigurationName = 'default';
|
|
9
9
|
|
|
10
|
-
const fetchWithToken =
|
|
11
|
-
(
|
|
12
|
-
fetch: Fetch,
|
|
13
|
-
getOidcWithConfigurationName: () => OidcClient | null,
|
|
14
|
-
demonstratingProofOfPossession: boolean = false,
|
|
15
|
-
) =>
|
|
16
|
-
async (...params: Parameters<Fetch>) => {
|
|
17
|
-
const oidc = getOidcWithConfigurationName();
|
|
18
|
-
const newFetch = oidc.fetchWithTokens(fetch, demonstratingProofOfPossession);
|
|
19
|
-
return await newFetch(...params);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
10
|
export const withOidcFetch =
|
|
23
11
|
(
|
|
24
12
|
fetch: Fetch = null,
|
|
@@ -46,14 +34,13 @@ export const useOidcFetch = (
|
|
|
46
34
|
const getOidc = OidcClient.getOrThrow;
|
|
47
35
|
|
|
48
36
|
const memoizedFetchCallback = useCallback(
|
|
49
|
-
(input: RequestInfo | URL, init?: RequestInit) => {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
37
|
+
async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
38
|
+
const oidc = getOidc(configurationName);
|
|
39
|
+
const authenticatedFetch = oidc.fetchWithTokens(
|
|
52
40
|
previousFetch,
|
|
53
|
-
getOidcWithConfigurationName,
|
|
54
41
|
demonstratingProofOfPossession,
|
|
55
42
|
);
|
|
56
|
-
return
|
|
43
|
+
return await authenticatedFetch(input, init);
|
|
57
44
|
},
|
|
58
45
|
[previousFetch, configurationName, demonstratingProofOfPossession],
|
|
59
46
|
);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { OidcSecure, withOidcSecure } from './OidcSecure';
|
|
5
|
+
|
|
6
|
+
const { getOrThrow, loginAsync } = vi.hoisted(() => ({
|
|
7
|
+
getOrThrow: vi.fn(),
|
|
8
|
+
loginAsync: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('@axa-fr/oidc-client', () => ({ OidcClient: { getOrThrow } }));
|
|
12
|
+
|
|
13
|
+
describe('OidcSecure', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.resetAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders children without logging in when tokens are present', () => {
|
|
19
|
+
getOrThrow.mockReturnValue({ tokens: {}, loginAsync });
|
|
20
|
+
|
|
21
|
+
render(<OidcSecure>Protected content</OidcSecure>);
|
|
22
|
+
|
|
23
|
+
expect(screen.getByText('Protected content')).toBeDefined();
|
|
24
|
+
expect(getOrThrow).toHaveBeenCalledWith('default');
|
|
25
|
+
expect(loginAsync).not.toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('hides children and starts login with the supplied configuration and arguments', () => {
|
|
29
|
+
getOrThrow.mockReturnValue({ tokens: null, loginAsync });
|
|
30
|
+
const extras = { prompt: 'login' };
|
|
31
|
+
|
|
32
|
+
render(
|
|
33
|
+
<OidcSecure configurationName="custom" callbackPath="/private" extras={extras}>
|
|
34
|
+
Protected content
|
|
35
|
+
</OidcSecure>,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(screen.queryByText('Protected content')).toBeNull();
|
|
39
|
+
expect(getOrThrow).toHaveBeenCalledWith('custom');
|
|
40
|
+
expect(loginAsync).toHaveBeenCalledExactlyOnceWith('/private', extras);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('does not restart login while logout is in progress', () => {
|
|
44
|
+
getOrThrow.mockReturnValue({ tokens: null, isLoggingOut: true, loginAsync });
|
|
45
|
+
|
|
46
|
+
render(<OidcSecure>Protected content</OidcSecure>);
|
|
47
|
+
|
|
48
|
+
expect(screen.queryByText('Protected content')).toBeNull();
|
|
49
|
+
expect(loginAsync).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('does not repeat login on an unchanged render', () => {
|
|
53
|
+
getOrThrow.mockReturnValue({ tokens: null, loginAsync });
|
|
54
|
+
const { rerender } = render(<OidcSecure>Protected content</OidcSecure>);
|
|
55
|
+
|
|
56
|
+
rerender(<OidcSecure>Protected content</OidcSecure>);
|
|
57
|
+
|
|
58
|
+
expect(loginAsync).toHaveBeenCalledExactlyOnceWith(null, null);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('preserves the fail-fast behavior for a missing configuration', () => {
|
|
62
|
+
const error = new Error('Missing configuration');
|
|
63
|
+
getOrThrow.mockImplementation(() => {
|
|
64
|
+
throw error;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(() => render(<OidcSecure>Protected content</OidcSecure>)).toThrow(error);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('passes props through the higher-order component', () => {
|
|
71
|
+
getOrThrow.mockReturnValue({ tokens: {}, loginAsync });
|
|
72
|
+
const Component = withOidcSecure(
|
|
73
|
+
({ children }) => <span>{children}</span>,
|
|
74
|
+
'/callback',
|
|
75
|
+
null,
|
|
76
|
+
'custom',
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
render(<Component>Wrapped content</Component>);
|
|
80
|
+
|
|
81
|
+
expect(screen.getByText('Wrapped content')).toBeDefined();
|
|
82
|
+
expect(getOrThrow).toHaveBeenCalledWith('custom');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { Tokens } from '@axa-fr/oidc-client';
|
|
2
|
+
import { act, renderHook } from '@testing-library/react';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { useOidc, useOidcAccessToken, useOidcIdToken } from './ReactOidc';
|
|
6
|
+
|
|
7
|
+
const { get, subscribeEvents, removeEventSubscription, generateProof } = vi.hoisted(() => ({
|
|
8
|
+
get: vi.fn(),
|
|
9
|
+
subscribeEvents: vi.fn<(listener: (name: string) => void) => string>(),
|
|
10
|
+
removeEventSubscription: vi.fn(),
|
|
11
|
+
generateProof: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('@axa-fr/oidc-client', () => ({
|
|
15
|
+
OidcClient: {
|
|
16
|
+
get,
|
|
17
|
+
eventNames: {
|
|
18
|
+
token_renewed: 'token_renewed',
|
|
19
|
+
token_acquired: 'token_acquired',
|
|
20
|
+
logout_from_another_tab: 'logout_from_another_tab',
|
|
21
|
+
logout_from_same_tab: 'logout_from_same_tab',
|
|
22
|
+
refreshTokensAsync_error: 'refreshTokensAsync_error',
|
|
23
|
+
syncTokensAsync_error: 'syncTokensAsync_error',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const initialTokens = {
|
|
29
|
+
accessToken: 'access-example',
|
|
30
|
+
accessTokenPayload: { sub: 'user' },
|
|
31
|
+
idToken: 'id-example',
|
|
32
|
+
idTokenPayload: { sub: 'user' },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const client = {
|
|
36
|
+
tokens: null as Pick<
|
|
37
|
+
Tokens,
|
|
38
|
+
'accessToken' | 'accessTokenPayload' | 'idToken' | 'idTokenPayload'
|
|
39
|
+
> | null,
|
|
40
|
+
configuration: { demonstrating_proof_of_possession: false },
|
|
41
|
+
subscribeEvents,
|
|
42
|
+
removeEventSubscription,
|
|
43
|
+
generateDemonstrationOfProofOfPossessionAsync: generateProof,
|
|
44
|
+
loginAsync: vi.fn(),
|
|
45
|
+
logoutAsync: vi.fn(),
|
|
46
|
+
renewTokensAsync: vi.fn(),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.resetAllMocks();
|
|
51
|
+
client.tokens = { ...initialTokens };
|
|
52
|
+
client.configuration.demonstrating_proof_of_possession = false;
|
|
53
|
+
get.mockReturnValue(client);
|
|
54
|
+
subscribeEvents.mockReturnValue('subscription');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe.each([
|
|
58
|
+
{
|
|
59
|
+
name: 'access token',
|
|
60
|
+
useToken: useOidcAccessToken,
|
|
61
|
+
initial: {
|
|
62
|
+
accessToken: initialTokens.accessToken,
|
|
63
|
+
accessTokenPayload: initialTokens.accessTokenPayload,
|
|
64
|
+
generateDemonstrationOfProofOfPossessionAsync: null,
|
|
65
|
+
},
|
|
66
|
+
empty: { accessToken: null, accessTokenPayload: null },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'ID token',
|
|
70
|
+
useToken: useOidcIdToken,
|
|
71
|
+
initial: {
|
|
72
|
+
idToken: initialTokens.idToken,
|
|
73
|
+
idTokenPayload: initialTokens.idTokenPayload,
|
|
74
|
+
},
|
|
75
|
+
empty: { idToken: null, idTokenPayload: null },
|
|
76
|
+
},
|
|
77
|
+
])('$name hook', ({ useToken, initial, empty }) => {
|
|
78
|
+
it('initializes from the configured client', () => {
|
|
79
|
+
const { result } = renderHook(() => useToken('custom'));
|
|
80
|
+
|
|
81
|
+
expect(result.current).toEqual(initial);
|
|
82
|
+
expect(get).toHaveBeenCalledWith('custom');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('uses the empty state when the client has no tokens', () => {
|
|
86
|
+
client.tokens = null;
|
|
87
|
+
const { result } = renderHook(() => useToken());
|
|
88
|
+
|
|
89
|
+
expect(result.current).toEqual(empty);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it.each([
|
|
93
|
+
'token_renewed',
|
|
94
|
+
'token_acquired',
|
|
95
|
+
'logout_from_another_tab',
|
|
96
|
+
'logout_from_same_tab',
|
|
97
|
+
'refreshTokensAsync_error',
|
|
98
|
+
'syncTokensAsync_error',
|
|
99
|
+
])('refreshes token state on %s', eventName => {
|
|
100
|
+
const { result } = renderHook(() => useToken());
|
|
101
|
+
const listener = subscribeEvents.mock.calls[0][0];
|
|
102
|
+
|
|
103
|
+
client.tokens = null;
|
|
104
|
+
act(() => listener(eventName));
|
|
105
|
+
expect(result.current).toEqual(empty);
|
|
106
|
+
|
|
107
|
+
client.tokens = { ...initialTokens };
|
|
108
|
+
act(() => listener(eventName));
|
|
109
|
+
expect(result.current).toEqual(initial);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('ignores unrelated events', () => {
|
|
113
|
+
const { result } = renderHook(() => useToken());
|
|
114
|
+
client.tokens = null;
|
|
115
|
+
|
|
116
|
+
act(() => subscribeEvents.mock.calls[0][0]('unrelated'));
|
|
117
|
+
|
|
118
|
+
expect(result.current).toEqual(initial);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('cleans up subscriptions on configuration changes and unmount', () => {
|
|
122
|
+
subscribeEvents.mockReturnValueOnce('first').mockReturnValueOnce('second');
|
|
123
|
+
const { rerender, unmount } = renderHook(({ name }) => useToken(name), {
|
|
124
|
+
initialProps: { name: 'first' },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
rerender({ name: 'second' });
|
|
128
|
+
expect(removeEventSubscription).toHaveBeenCalledExactlyOnceWith('first');
|
|
129
|
+
expect(subscribeEvents).toHaveBeenCalledTimes(2);
|
|
130
|
+
expect(get).toHaveBeenLastCalledWith('second');
|
|
131
|
+
|
|
132
|
+
unmount();
|
|
133
|
+
expect(removeEventSubscription.mock.calls).toEqual([['first'], ['second']]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('access token proof generation', () => {
|
|
138
|
+
it('uses the initial access token when generating a proof', async () => {
|
|
139
|
+
client.configuration.demonstrating_proof_of_possession = true;
|
|
140
|
+
generateProof.mockResolvedValue('proof');
|
|
141
|
+
const { result } = renderHook(() => useOidcAccessToken());
|
|
142
|
+
|
|
143
|
+
await expect(
|
|
144
|
+
result.current.generateDemonstrationOfProofOfPossessionAsync(
|
|
145
|
+
'https://api.example.com',
|
|
146
|
+
'GET',
|
|
147
|
+
),
|
|
148
|
+
).resolves.toBe('proof');
|
|
149
|
+
expect(generateProof).toHaveBeenCalledExactlyOnceWith(
|
|
150
|
+
'access-example',
|
|
151
|
+
'https://api.example.com',
|
|
152
|
+
'GET',
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('uses refreshed tokens and forwards proof extras after a token event', async () => {
|
|
157
|
+
client.configuration.demonstrating_proof_of_possession = true;
|
|
158
|
+
const { result } = renderHook(() => useOidcAccessToken());
|
|
159
|
+
client.tokens = { ...initialTokens, accessToken: 'renewed-example' };
|
|
160
|
+
act(() => subscribeEvents.mock.calls[0][0]('token_renewed'));
|
|
161
|
+
const extras = { nonce: 'example-nonce' };
|
|
162
|
+
|
|
163
|
+
await result.current.generateDemonstrationOfProofOfPossessionAsync(
|
|
164
|
+
'https://api.example.com',
|
|
165
|
+
'POST',
|
|
166
|
+
extras,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(generateProof).toHaveBeenCalledExactlyOnceWith(
|
|
170
|
+
'renewed-example',
|
|
171
|
+
'https://api.example.com',
|
|
172
|
+
'POST',
|
|
173
|
+
extras,
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('useOidc', () => {
|
|
179
|
+
it('tracks authentication on login and logout events', () => {
|
|
180
|
+
client.tokens = null;
|
|
181
|
+
const { result } = renderHook(() => useOidc('custom'));
|
|
182
|
+
const listener = subscribeEvents.mock.calls[0][0];
|
|
183
|
+
expect(result.current.isAuthenticated).toBe(false);
|
|
184
|
+
|
|
185
|
+
client.tokens = { ...initialTokens };
|
|
186
|
+
act(() => listener('token_acquired'));
|
|
187
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
188
|
+
|
|
189
|
+
client.tokens = null;
|
|
190
|
+
act(() => listener('logout_from_same_tab'));
|
|
191
|
+
expect(result.current.isAuthenticated).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('forwards login and logout arguments and returns their promises', () => {
|
|
195
|
+
const loginPromise = Promise.resolve();
|
|
196
|
+
const logoutPromise = Promise.resolve();
|
|
197
|
+
client.loginAsync.mockReturnValue(loginPromise);
|
|
198
|
+
client.logoutAsync.mockReturnValue(logoutPromise);
|
|
199
|
+
const { result } = renderHook(() => useOidc('custom'));
|
|
200
|
+
const extras = { prompt: 'login' };
|
|
201
|
+
|
|
202
|
+
expect(result.current.login('/callback', extras, true, 'openid')).toBe(loginPromise);
|
|
203
|
+
expect(client.loginAsync).toHaveBeenCalledExactlyOnceWith(
|
|
204
|
+
'/callback',
|
|
205
|
+
extras,
|
|
206
|
+
false,
|
|
207
|
+
'openid',
|
|
208
|
+
true,
|
|
209
|
+
);
|
|
210
|
+
expect(result.current.logout('/logout', extras)).toBe(logoutPromise);
|
|
211
|
+
expect(client.logoutAsync).toHaveBeenCalledExactlyOnceWith('/logout', extras);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('returns renewed token data without adding internal fields', async () => {
|
|
215
|
+
client.renewTokensAsync.mockResolvedValue({ ...initialTokens, internal: 'not exposed' });
|
|
216
|
+
const { result } = renderHook(() => useOidc());
|
|
217
|
+
const extras = { scope: 'openid' };
|
|
218
|
+
|
|
219
|
+
await expect(result.current.renewTokens(extras)).resolves.toEqual(initialTokens);
|
|
220
|
+
expect(client.renewTokensAsync).toHaveBeenCalledExactlyOnceWith(extras);
|
|
221
|
+
});
|
|
222
|
+
});
|
package/src/ReactOidc.tsx
CHANGED
|
@@ -9,15 +9,19 @@ type GetOidcFn = {
|
|
|
9
9
|
(configurationName?: string): any;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
const defaultIsAuthenticated = (getOidc: GetOidcFn, configurationName: string) => {
|
|
13
|
-
let isAuthenticated = false;
|
|
12
|
+
const defaultIsAuthenticated = (getOidc: GetOidcFn, configurationName: string): boolean => {
|
|
14
13
|
const oidc = getOidc(configurationName);
|
|
15
|
-
|
|
16
|
-
isAuthenticated = oidc.tokens != null;
|
|
17
|
-
}
|
|
18
|
-
return isAuthenticated;
|
|
14
|
+
return oidc ? oidc.tokens != null : false;
|
|
19
15
|
};
|
|
20
16
|
|
|
17
|
+
const isTokenStateEvent = (name: string): boolean =>
|
|
18
|
+
name === OidcClient.eventNames.token_renewed ||
|
|
19
|
+
name === OidcClient.eventNames.token_acquired ||
|
|
20
|
+
name === OidcClient.eventNames.logout_from_another_tab ||
|
|
21
|
+
name === OidcClient.eventNames.logout_from_same_tab ||
|
|
22
|
+
name === OidcClient.eventNames.refreshTokensAsync_error ||
|
|
23
|
+
name === OidcClient.eventNames.syncTokensAsync_error;
|
|
24
|
+
|
|
21
25
|
export const useOidc = (configurationName = defaultConfigurationName) => {
|
|
22
26
|
const getOidc = OidcClient.get;
|
|
23
27
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() =>
|
|
@@ -149,14 +153,7 @@ export const useOidcAccessToken = (configurationName = defaultConfigurationName)
|
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
const newSubscriptionId = oidc.subscribeEvents((name: string, data: any) => {
|
|
152
|
-
if (
|
|
153
|
-
name === OidcClient.eventNames.token_renewed ||
|
|
154
|
-
name === OidcClient.eventNames.token_acquired ||
|
|
155
|
-
name === OidcClient.eventNames.logout_from_another_tab ||
|
|
156
|
-
name === OidcClient.eventNames.logout_from_same_tab ||
|
|
157
|
-
name === OidcClient.eventNames.refreshTokensAsync_error ||
|
|
158
|
-
name === OidcClient.eventNames.syncTokensAsync_error
|
|
159
|
-
) {
|
|
156
|
+
if (isTokenStateEvent(name)) {
|
|
160
157
|
if (isMounted) {
|
|
161
158
|
const tokens = oidc.tokens;
|
|
162
159
|
setAccessToken(
|
|
@@ -215,14 +212,7 @@ export const useOidcIdToken = (configurationName = defaultConfigurationName) =>
|
|
|
215
212
|
}
|
|
216
213
|
|
|
217
214
|
const newSubscriptionId = oidc.subscribeEvents((name: string, data: any) => {
|
|
218
|
-
if (
|
|
219
|
-
name === OidcClient.eventNames.token_renewed ||
|
|
220
|
-
name === OidcClient.eventNames.token_acquired ||
|
|
221
|
-
name === OidcClient.eventNames.logout_from_another_tab ||
|
|
222
|
-
name === OidcClient.eventNames.logout_from_same_tab ||
|
|
223
|
-
name === OidcClient.eventNames.refreshTokensAsync_error ||
|
|
224
|
-
name === OidcClient.eventNames.syncTokensAsync_error
|
|
225
|
-
) {
|
|
215
|
+
if (isTokenStateEvent(name)) {
|
|
226
216
|
if (isMounted) {
|
|
227
217
|
const tokens = oidc.tokens;
|
|
228
218
|
setIDToken(
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import SilentLogin from './SilentLogin.component';
|
|
5
|
+
|
|
6
|
+
const { getOrThrow, getParseQueryStringFromLocation, loginAsync } = vi.hoisted(() => ({
|
|
7
|
+
getOrThrow: vi.fn(),
|
|
8
|
+
getParseQueryStringFromLocation: vi.fn(),
|
|
9
|
+
loginAsync: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('@axa-fr/oidc-client', () => ({
|
|
13
|
+
OidcClient: { getOrThrow },
|
|
14
|
+
getParseQueryStringFromLocation,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('silent login', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.resetAllMocks();
|
|
20
|
+
getOrThrow.mockReturnValue({ tokens: null, loginAsync });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it.each([{}, { state: 'state' }, { state: 'state', scope: 'openid' }])(
|
|
24
|
+
'passes null extras when the query contains only reserved parameters: %s',
|
|
25
|
+
query => {
|
|
26
|
+
getParseQueryStringFromLocation.mockReturnValue(query);
|
|
27
|
+
|
|
28
|
+
render(<SilentLogin configurationName="custom" />);
|
|
29
|
+
|
|
30
|
+
expect(getOrThrow).toHaveBeenCalledWith('custom');
|
|
31
|
+
expect(loginAsync).toHaveBeenCalledExactlyOnceWith(null, null, true, query.scope);
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
it('forwards non-reserved parameters without changing the query data', () => {
|
|
36
|
+
const query = Object.freeze({
|
|
37
|
+
state: 'state',
|
|
38
|
+
scope: 'openid profile',
|
|
39
|
+
prompt: 'none',
|
|
40
|
+
login_hint: 'example',
|
|
41
|
+
});
|
|
42
|
+
getParseQueryStringFromLocation.mockReturnValue(query);
|
|
43
|
+
|
|
44
|
+
render(<SilentLogin configurationName="custom" />);
|
|
45
|
+
|
|
46
|
+
expect(loginAsync).toHaveBeenCalledExactlyOnceWith(
|
|
47
|
+
null,
|
|
48
|
+
{ prompt: 'none', login_hint: 'example' },
|
|
49
|
+
true,
|
|
50
|
+
'openid profile',
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does not start login when already authenticated', () => {
|
|
55
|
+
getParseQueryStringFromLocation.mockReturnValue({});
|
|
56
|
+
getOrThrow.mockReturnValue({ tokens: {}, loginAsync });
|
|
57
|
+
|
|
58
|
+
render(<SilentLogin configurationName="custom" />);
|
|
59
|
+
|
|
60
|
+
expect(loginAsync).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('only starts silent login on the initial mount', () => {
|
|
64
|
+
getParseQueryStringFromLocation.mockReturnValue({ prompt: 'none' });
|
|
65
|
+
const { rerender } = render(<SilentLogin configurationName="custom" />);
|
|
66
|
+
|
|
67
|
+
rerender(<SilentLogin configurationName="custom" />);
|
|
68
|
+
|
|
69
|
+
expect(loginAsync).toHaveBeenCalledOnce();
|
|
70
|
+
});
|
|
71
|
+
});
|