UNPKG

piral-oidc

Version:

Plugin to integrate authentication using OpenID connect in Piral.

334 lines (304 loc) • 13.2 kB
/** * @vitest-environment jsdom */ import { describe, it, expect, vitest, beforeAll, beforeEach, afterAll } from 'vitest'; import { UserManager } from 'oidc-client-ts'; import { setupOidcClient } from './setup'; import { OidcConfig, OidcErrorType } from './types'; describe('Piral-Oidc setup module', () => { const setWindowInIFrame = () => Object.defineProperty(window, 'top', { value: { ...window }, configurable: true, writable: true }); const setWindowInTop = () => Object.defineProperty(window, 'top', { value: window, configurable: true, writable: true }); const originalWindowLocation = window.location; let oidcConfig: OidcConfig; const user = { access_token: '123', expires_in: 100, }; const mockGetUser = vitest.fn().mockResolvedValue(user).mockName('getUser'); const mockSigninCallback = vitest.fn().mockResolvedValue(undefined).mockName('signinCallback'); const mockSigninRedirectCallback = vitest.fn().mockResolvedValue(undefined).mockName('signinRedirectCallback'); const mockSigninRedirect = vitest.fn().mockResolvedValue(undefined).mockName('signinRedirect'); const mockSigninSilent = vitest.fn().mockResolvedValue(undefined).mockName('signinSilent'); const mockSigninSilentCallback = vitest.fn().mockResolvedValue(undefined).mockName('signinSilentCallback'); const mockSignoutRedirect = vitest.fn().mockResolvedValue(undefined).mockName('signoutRedirect'); const mockSignoutRedirectCallback = vitest.fn().mockResolvedValue(undefined).mockName('signoutRedirectCallback'); const mockSignoutPopupCallback = vitest.fn().mockResolvedValue(undefined).mockName('signoutPopupCallback'); const postLogoutRedirectUri = 'http://localhost:8000/post-logout'; const redirectUri = 'http://localhost:8000/callback'; const appUri = 'http://localhost:8000/app'; beforeAll(() => { vitest.spyOn(UserManager.prototype, 'getUser').mockImplementation(mockGetUser); vitest.spyOn(UserManager.prototype, 'signinCallback').mockImplementation(mockSigninCallback); vitest.spyOn(UserManager.prototype, 'signinRedirectCallback').mockImplementation(mockSigninRedirectCallback); vitest.spyOn(UserManager.prototype, 'signinRedirect').mockImplementation(mockSigninRedirect); vitest.spyOn(UserManager.prototype, 'signinSilent').mockImplementation(mockSigninSilent); vitest.spyOn(UserManager.prototype, 'signinSilentCallback').mockImplementation(mockSigninSilentCallback); vitest.spyOn(UserManager.prototype, 'signoutRedirect').mockImplementation(mockSignoutRedirect); vitest.spyOn(UserManager.prototype, 'signoutRedirectCallback').mockImplementation(mockSignoutRedirectCallback); vitest.spyOn(UserManager.prototype, 'signoutPopupCallback').mockImplementation(mockSignoutPopupCallback); }); afterAll(() => { window.location = originalWindowLocation; setWindowInTop(); }); beforeEach(() => { vitest.clearAllMocks(); mockGetUser.mockResolvedValue(user); //@ts-ignore delete window.location; //@ts-ignore window.location = new URL('http://localhost:8000/'); setWindowInTop(); oidcConfig = { clientId: 'clientId', identityProviderUri: 'http://localhost:8080/provider/uri', appUri, redirectUri, postLogoutRedirectUri, scopes: ['openid', 'custom'], restrict: false, }; }); it('setupOidcClient should return the following signature', () => { const client = setupOidcClient(oidcConfig); expect(client).toMatchObject({ login: expect.any(Function), logout: expect.any(Function), extendHeaders: expect.any(Function), token: expect.any(Function), account: expect.any(Function), handleAuthentication: expect.any(Function), }); }); it('should call signoutRedirectCallback when on the post_logout_redirect_uri in the top frame', () => { setWindowInTop(); expect(mockSignoutRedirectCallback).not.toHaveBeenCalled(); //@ts-ignore window.location = new URL(postLogoutRedirectUri); setupOidcClient(oidcConfig); expect(mockSignoutRedirectCallback).toHaveBeenCalledTimes(1); expect(mockSignoutPopupCallback).not.toHaveBeenCalled(); }); it('should call signoutPopupCallback and signoutSilentCallback when on the post_logout_redirect_uri in an IFrame', () => { setWindowInIFrame(); expect(mockSignoutPopupCallback).not.toHaveBeenCalled(); //@ts-ignore window.location = new URL(postLogoutRedirectUri); const client = setupOidcClient(oidcConfig); expect(mockSignoutPopupCallback).toHaveBeenCalledTimes(1); expect(mockSignoutRedirectCallback).not.toHaveBeenCalled(); }); it('login() should signInRedirect on the UserManager', async () => { const client = setupOidcClient(oidcConfig); const settings = client._.settings; Object.defineProperty(settings, 'popup_redirect_uri', { get() { return settings.popup_redirect_uri || settings.redirect_uri; }, }); Object.defineProperty(settings, 'post_logout_redirect_uri', { get() { return settings.post_logout_redirect_uri; }, }); Object.defineProperty(settings, 'redirect_uri', { get() { return settings.redirect_uri; }, }); Object.defineProperty(settings, 'silent_redirect_uri', { get() { return settings.silent_redirect_uri || settings.redirect_uri; }, }); expect(mockSigninRedirect).not.toHaveBeenCalled(); await client.login(); expect(mockSigninRedirect).toHaveBeenCalledTimes(1); }); it('logout() should call signoutRedirect on the UserManager', () => { const client = setupOidcClient(oidcConfig); expect(mockSignoutRedirect).not.toHaveBeenCalled(); client.logout(); expect(mockSignoutRedirect).toHaveBeenCalledTimes(1); }); it('token() should get the token from the user manager', async () => { const client = setupOidcClient(oidcConfig); const token = await client.token(); expect(token).toBe(user.access_token); }); it('token() should get a new token via signinSilent() with an expired user', async () => { const client = setupOidcClient(oidcConfig); const user: any = { access_token: '123', expires_in: 0, }; const userTwo: any = { access_token: '456', expires_in: 10000, }; mockSigninSilent.mockResolvedValueOnce(userTwo); mockGetUser.mockResolvedValueOnce(user); const token = await client.token(); expect(token).toBe(userTwo.access_token); }); it('token() should reject without a user', () => { const client = setupOidcClient(oidcConfig); mockGetUser.mockResolvedValue(undefined); return expect(client.token()).rejects.toHaveProperty('type', OidcErrorType.notAuthorized); }); it('token() rejects when UserManager rejects', async () => { const client = setupOidcClient(oidcConfig); const e = new Error('test error'); mockGetUser.mockRejectedValue(e); await expect(client.token()).rejects.toHaveProperty('type', OidcErrorType.unknown); }); it('token() should reject when a user does not have an access_token', () => { const client = setupOidcClient(oidcConfig); mockGetUser.mockResolvedValue({}); mockSigninSilent.mockResolvedValue({}); return expect(client.token()).rejects.toHaveProperty('type', OidcErrorType.invalidToken); }); it('account() should return the User profile', () => { const client = setupOidcClient(oidcConfig); const user: any = { access_token: '123', expires_in: 100, profile: { foo: 'bar', }, }; mockGetUser.mockResolvedValue(user); return expect(client.account()).resolves.toBe(user.profile); }); it('account() should reject when user is expired', () => { const client = setupOidcClient(oidcConfig); const user: any = { access_token: '123', expires_in: 0, profile: { foo: 'bar', }, }; mockGetUser.mockResolvedValue(user); return expect(client.account()).rejects.toHaveProperty('type', OidcErrorType.notAuthorized); }); it('account() should reject when user is not authenticated', () => { const client = setupOidcClient(oidcConfig); mockGetUser.mockResolvedValue(undefined); return expect(client.account()).rejects.toHaveProperty('type', OidcErrorType.notAuthorized); }); it('handleAuthentication() calls signinSilentCallback when on the silent_redirect_uri path in an IFrame', async () => { const client = setupOidcClient(oidcConfig); setWindowInIFrame(); //@ts-ignore window.location = new URL(redirectUri); expect(mockSigninSilentCallback).toBeCalledTimes(0); const { shouldRender } = await client.handleAuthentication(); expect(mockSigninSilentCallback).toBeCalledTimes(1); expect(mockSigninCallback).toBeCalledTimes(0); expect(shouldRender).toBe(false); }); it('handleAuthentication() calls signinCallback when on the redirect_uri path in the main viewport', async () => { const client = setupOidcClient(oidcConfig); setWindowInTop(); //@ts-ignore window.location = new URL(redirectUri); expect(mockSigninCallback).toBeCalledTimes(0); const { shouldRender } = await client.handleAuthentication(); expect(mockSigninSilentCallback).toBeCalledTimes(0); expect(mockSigninCallback).toBeCalledTimes(1); expect(shouldRender).toBe(false); }); it('handleAuthentication() redirects to appUri when on the redirect_uri path in the main viewport and appUri is configured', async () => { const client = setupOidcClient(oidcConfig); setWindowInTop(); const url = new URL(redirectUri); //@ts-ignore window.location = url; expect(window.location.href).not.toBe(appUri); await client.handleAuthentication(); expect(window.location.href).toBe(appUri); }); it('handleAuthentication() does not redirect to appUri when appUri is not configured', async () => { const client = setupOidcClient(oidcConfig); const secondClient = setupOidcClient({ ...oidcConfig, appUri: undefined, }); setWindowInTop(); const url = new URL(redirectUri); //@ts-ignore window.location = url; expect(window.location.href).not.toBe(appUri); const { shouldRender } = await secondClient.handleAuthentication(); expect(window.location.href).not.toBe(appUri); expect(shouldRender).toBe(true); }); it('handleAuthentication() returns true when the user has an acess token on normal routes', async () => { const client = setupOidcClient(oidcConfig); const url = new URL(appUri); //@ts-ignore window.location = url; const { shouldRender } = await client.handleAuthentication(); expect(shouldRender).toBe(true); }); it('handleAuthentication() redirects to login when the user does not have an access token', async () => { const client = setupOidcClient(oidcConfig); const url = new URL(appUri); mockGetUser.mockResolvedValue(undefined); //@ts-ignore window.location = url; expect(mockSigninRedirect).toBeCalledTimes(0); const { shouldRender } = await client.handleAuthentication(); expect(mockSigninRedirect).toBeCalledTimes(1); expect(shouldRender).toBe(false); }); it('handleAuthentication() rejects when token() rejects', async () => { const client = setupOidcClient(oidcConfig); const e = new Error('test error'); mockGetUser.mockRejectedValue(e); await expect(client.handleAuthentication()).rejects.toHaveProperty('type', OidcErrorType.unknown); }); it('extendHeaders() calls request.setHeaders with the authorization header when the user has a token', async () => { const client = setupOidcClient(oidcConfig); mockGetUser.mockResolvedValue(user); const expected = { Authorization: `Bearer ${user.access_token}` }; const mockSetHeaders = vitest.fn().mockResolvedValue(undefined); const req = { setHeaders: mockSetHeaders, }; expect(mockSetHeaders).not.toHaveBeenCalled(); await client.extendHeaders(req); expect(mockSetHeaders).toBeCalledTimes(1); const retrieveTokenPromise = mockSetHeaders.mock.calls[0][0]; expect(await retrieveTokenPromise).toEqual(expected); }); it('extendHeaders() is a noop when `restrict` is true in configuration', async () => { const client = setupOidcClient(oidcConfig); const mockSetHeaders = vitest.fn().mockResolvedValue(undefined); const req = { setHeaders: mockSetHeaders, }; const client2 = setupOidcClient({ ...oidcConfig, restrict: true, }); await client2.extendHeaders(req); expect(mockSetHeaders).not.toBeCalled(); }); it('extendHeaders() does nothing when the user has no token', async () => { const client = setupOidcClient(oidcConfig); mockGetUser.mockResolvedValue(undefined); const mockSetHeaders = vitest.fn().mockResolvedValue(undefined); const req = { setHeaders: mockSetHeaders, }; expect(mockSetHeaders).not.toHaveBeenCalled(); await client.extendHeaders(req); expect(mockSetHeaders).toBeCalledTimes(1); const retrieveTokenPromise = mockSetHeaders.mock.calls[0][0]; expect(await retrieveTokenPromise).toEqual(undefined); }); });