Skip to content

Commit

Permalink
feat(AuthContext): added onSignInError prop to handle Authentication …
Browse files Browse the repository at this point in the history
…Error Response bjerkio#549
  • Loading branch information
mancioshell committed Mar 25, 2021
1 parent 2957e85 commit 1eae747
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 19 deletions.
29 changes: 29 additions & 0 deletions src/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { FC, useState, useEffect, useRef } from 'react';
import { UserManager, User } from 'oidc-client';
import {
Location,
Error,
AuthProviderProps,
AuthContextProps,
} from './AuthContextInterface';
Expand All @@ -27,6 +28,27 @@ export const hasCodeInUrl = (location: Location): boolean => {
);
};

export const errorInUrl = (location: Location): Error | null => {
const searchParams = new URLSearchParams(location.search);
const hashParams = new URLSearchParams(location.hash.replace('#', '?'));

const errorCode = searchParams.get('error') || hashParams.get('error');
const errorDescription =
searchParams.get('error_description') ||
hashParams.get('error_description');
const errorUri = searchParams.get('error_uri') || hashParams.get('error_uri');
const state = searchParams.get('state') || hashParams.get('state');

const error: Error = {
errorCode,
errorDescription,
errorUri,
state,
};

return errorCode ? error : null;
};

/**
* @private
* @hidden
Expand Down Expand Up @@ -75,6 +97,7 @@ export const AuthProvider: FC<AuthProviderProps> = ({
autoSignIn = true,
onBeforeSignIn,
onSignIn,
onSignInError,
onSignOut,
location = window.location,
...props
Expand Down Expand Up @@ -113,6 +136,12 @@ export const AuthProvider: FC<AuthProviderProps> = ({
return;
}

const error = errorInUrl(location);
if (error) {
onSignInError && onSignInError(error);
return;
}

const user = await userManager!.getUser();
if ((!user || user.expired) && autoSignIn) {
onBeforeSignIn && onBeforeSignIn();
Expand Down
21 changes: 16 additions & 5 deletions src/AuthContextInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ export interface Location {
hash: string;
}

export interface Error {
errorCode: string | null;
errorDescription: string | null;
errorUri: string | null;
state: string | null;
}

export interface AuthProviderSignOutProps {
/**
* Trigger a redirect of the current window to the end session endpoint
Expand Down Expand Up @@ -81,7 +88,7 @@ export interface AuthProviderProps {
*
* defaults to true
*/
loadUserInfo?:boolean;
loadUserInfo?: boolean;
/**
* The features parameter to window.open for the popup signin window
*
Expand All @@ -98,34 +105,38 @@ export interface AuthProviderProps {
*
* defaults to '_blank'
*/
popupWindowTarget?:string;
popupWindowTarget?: string;
/**
* On before sign in hook. Can be use to store the current url for use after signing in.
*
* This only gets called if autoSignIn is true
*/
onBeforeSignIn?: () => void;
/**
* On sign out hook. Can be a async function.
* On sign in hook. Can be a async function.
* @param userData User
*/
onSignIn?: (userData: User | null) => Promise<void> | void;
/**
* On sign in error hook. Can be a async function.
* @param userData User
*/
onSignInError?: (error: Error | null) => Promise<void> | void;
/**
* On sign out hook. Can be a async function.
*/
onSignOut?: (options?: AuthProviderSignOutProps) => Promise<void> | void;
}

export interface AuthContextProps {

/**
* Alias for userManager.signInRedirect
*/
signIn: (args?: unknown) => Promise<void>;
/**
* Alias for userManager.signinPopup
*/
signInPopup: () => Promise<void>
signInPopup: () => Promise<void>;
/**
* Alias for removeUser
*/
Expand Down
65 changes: 51 additions & 14 deletions src/__tests__/AuthContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { render, act, waitFor } from '@testing-library/react';
const events = {
addUserLoaded: () => undefined,
removeUserLoaded: () => undefined,
}
};

jest.mock('oidc-client', () => {
return {
Expand Down Expand Up @@ -110,9 +110,13 @@ describe('AuthContext', () => {
postLogoutRedirectUri="https://localhost"
/>,
);
await waitFor(() => expect(UserManager).toHaveBeenLastCalledWith(
expect.objectContaining({ post_logout_redirect_uri: 'https://localhost'})
));
await waitFor(() =>
expect(UserManager).toHaveBeenLastCalledWith(
expect.objectContaining({
post_logout_redirect_uri: 'https://localhost',
}),
),
);
});
it('should fall back to redirectUri when post-logout redirect URI is not given', async () => {
render(
Expand All @@ -122,9 +126,13 @@ describe('AuthContext', () => {
redirectUri="http://127.0.0.1"
/>,
);
await waitFor(() => expect(UserManager).toHaveBeenLastCalledWith(
expect.objectContaining({ post_logout_redirect_uri: 'http://127.0.0.1'})
));
await waitFor(() =>
expect(UserManager).toHaveBeenLastCalledWith(
expect.objectContaining({
post_logout_redirect_uri: 'http://127.0.0.1',
}),
),
);
});
it('should use silent redirect URI when given', async () => {
render(
Expand All @@ -135,9 +143,11 @@ describe('AuthContext', () => {
silentRedirectUri="https://localhost"
/>,
);
await waitFor(() => expect(UserManager).toHaveBeenLastCalledWith(
expect.objectContaining({ silent_redirect_uri: 'https://localhost'})
));
await waitFor(() =>
expect(UserManager).toHaveBeenLastCalledWith(
expect.objectContaining({ silent_redirect_uri: 'https://localhost' }),
),
);
});
it('should fall back to redirectUri when silent redirect URI is not given', async () => {
render(
Expand All @@ -147,9 +157,11 @@ describe('AuthContext', () => {
redirectUri="http://127.0.0.1"
/>,
);
await waitFor(() => expect(UserManager).toHaveBeenLastCalledWith(
expect.objectContaining({ silent_redirect_uri: 'http://127.0.0.1'})
));
await waitFor(() =>
expect(UserManager).toHaveBeenLastCalledWith(
expect.objectContaining({ silent_redirect_uri: 'http://127.0.0.1' }),
),
);
});

it('should get userData', async () => {
Expand Down Expand Up @@ -224,8 +236,33 @@ describe('AuthContext', () => {
/>,
);
await waitFor(() => expect(onSignIn).toHaveBeenCalled());
await waitFor(() => expect(userManager.signinCallback).toHaveBeenCalled());
});

it('should handle authentication error when onSignInError callback is passed', async () => {
const userManager = {
getUser: jest.fn(),
signinCallback: jest.fn(),
events,
} as any;
const location = {
search: '?error=invalid_request',
hash: '',
};
const onSignIn = jest.fn();
const onSignInError = jest.fn();
render(
<AuthProvider
onSignIn={onSignIn}
onSignInError={onSignInError}
userManager={userManager}
location={location}
/>,
);
await waitFor(() => expect(onSignInError).toHaveBeenCalled());
await waitFor(() => expect(onSignIn).not.toHaveBeenCalled());
await waitFor(() =>
expect(userManager.signinCallback).toHaveBeenCalled(),
expect(userManager.signinCallback).not.toHaveBeenCalled(),
);
});

Expand Down

0 comments on commit 1eae747

Please sign in to comment.