@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/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
- if (oidc) {
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
+ });
@@ -13,9 +13,7 @@ const SilentLogin: ComponentType<any> = ({ configurationName }) => {
13
13
  if (key === 'state' || key === 'scope') {
14
14
  continue;
15
15
  }
16
- if (extras === null) {
17
- extras = {};
18
- }
16
+ extras ??= {};
19
17
  extras[key] = value;
20
18
  }
21
19
 
@@ -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
- if (path === getPath(silent_redirect_uri)) {
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
- if (path === getPath(silent_login_uri)) {
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
- switch (path) {
60
- case callbackPath:
61
- return (
62
- <CallbackComponent
63
- callBackError={callbackErrorComponent}
64
- callBackSuccess={callbackSuccessComponent}
65
- configurationName={configurationName}
66
- withCustomHistory={withCustomHistory}
67
- navigateAfterCallback={navigateAfterCallback}
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);