Skip to content

Commit

Permalink
[SDK-3849] Remove claimCheck from withAuthenticationRequired (#456)
Browse files Browse the repository at this point in the history
Co-authored-by: Adam Mcgrath <[email protected]>
  • Loading branch information
ewanharris and adamjmcgrath authored Dec 9, 2022
1 parent 7750429 commit 67ce84e
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 79 deletions.
21 changes: 21 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,24 @@ const App = () => {
return <div>...</div>;
};
```
## Protecting a route with a claims check
In order to protect a route with a claims check alongside an authentication required check, you can create a HOC that will wrap your component and use that to check that the user has the required claims.
```jsx
const withClaimCheck = (Component, myClaimCheckFunction, returnTo) => {
const { user } = useAuth0();
if (myClaimCheckFunction(user)) {
return <Component />
}
Router.push(returnTo);
}

const checkClaims = (claim?: User) => claim?.['https://my.app.io/jwt/claims']?.ROLE?.includes('ADMIN');

// Usage
const Page = withAuthenticationRequired(
withClaimCheck(Component, checkClaims, '/missing-roles' )
);
```
33 changes: 33 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Please review this guide thoroughly to understand the changes required to migrat
- [No more iframe fallback by default when using refresh tokens](#no-more-iframe-fallback-by-default-when-using-refresh-tokens)
- [Changes to default scopes](#changes-to-default-scopes)
- [`advancedOptions` and `defaultScope` are removed](#advancedoptions-and-defaultscope-are-removed)
- [Removal of `claimCheck` on `withAuthenticationRequired`](#removal-of-claimcheck-on-withauthenticationrequired)

## Polyfills and supported browsers

Expand Down Expand Up @@ -288,3 +289,35 @@ ReactDOM.render(
```

As you can see, `scope` becomes a merged value of the previous `defaultScope` and `scope`.

## Removal of `claimCheck` on `withAuthenticationRequired`

In v1 of Auth0-React the `withAuthenticationRequired` Higher Order Component supported a `claimCheck` property that would check the ID Token's claims and redirect the user back to the Auth0 login page if the check failed. Given that it is unlikely for most user claims to change by logging in again, it would most likely lead to users being stuck in infinite login loops. Therefore, we have chosen to remove this functionality from Auth0-React and instead provide guidance on how to achieve this so that developers can have greater control over the behavior of their application.

In v1, a claim check could be implemented as so

```js
withAuthenticationRequired(MyComponent, {
claimCheck: (claim?: User) =>
claim?.['https://my.app.io/jwt/claims']?.ROLE?.includes('ADMIN'),
});
```
Our recommendation is to create another HOC that will perform the claim check and provide this to `withAuthenticationRequired`
```jsx
const withClaimCheck = (Component, myClaimCheckFunction, returnTo) => {
const { user } = useAuth0();
if (myClaimCheckFunction(user)) {
return <Component />
}
Router.push(returnTo);
}

const checkClaims = (claim?: User) => claim?.['https://my.app.io/jwt/claims']?.ROLE?.includes('ADMIN');

// Usage
const Page = withAuthenticationRequired(
withClaimCheck(Component, checkClaims, '/missing-roles' )
);
```
62 changes: 1 addition & 61 deletions __tests__/with-authentication-required.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import '@testing-library/jest-dom/extend-expect';
import React from 'react';
import withAuthenticationRequired from '../src/with-authentication-required';
import { render, screen, waitFor, act } from '@testing-library/react';
import { Auth0Client, User } from '@auth0/auth0-spa-js';
import { Auth0Client} from '@auth0/auth0-spa-js';
import Auth0Provider from '../src/auth0-provider';
import { Auth0ContextInterface, initialContext } from '../src/auth0-context';

Expand Down Expand Up @@ -41,66 +41,6 @@ describe('withAuthenticationRequired', () => {
);
});

it('should not allow access to claims-restricted components', async () => {
const MyComponent = (): JSX.Element => <>Private</>;
const WrappedComponent = withAuthenticationRequired(MyComponent, {
claimCheck: (claims?: User) =>
claims?.['https://my.app.io/jwt/roles']?.includes('ADMIN'),
});
/**
* A user with USER and MODERATOR roles.
*/
const mockUser = {
name: '__test_user__',
'https://my.app.io/jwt/claims': {
USER: '__test_user__',
ROLE: ['USER', 'MODERATOR'],
},
};
mockClient.getUser.mockResolvedValue(mockUser);

render(
<Auth0Provider clientId="__test_client_id__" domain="__test_domain__">
<WrappedComponent />
</Auth0Provider>
);
await waitFor(() =>
expect(mockClient.loginWithRedirect).toHaveBeenCalled()
);
expect(screen.queryByText('Private')).not.toBeInTheDocument();
});

it('should allow access to restricted components when JWT claims present', async () => {
const MyComponent = (): JSX.Element => <>Private</>;
const WrappedComponent = withAuthenticationRequired(MyComponent, {
claimCheck: (claim?: User) =>
claim?.['https://my.app.io/jwt/claims']?.ROLE?.includes('ADMIN'),
});
/**
* User with ADMIN role.
*/
const mockUser = {
name: '__test_user__',
'https://my.app.io/jwt/claims': {
USER: '__test_user__',
ROLE: ['ADMIN'],
},
};
mockClient.getUser.mockResolvedValue(mockUser);

render(
<Auth0Provider clientId="__test_client_id__" domain="__test_domain__">
<WrappedComponent />
</Auth0Provider>
);
await waitFor(() =>
expect(mockClient.loginWithRedirect).not.toHaveBeenCalled()
);
await waitFor(() =>
expect(screen.getByText('Private')).toBeInTheDocument()
);
});

it('should show a custom redirecting message', async () => {
mockClient.getUser.mockResolvedValue(
Promise.resolve({ name: '__test_user__' })
Expand Down
22 changes: 4 additions & 18 deletions src/with-authentication-required.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { ComponentType, useEffect, FC } from 'react';
import { User } from '@auth0/auth0-spa-js';
import useAuth0 from './use-auth0';
import Auth0Context, {
Auth0ContextInterface,
Expand Down Expand Up @@ -64,12 +63,6 @@ export interface WithAuthenticationRequiredOptions {
* This will be merged with the `returnTo` option used by the `onRedirectCallback` handler.
*/
loginOptions?: RedirectLoginOptions;
/**
* Check the user object for JWT claims and return a boolean indicating
* whether or not they are authorized to view the component.
*/
claimCheck?: (claims?: User) => boolean;

/**
* The context to be used when calling useAuth0, this should only be provided if you are using multiple Auth0Providers
* within your application and you wish to tie a specific component to a Auth0Provider other than the Auth0Provider
Expand All @@ -94,22 +87,15 @@ const withAuthenticationRequired = <P extends object>(
const {
returnTo = defaultReturnTo,
onRedirecting = defaultOnRedirecting,
claimCheck = (): boolean => true,
loginOptions,
context = Auth0Context,
} = options;

const { user, isAuthenticated, isLoading, loginWithRedirect } =
const { isAuthenticated, isLoading, loginWithRedirect } =
useAuth0(context);

/**
* The route is authenticated if the user has valid auth and there are no
* JWT claim mismatches.
*/
const routeIsAuthenticated = isAuthenticated && claimCheck(user);

useEffect(() => {
if (isLoading || routeIsAuthenticated) {
if (isLoading || isAuthenticated) {
return;
}
const opts = {
Expand All @@ -124,13 +110,13 @@ const withAuthenticationRequired = <P extends object>(
})();
}, [
isLoading,
routeIsAuthenticated,
isAuthenticated,
loginWithRedirect,
loginOptions,
returnTo,
]);

return routeIsAuthenticated ? <Component {...props} /> : onRedirecting();
return isAuthenticated ? <Component {...props} /> : onRedirecting();
};
};

Expand Down

0 comments on commit 67ce84e

Please sign in to comment.