Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add extra options to withAuthenticationRequired #41

Merged
merged 1 commit into from
Jun 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => <div>Redirecting you to the login page...</div>;

const PrivateRoute = () => <div>Private</div>;

export default withAuthenticationRequired(PrivateRoute, Redirecting);
export default withAuthenticationRequired(PrivateRoute, {
// Show a message while the user waits to be redirected to the login page.
onRedirecting: () => <div>Redirecting you to the login page...</div>,
});
adamjmcgrath marked this conversation as resolved.
Show resolved Hide resolved
```

**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).
Expand Down
80 changes: 76 additions & 4 deletions __tests__/with-authentication-required.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
<Auth0Provider clientId="__test_client_id__" domain="__test_domain__">
<WrappedComponent />
Expand All @@ -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(
<Auth0Provider clientId="__test_client_id__" domain="__test_domain__">
<WrappedComponent />
</Auth0Provider>
);
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(
<Auth0Provider clientId="__test_client_id__" domain="__test_domain__">
<WrappedComponent />
</Auth0Provider>
);
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(
<Auth0Provider clientId="__test_client_id__" domain="__test_domain__">
<WrappedComponent />
</Auth0Provider>
);
await waitFor(() =>
expect(mockClient.loginWithRedirect).toHaveBeenCalledWith(
expect.objectContaining({
appState: {
returnTo: '/foo',
},
})
)
);
});
});
5 changes: 4 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
76 changes: 71 additions & 5 deletions src/with-authentication-required.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,67 @@
import React, { ComponentType, useEffect, FC } from 'react';
import { RedirectLoginOptions } from '@auth0/auth0-spa-js';
import useAuth0 from './use-auth0';

/**
* @ignore
*/
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: () => <div>Redirecting you to the login...</div>
* })
* ```
*
* 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);
Expand All @@ -16,20 +72,30 @@ const defaultOnRedirecting = (): JSX.Element => <></>;
*/
const withAuthenticationRequired = <P extends object>(
Component: ComponentType<P>,
onRedirecting: () => JSX.Element = defaultOnRedirecting
options: WithAuthenticationRequiredOptions = {}
): FC<P> => (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<void> => {
await loginWithRedirect({
appState: { returnTo: window.location.pathname },
});
await loginWithRedirect(opts);
})();
}, [isLoading, isAuthenticated, loginWithRedirect]);
}, [isLoading, isAuthenticated, loginWithRedirect, loginOptions, returnTo]);

return isAuthenticated ? <Component {...props} /> : onRedirecting();
};
Expand Down
1 change: 1 addition & 0 deletions typedoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
'Auth0ProviderOptions',
'Auth0ContextInterface',
'WithAuth0Props',
'WithAuthenticationRequiredOptions',
],
mode: 'file',
exclude: ['./src/utils.tsx', './src/reducer.tsx', './src/auth-state.tsx'],
Expand Down