@axa-fr/react-oidc 7.29.3 → 7.29.5
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/README.md +268 -643
- package/dist/FetchToken.d.ts +1 -1
- package/dist/FetchToken.d.ts.map +1 -1
- package/dist/OidcSecure.d.ts +1 -1
- package/dist/OidcSecure.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 +164 -169
- package/dist/index.umd.cjs +1 -1
- package/package.json +18 -18
- 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
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
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { act, render, screen } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import Callback from '../default-component/Callback.component';
|
|
5
|
+
import SilentCallback from '../default-component/SilentCallback.component';
|
|
6
|
+
import SilentLogin from '../default-component/SilentLogin.component';
|
|
7
|
+
import OidcRoutes from './OidcRoutes';
|
|
8
|
+
|
|
9
|
+
vi.mock('../default-component/Callback.component', () => ({
|
|
10
|
+
default: vi.fn(() => <span>Callback</span>),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock('../default-component/SilentCallback.component', () => ({
|
|
13
|
+
default: vi.fn(() => <span>Silent callback</span>),
|
|
14
|
+
}));
|
|
15
|
+
vi.mock('../default-component/SilentLogin.component', () => ({
|
|
16
|
+
default: vi.fn(() => <span>Silent login</span>),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const props = {
|
|
20
|
+
configurationName: 'custom',
|
|
21
|
+
redirect_uri: new URL('/callback', window.location.href).href,
|
|
22
|
+
silent_redirect_uri: new URL('/silent-callback', window.location.href).href,
|
|
23
|
+
silent_login_uri: new URL('/silent-login', window.location.href).href,
|
|
24
|
+
location: {
|
|
25
|
+
getCurrentHref: () => window.location.href,
|
|
26
|
+
getPath: () => window.location.pathname,
|
|
27
|
+
getOrigin: () => window.location.origin,
|
|
28
|
+
open: vi.fn(),
|
|
29
|
+
reload: vi.fn(),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe('OIDC route selection', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
window.history.replaceState(null, '', '/');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
window.history.replaceState(null, '', '/');
|
|
41
|
+
vi.restoreAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders application content on a non-OIDC route', () => {
|
|
45
|
+
render(<OidcRoutes {...props}>Application</OidcRoutes>);
|
|
46
|
+
|
|
47
|
+
expect(screen.getByText('Application')).toBeDefined();
|
|
48
|
+
expect(Callback).not.toHaveBeenCalled();
|
|
49
|
+
expect(SilentCallback).not.toHaveBeenCalled();
|
|
50
|
+
expect(SilentLogin).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it.each([
|
|
54
|
+
['/callback?code=example#', 'Callback', Callback],
|
|
55
|
+
['/silent-callback', 'Silent callback', SilentCallback],
|
|
56
|
+
['/silent-login', 'Silent login', SilentLogin],
|
|
57
|
+
])('renders the matching handler for %s', (path, text, handler) => {
|
|
58
|
+
window.history.replaceState(null, '', path);
|
|
59
|
+
|
|
60
|
+
render(<OidcRoutes {...props}>Application</OidcRoutes>);
|
|
61
|
+
|
|
62
|
+
expect(screen.getByText(text)).toBeDefined();
|
|
63
|
+
expect(screen.queryByText('Application')).toBeNull();
|
|
64
|
+
expect(vi.mocked(handler).mock.calls[0][0]).toMatchObject({ configurationName: 'custom' });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('gives the silent callback priority when configured paths overlap', () => {
|
|
68
|
+
window.history.replaceState(null, '', '/callback');
|
|
69
|
+
|
|
70
|
+
render(
|
|
71
|
+
<OidcRoutes
|
|
72
|
+
{...props}
|
|
73
|
+
silent_redirect_uri={props.redirect_uri}
|
|
74
|
+
silent_login_uri={props.redirect_uri}
|
|
75
|
+
/>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(screen.getByText('Silent callback')).toBeDefined();
|
|
79
|
+
expect(Callback).not.toHaveBeenCalled();
|
|
80
|
+
expect(SilentLogin).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('forwards custom callback components and navigation callbacks unchanged', () => {
|
|
84
|
+
window.history.replaceState(null, '', '/callback');
|
|
85
|
+
const success = (): null => null;
|
|
86
|
+
const error = (): null => null;
|
|
87
|
+
const history = vi.fn();
|
|
88
|
+
const navigate = vi.fn();
|
|
89
|
+
|
|
90
|
+
render(
|
|
91
|
+
<OidcRoutes
|
|
92
|
+
{...props}
|
|
93
|
+
callbackSuccessComponent={success}
|
|
94
|
+
callbackErrorComponent={error}
|
|
95
|
+
withCustomHistory={history}
|
|
96
|
+
navigateAfterCallback={navigate}
|
|
97
|
+
/>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(vi.mocked(Callback).mock.calls[0][0]).toMatchObject({
|
|
101
|
+
callBackSuccess: success,
|
|
102
|
+
callBackError: error,
|
|
103
|
+
withCustomHistory: history,
|
|
104
|
+
navigateAfterCallback: navigate,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('updates the route on popstate and removes its listener on unmount', () => {
|
|
109
|
+
const addEventListener = vi.spyOn(window, 'addEventListener');
|
|
110
|
+
const removeEventListener = vi.spyOn(window, 'removeEventListener');
|
|
111
|
+
const { unmount } = render(<OidcRoutes {...props}>Application</OidcRoutes>);
|
|
112
|
+
const listener = addEventListener.mock.calls.find(([name]) => name === 'popstate')[1];
|
|
113
|
+
|
|
114
|
+
act(() => {
|
|
115
|
+
window.history.replaceState(null, '', '/silent-login');
|
|
116
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
117
|
+
});
|
|
118
|
+
expect(screen.getByText('Silent login')).toBeDefined();
|
|
119
|
+
|
|
120
|
+
unmount();
|
|
121
|
+
expect(removeEventListener).toHaveBeenCalledWith('popstate', listener, false);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -44,32 +44,26 @@ const OidcRoutes: FC<PropsWithChildren<OidcRoutesProps>> = ({
|
|
|
44
44
|
|
|
45
45
|
const callbackPath = getPath(redirect_uri);
|
|
46
46
|
|
|
47
|
-
if (silent_redirect_uri) {
|
|
48
|
-
|
|
49
|
-
return <SilentCallbackComponent configurationName={configurationName} />;
|
|
50
|
-
}
|
|
47
|
+
if (silent_redirect_uri && path === getPath(silent_redirect_uri)) {
|
|
48
|
+
return <SilentCallbackComponent configurationName={configurationName} />;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
if (silent_login_uri) {
|
|
54
|
-
|
|
55
|
-
return <SilentLoginComponent configurationName={configurationName} />;
|
|
56
|
-
}
|
|
51
|
+
if (silent_login_uri && path === getPath(silent_login_uri)) {
|
|
52
|
+
return <SilentLoginComponent configurationName={configurationName} />;
|
|
57
53
|
}
|
|
58
54
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
70
|
-
default:
|
|
71
|
-
return <>{children}</>;
|
|
55
|
+
if (path === callbackPath) {
|
|
56
|
+
return (
|
|
57
|
+
<CallbackComponent
|
|
58
|
+
callBackError={callbackErrorComponent}
|
|
59
|
+
callBackSuccess={callbackSuccessComponent}
|
|
60
|
+
configurationName={configurationName}
|
|
61
|
+
withCustomHistory={withCustomHistory}
|
|
62
|
+
navigateAfterCallback={navigateAfterCallback}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
72
65
|
}
|
|
66
|
+
return <>{children}</>;
|
|
73
67
|
};
|
|
74
68
|
|
|
75
69
|
export default React.memo(OidcRoutes);
|