From ce6bb63d854fa488d239e9c81172f93bf1098459 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Wed, 17 Jun 2020 15:57:34 +0100 Subject: [PATCH] Add option to customise returnTo path for hash routing --- README.md | 8 +- .../with-authentication-required.test.tsx | 80 ++++++++++++++++++- src/index.tsx | 5 +- src/with-authentication-required.tsx | 76 ++++++++++++++++-- typedoc.js | 1 + 5 files changed, 156 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 79b2e406..6604dda7 100644 --- a/README.md +++ b/README.md @@ -129,12 +129,12 @@ Protect a route component using the `withAuthenticationRequired` higher order co import React from 'react'; import { withAuthenticationRequired } from '@auth0/auth0-react'; -// Show a message while the user waits to be redirected to the login page. -const Redirecting = () =>
Redirecting you to the login page...
; - const PrivateRoute = () =>
Private
; -export default withAuthenticationRequired(PrivateRoute, Redirecting); +export default withAuthenticationRequired(PrivateRoute, { + // Show a message while the user waits to be redirected to the login page. + onRedirecting: () =>
Redirecting you to the login page...
, +}); ``` **Note** If you are using a custom router, you will need to supply the `Auth0Provider` with a custom `onRedirectCallback` method to perform the action that returns the user to the protected page. See examples for [react-router](https://github.com/auth0/auth0-react/blob/master/EXAMPLES.md#1-protecting-a-route-in-a-react-router-dom-app), [Gatsby](https://github.com/auth0/auth0-react/blob/master/EXAMPLES.md#2-protecting-a-route-in-a-gatsby-app) and [Next.js](https://github.com/auth0/auth0-react/blob/master/EXAMPLES.md#3-protecting-a-route-in-a-nextjs-app-in-spa-mode). diff --git a/__tests__/with-authentication-required.test.tsx b/__tests__/with-authentication-required.test.tsx index 8c980727..b33de7d8 100644 --- a/__tests__/with-authentication-required.test.tsx +++ b/__tests__/with-authentication-required.test.tsx @@ -10,6 +10,7 @@ const mockClient = mocked(new Auth0Client({ client_id: '', domain: '' })); describe('withAuthenticationRequired', () => { it('should block access to a private component when not authenticated', async () => { + mockClient.isAuthenticated.mockResolvedValue(false); const MyComponent = (): JSX.Element => <>Private; const WrappedComponent = withAuthenticationRequired(MyComponent); render( @@ -42,10 +43,9 @@ describe('withAuthenticationRequired', () => { mockClient.isAuthenticated.mockResolvedValue(true); const MyComponent = (): JSX.Element => <>Private; const OnRedirecting = (): JSX.Element => <>Redirecting; - const WrappedComponent = withAuthenticationRequired( - MyComponent, - OnRedirecting - ); + const WrappedComponent = withAuthenticationRequired(MyComponent, { + onRedirecting: OnRedirecting, + }); render( @@ -59,4 +59,76 @@ describe('withAuthenticationRequired', () => { ); expect(screen.queryByText('Redirecting')).not.toBeInTheDocument(); }); + + it('should pass additional options on to loginWithRedirect', async () => { + mockClient.isAuthenticated.mockResolvedValue(false); + const MyComponent = (): JSX.Element => <>Private; + const WrappedComponent = withAuthenticationRequired(MyComponent, { + loginOptions: { + fragment: 'foo', + }, + }); + render( + + + + ); + await waitFor(() => + expect(mockClient.loginWithRedirect).toHaveBeenCalledWith( + expect.objectContaining({ + fragment: 'foo', + }) + ) + ); + }); + + it('should merge additional appState with the returnTo', async () => { + mockClient.isAuthenticated.mockResolvedValue(false); + const MyComponent = (): JSX.Element => <>Private; + const WrappedComponent = withAuthenticationRequired(MyComponent, { + loginOptions: { + appState: { + foo: 'bar', + }, + }, + returnTo: '/baz', + }); + render( + + + + ); + await waitFor(() => + expect(mockClient.loginWithRedirect).toHaveBeenCalledWith( + expect.objectContaining({ + appState: { + foo: 'bar', + returnTo: '/baz', + }, + }) + ) + ); + }); + + it('should accept a returnTo function', async () => { + mockClient.isAuthenticated.mockResolvedValue(false); + const MyComponent = (): JSX.Element => <>Private; + const WrappedComponent = withAuthenticationRequired(MyComponent, { + returnTo: () => '/foo', + }); + render( + + + + ); + await waitFor(() => + expect(mockClient.loginWithRedirect).toHaveBeenCalledWith( + expect.objectContaining({ + appState: { + returnTo: '/foo', + }, + }) + ) + ); + }); }); diff --git a/src/index.tsx b/src/index.tsx index 7cbcd529..d61c8b45 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,10 @@ export { } from './auth0-provider'; export { default as useAuth0 } from './use-auth0'; export { default as withAuth0, WithAuth0Props } from './with-auth0'; -export { default as withAuthenticationRequired } from './with-authentication-required'; +export { + default as withAuthenticationRequired, + WithAuthenticationRequiredOptions, +} from './with-authentication-required'; export { default as Auth0Context, Auth0ContextInterface, diff --git a/src/with-authentication-required.tsx b/src/with-authentication-required.tsx index 7dacc704..71b31c39 100644 --- a/src/with-authentication-required.tsx +++ b/src/with-authentication-required.tsx @@ -1,4 +1,5 @@ import React, { ComponentType, useEffect, FC } from 'react'; +import { RedirectLoginOptions } from '@auth0/auth0-spa-js'; import useAuth0 from './use-auth0'; /** @@ -6,6 +7,61 @@ import useAuth0 from './use-auth0'; */ const defaultOnRedirecting = (): JSX.Element => <>; +/** + * @ignore + */ +const defaultReturnTo = (): string => + `${window.location.pathname}${window.location.search}`; + +/** + * Options for the withAuthenticationRequired Higher Order Component + */ +export interface WithAuthenticationRequiredOptions { + /** + * ```js + * withAuthenticationRequired(Profile, { + * returnTo: '/profile' + * }) + * ``` + * + * or + * + * ```js + * withAuthenticationRequired(Profile, { + * returnTo: () => window.location.hash.substr(1) + * }) + * ``` + * + * Add a path for the `onRedirectCallback` handler to return the user to after login. + */ + returnTo?: string | (() => string); + /** + * ```js + * withAuthenticationRequired(Profile, { + * onRedirecting: () =>
Redirecting you to the login...
+ * }) + * ``` + * + * Render a message to show that the user is being redirected to the login. + */ + onRedirecting?: () => JSX.Element; + /** + * ```js + * withAuthenticationRequired(Profile, { + * loginOptions: { + * appState: { + * customProp: 'foo' + * } + * } + * }) + * ``` + * + * Pass additional login options, like extra `appState` to the login page. + * This will be merged with the `returnTo` option used by the `onRedirectCallback` handler. + */ + loginOptions?: RedirectLoginOptions; +} + /** * ```js * const MyProtectedComponent = withAuthenticationRequired(MyComponent); @@ -16,20 +72,30 @@ const defaultOnRedirecting = (): JSX.Element => <>; */ const withAuthenticationRequired =

( Component: ComponentType

, - onRedirecting: () => JSX.Element = defaultOnRedirecting + options: WithAuthenticationRequiredOptions = {} ): FC

=> (props: P): JSX.Element => { const { isAuthenticated, isLoading, loginWithRedirect } = useAuth0(); + const { + returnTo = defaultReturnTo, + onRedirecting = defaultOnRedirecting, + loginOptions = {}, + } = options; useEffect(() => { if (isLoading || isAuthenticated) { return; } + const opts = { + ...loginOptions, + appState: { + ...loginOptions.appState, + returnTo: typeof returnTo === 'function' ? returnTo() : returnTo, + }, + }; (async (): Promise => { - await loginWithRedirect({ - appState: { returnTo: window.location.pathname }, - }); + await loginWithRedirect(opts); })(); - }, [isLoading, isAuthenticated, loginWithRedirect]); + }, [isLoading, isAuthenticated, loginWithRedirect, loginOptions, returnTo]); return isAuthenticated ? : onRedirecting(); }; diff --git a/typedoc.js b/typedoc.js index 5f87c5ce..ebeacdda 100644 --- a/typedoc.js +++ b/typedoc.js @@ -10,6 +10,7 @@ module.exports = { 'Auth0ProviderOptions', 'Auth0ContextInterface', 'WithAuth0Props', + 'WithAuthenticationRequiredOptions', ], mode: 'file', exclude: ['./src/utils.tsx', './src/reducer.tsx', './src/auth-state.tsx'],