diff --git a/.circleci/config.yml b/.circleci/config.yml index 20c5021b..9af1ecb5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,11 +5,12 @@ orbs: parameters: docker_image: type: string - default: cimg/node:16.17-browsers + default: cimg/node:18.12-browsers jobs: build: docker: - image: << pipeline.parameters.docker_image >> + resource_class: xlarge steps: - checkout - restore_cache: @@ -49,7 +50,7 @@ jobs: - restore_cache: key: dependencies-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "examples/cra-react-router/package.json" }}-{{ checksum "examples/gatsby-app/package.json" }}-{{ checksum "examples/nextjs-app/package.json" }}-{{ checksum "examples/users-api/package-lock.json" }} - run: npm ci - - run: npx concurrently --raw --kill-others --success first "npm:start" "wait-on http://127.0.0.1:3000/ && browserstack-cypress run --build-name $CIRCLE_BRANCH --specs "cypress/integration/smoke-bs.test.ts"" + - run: npx concurrently --raw --kill-others --success first "npm:start" "wait-on http://127.0.0.1:3000/ && browserstack-cypress run --build-name $CIRCLE_BRANCH --no-wrap --specs "cypress/integration/smoke-bs.test.ts"" workflows: Build and Test: @@ -61,6 +62,8 @@ workflows: context: - browserstack-env - ship/node-publish: + publish-command: npm publish --tag beta + node-version: 18.12.1 context: - publish-npm - publish-gh @@ -68,5 +71,6 @@ workflows: branches: only: - master + - beta requires: - browserstack diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 60245228..420c557b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,12 +1,12 @@ -name: "CodeQL" +name: 'CodeQL' on: push: - branches: [ "master", "beta" ] + branches: ['master', 'beta'] pull_request: - branches: [ "master" ] + branches: ['master'] schedule: - - cron: "37 10 * * 2" + - cron: '37 10 * * 2' jobs: analyze: @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ javascript ] + language: [javascript] steps: - name: Checkout @@ -38,4 +38,4 @@ jobs: - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 with: - category: "/language:${{ matrix.language }}" + category: '/language:${{ matrix.language }}' diff --git a/CHANGELOG.md b/CHANGELOG.md index fef24882..66e4323d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Change Log +## [v2.0.0-beta.0](https://github.com/auth0/auth0-react/tree/v2.0.0-beta.0) (2022-12-12) + +Auth0-React v2 includes many significant changes compared to v1: + +- Removal of polyfills from bundles +- Introduction of `authorizationParams` and `logoutParams` for properties sent to Auth0 +- Removal of `buildAuthorizeUrl` and `buildLogoutUrl` +- Removal of `redirectMethod` on `loginWithRedirect` in favour of `openUrl` +- Removal of `localOnly` from `logout` in favour of `openUrl` +- Renaming of `ignoreCache` to `cacheMode` and introduction of `cache-only` +- Use `application/x-www-form-urlencoded` by default +- Do not fallback to refreshing tokens via iframe by default +- Changes to default scopes and removal of `advancedOptions.defaultScope` +- Removal of `claimCheck` on `withAuthenticationRequired` + +As with any major version bump, v2 of Auth0-React contains a set of breaking changes. **Please review [the migration guide](./MIGRATION_GUIDE.md) thoroughly to understand the changes required to migrate your application to v2.** + ## [v1.12.1](https://github.com/auth0/auth0-react/tree/v1.12.1) (2023-01-12) [Full Changelog](https://github.com/auth0/auth0-react/compare/v1.12.0...v1.12.1) diff --git a/EXAMPLES.md b/EXAMPLES.md index 86f5070e..3c1f1dfb 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -62,8 +62,10 @@ const Posts = () => { (async () => { try { const token = await getAccessTokenSilently({ - audience: 'https://api.example.com/', - scope: 'read:posts', + authorizationParams: { + audience: 'https://api.example.com/', + scope: 'read:posts', + }, }); const response = await fetch('https://api.example.com/posts', { headers: { @@ -72,6 +74,7 @@ const Posts = () => { }); setPosts(await response.json()); } catch (e) { + // Handle errors such as `login_required` and `consent_required` by re-prompting for a login console.error(e); } })(); @@ -132,7 +135,9 @@ export default function App() { @@ -171,8 +176,10 @@ export const wrapRootElement = ({ element }) => { {element} @@ -228,10 +235,11 @@ class MyApp extends App { @@ -265,104 +273,6 @@ export default withAuthenticationRequired(Profile); See [Next.js example app](./examples/nextjs-app) -## Create a `useApi` hook for accessing protected APIs with an access token. - -```js -// use-api.js -import { useEffect, useState } from 'react'; -import { useAuth0 } from '@auth0/auth0-react'; - -export const useApi = (url, options = {}) => { - const { getAccessTokenSilently } = useAuth0(); - const [state, setState] = useState({ - error: null, - loading: true, - data: null, - }); - const [refreshIndex, setRefreshIndex] = useState(0); - - useEffect(() => { - (async () => { - try { - const { audience, scope, ...fetchOptions } = options; - const accessToken = await getAccessTokenSilently({ audience, scope }); - const res = await fetch(url, { - ...fetchOptions, - headers: { - ...fetchOptions.headers, - // Add the Authorization header to the existing headers - Authorization: `Bearer ${accessToken}`, - }, - }); - setState({ - ...state, - data: await res.json(), - error: null, - loading: false, - }); - } catch (error) { - setState({ - ...state, - error, - loading: false, - }); - } - })(); - }, [refreshIndex]); - - return { - ...state, - refresh: () => setRefreshIndex(refreshIndex + 1), - }; -}; -``` - -Then use it for accessing protected APIs from your components: - -```jsx -// users.js -import { useApi } from './use-api'; - -export const Profile = () => { - const opts = { - audience: 'https://api.example.com/', - scope: 'read:users', - }; - const { login, getAccessTokenWithPopup } = useAuth0(); - const { - loading, - error, - refresh, - data: users, - } = useApi('https://api.example.com/users', opts); - const getTokenAndTryAgain = async () => { - await getAccessTokenWithPopup(opts); - refresh(); - }; - if (loading) { - return
Loading...
; - } - if (error) { - if (error.error === 'login_required') { - return ; - } - if (error.error === 'consent_required') { - return ( - - ); - } - return
Oops {error.message}
; - } - return ( -
    - {users.map((user, index) => { - return
  • {user}
  • ; - })} -
- ); -}; -``` - ## Use with Auth0 organizations [Organizations](https://auth0.com/docs/organizations) is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications. Note that Organizations is currently only available to customers on our Enterprise and Startup subscription plans. @@ -375,8 +285,10 @@ ReactDOM.render( @@ -406,3 +318,24 @@ const App = () => { return
...
; }; ``` + +## 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 + } + 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' ) +); +``` \ No newline at end of file diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 00000000..1c965e59 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,323 @@ +# Auth0-React v2 Migration Guide + +With the v2 release of Auth0-React we have updated to the latest version of Auth0-SPA-JS which brings improvements to performance and developer experience. However, as with any major version bump there are some breaking changes that will impact your applications. + +Please review this guide thoroughly to understand the changes required to migrate your application to v2. + +- [Polyfills and supported browsers](#polyfills-and-supported-browsers) +- [Public API Changes](#public-api-changes) + - [Introduction of `authorizationParams`](#introduction-of-authorizationparams) + - [Introduction of `logoutParams`](#introduction-of-logoutparams) + - [`buildAuthorizeUrl` has been removed](#buildauthorizeurl-has-been-removed) + - [`buildLogoutUrl` has been removed](#buildlogouturl-has-been-removed) + - [`redirectMethod` has been removed from `loginWithRedirect`](#redirectmethod-has-been-removed-from-loginwithredirect) + - [`localOnly` logout has been removed, and replaced by `openUrl`](#localonly-logout-has-been-removed-and-replaced-by-openUrl) + - [`ignoreCache` on `getAccessTokenSilently` has been removed and replace with `cacheMode`](#ignorecache-on-getaccesstokensilently-has-been-removed-and-replace-with-cachemode) + - [`application/x-www-form-urlencoded` used by default instead of `application/json`](#applicationx-www-form-urlencoded-used-by-default-instead-of-applicationjson) + - [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 + +As [Microsoft has dropped support for IE11](https://blogs.windows.com/windowsexperience/2022/06/15/internet-explorer-11-has-retired-and-is-officially-out-of-support-what-you-need-to-know), Auth0-SPA-JS no longer includes any polyfills in its bundle, as all of these polyfills were for IE11. Therefore, Auth0-React no longer supports IE11 in v2. + +The following is the list of polyfills that were removed. If your applications requires any of these, you will need to include them in your application: + +- [AbortController](https://www.npmjs.com/package/abortcontroller-polyfill): Used to polyfill [AbortController on IE11, Opera Mini, and some mobile-specific browsers](https://caniuse.com/?search=abortcontroller). +- [Promise](https://www.npmjs.com/package/promise-polyfill): Used to polyfill [Promise on IE11 and Opera Mini](https://caniuse.com/promises) +- [Core-js](https://www.npmjs.com/package/core-js): Used to polyfill a couple of things, also mostly on IE11, Opera Mini, and some mobile-specific browsers: + - [string/startsWith](https://caniuse.com/?search=startsWith) + - [string/includes](https://caniuse.com/es6-string-includes) + - [set](https://caniuse.com/mdn-javascript_builtins_set) + - [symbol](https://caniuse.com/mdn-javascript_builtins_symbol) + - [array/from](https://caniuse.com/mdn-javascript_builtins_array_from) + - [array/includes](https://caniuse.com/array-includes) +- [fast-text-encoding](https://www.npmjs.com/package/fast-text-encoding): Used to polyfill TextEncoder and TextDecoder on IE11 and Opera Mini. +- [unfetch](https://www.npmjs.com/package/unfetch): Used to [ponyfill fetch on IE11](https://caniuse.com/?search=fetch). + +By removing these polyfills, the bundle size for Auth0-SPA-JS has dropped 60%. As this is a core dependency of Auth0-React this ensures your users have a better experience when integrating Auth0 into your application using Auth0-React. + +## Public API Changes + +With the release of this new major version, some changes were made that impact the public API of Auth0-React. If you are using TypeScript, these should be flagged for you. However we still recommend reviewing this list thoroughly as some changes are changes in behavior. + +### Introduction of `authorizationParams` + +A breaking change that will affect pretty much everyone is the introduction of `authorizationParams`, a more structured approach to providing parameters - including custom parameters - to Auth0. + +In v1, objects passed to our methods are always a mix of properties used for configuring the SDK and properties with the sole purpose to pass through to Auth0. + +```jsx +ReactDOM.render( + + + , + document.getElementById('app') +); +``` + +With v2 of our SDK, we have improved the API by separating those properties used to configure the SDK, from properties that are sent to Auth0. The SDK configuration properties will stay on the root, while any property that should be sent to Auth0 should be set on `authorizationParams`. + +```jsx +ReactDOM.render( + + + , + document.getElementById('app') +); +``` + +The above changes affect the following methods: + +- loginWithRedirect +- loginWithPopup +- getAccessTokenWithPopup +- getAccessTokenSilently + +### Changes to parameter casing + +With the move to placing Auth0 specific properties in the `authorizationParams` object, these properties will now use the casing used by the Auth0 API. This specifically impacts `redirectUri` and `maxAge` as they previously were camelCase but are now kebab-case: + +- `redirectUri` is now `redirect_uri` +- `maxAge` is now `max_age` + +### Introduction of `logoutParams` + +In v1, `logout` can be called with an object containing a number of properties, both a mix between properties used to configure the SDK as well as those used to pass through to Auth0. + +With v2, logout now takes an object that can only contain two properties, `clientId` and `logoutParams`. + +Any property, apart from clientId, that you used to set on the root of the object passed to `logout` should now be set on `logoutParams` instead. + +```ts +await logout({ + clientId: '', + logoutParams: { + federated: true / false, + returnTo: '', + any_custom_property: 'value', + }, +}); +``` + +### `buildAuthorizeUrl` has been removed + +In v1, we introduced `buildAuthorizeUrl` for applications that couldn’t rely on `window.location.assign` to redirect to Auth0 when calling `loginWithRedirect`, a typical example is for people using v1 of our SDK with Ionic: + +```ts +const { buildAuthorizeUrl } = useAuth0(); +const url = buildAuthorizeUrl(); +await Browser.open({ url }); +``` + +With v2, we have removed `buildAuthorizeUrl`. This means that the snippet above will no longer work, and you should update your code by using `openUrl` instead. + +```ts +const { loginWithRedirect } = useAuth0(); + +await loginWithRedirect({ + async openUrl(url) { + await Browser.open({ url }); + }, +}); +``` + +The above snippet aligns more with the intent, using our SDK to login but relying on Capacitor (or any other external browser) to do the actual redirect. + +### `buildLogoutUrl` has been removed + +In v1, we introduced `buildLogoutUrl` for applications that are unable to use `window.location.assign` when logging out from Auth0, a typical example is for people using v1 of our SDK with Ionic: + +```ts +const { buildLogoutUrl } = useAuth0(); +const url = buildLogoutUrl(); +await Browser.open({ url }); +``` + +With v2, `buildLogoutUrl` has been removed and you should update any code that is not able to rely on `window.location.assign` to use `openUrl` when calling `logout`: + +```ts +const { logout } = useAuth0(); + +client.logout({ + async openUrl(url) { + await Browser.open({ url }); + }, +}); +``` + +This method was removed because, when using our SDK, the logout method is expected to be called regardless of the browser used. Instead of calling both `logout` and `buildLogoutUrl`, you can now change the redirect behaviour when calling `logout`. + +### `redirectMethod` has been removed from `loginWithRedirect` + +In v1, `loginWithRedirect` takes a `redirectMethod` that can be set to any of `assign` and `replace`, allowing the users to control whether the SDK should redirect using `window.location.assign` or `window.location.replace`. + +```ts +const { loginWithRedirect } = useAuth0(); +await loginWithRedirect({ + redirectMethod: 'replace', +}); +``` + +With the release of v2, we have removed `redirectMethod`. If you want to use anything but `window.location.assign` to handle the redirect to Auth0, you should implement `openUrl`: + +```ts +const { loginWithRedirect } = useAuth0(); +await loginWithRedirect({ + async openUrl(url) { + window.location.replace(url); + }, +}); +``` + +### `localOnly` logout has been removed, and replaced by `openUrl` + +When calling the SDK's `logout` method, v1 supports the ability to specify `localOnly: true`, ensuring our SDK does not redirect to Auth0 but only clears the user state from the application. + +With v2, we have removed `localOnly`, but instead provided a way for developers to take control of the redirect behavior by setting `openUrl`. In order to achieve localOnly logout with v2, you should set `openUrl` to `false`. + +```ts +const { logout } = useAuth0(); +await logout({ + openUrl: false, +}); +``` + +### `ignoreCache` on `getAccessTokenSilently` has been removed and replace with `cacheMode` + +In v1, users can bypass the cache when calling `getAccessTokenSilently` by passing ignoreCache: true. + +```ts +const { getAccessTokenSilently } = useAuth0(); +const token = await getAccessTokenSilently({ ignoreCache: true }); +``` + +With v2, we wanted to add the ability to only retrieve a token from the cache, without contacting Auth0 if no token was found. To do so, we have removed the `ignoreCache` property and replaced it with `cacheMode` that can take any of the following three values: + +- **on** (default): read from the cache caching, but fall back to Auth0 as needed +- **off**: ignore the cache, instead always call Auth0 +- **cache-only**: read from the cache, don’t fall back to Auth0 + +Any code that was previously using `ignoreCache: true` should be changed to use `cacheMode: 'off'`: + +```ts +const { getAccessTokenSilently } = useAuth0(); +const token = await getAccessTokenSilently({ cacheMode: 'off' }); +``` + +### `application/x-www-form-urlencoded` used by default instead of `application/json` + +Auth0’s token endpoint supports both `application/x-www-form-urlencoded` and `application/json` content types. However, using `application/x-www-form-urlencoded` provides a small performance benefit. + +In v1 of the SDK, the default was to send request to /oauth/token using json, allowing to opt-in to use x-www-form-urlencoded by setting the `useFormData` flag to _true_. + +With v2, we have flipped the default value for `useFormData` to **true**, meaning we will be sending requests to Auth0’s token endpoint using `application/x-www-form-urlencoded` as the content type by default. + +> :warning: This can affect existing rules and actions, and it’s important to ensure all your actions still work as expected after upgrading to v2. +> To restore the original behaviour, you can set `useFormData` to **false**, and your rules and actions should continue to work as before. + +### No more iframe fallback by default when using refresh tokens + +When using refresh tokens in v1, we fall back to using iframes whenever a refresh token exchange would fail. This has caused problems before in environments that do not support iframes, and we have specifically introduced `useRefreshTokensFallback` to be able to opt-out of falling back to iframes in the case a refresh_grant fails. + +With v2, we have flipped the default value for `useRefreshTokensFallback` to false we do not fall back to using iframes by default when `useRefreshTokens` is `true`, and the refresh token exchange fails. + +If you want to restore the original behaviour, and still fall back to iframes when the refresh token exchange fails, you can set `useRefreshTokensFallback` to true. + +### Changes to default scopes + +Our SDK defaults to requesting `openid profile email` as the scopes. However, when explicitly setting the `scope`, v1 would still include `openid profile email` as well. + +With v2, we have reworked this to still default to `openid profile email` when the scope property has been omitted, but only include `openid` when the user sets a scope explicitly. + +This means that the following code in v1: + +```jsx +ReactDOM.render( + + + , + document.getElementById('app') +); +``` + +Needs to be updated to explicitly include the `profile email` scopes to achieve the same in v2: + +```jsx +ReactDOM.render( + + + , + document.getElementById('app') +); +``` + +#### `advancedOptions` and `defaultScope` are removed + +With v1 of our SDK, users can set both `scope: '...'` and `advancedOptions: { defaultScope: '...' }` when configuring the `Auth0Provider`. As this has proven to be confusing, with v2 we have decided to drop `defaultScope` altogether. As this was its own property, we have also removed `advancedOptions`. Any code that used to rely on `defaultScope` will need to move those scopes into `scope` instead: + +```jsx +ReactDOM.render( + + + , + document.getElementById('app') +); +``` + +Will need to move those scopes into `scope` instead: + +```jsx +ReactDOM.render( + + + , + document.getElementById('app') +); +``` + +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 + } + 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' ) +); +``` \ No newline at end of file diff --git a/README.md b/README.md index 8f27f894..3bc0fb1c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ![Auth0 SDK for React Single Page Applications](https://cdn.auth0.com/website/sdks/banners/auth0-react-banner.png) +> :warning: Please be aware that v2 is currently in [**Beta**](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages). Whilst we encourage you to test the update within your applications, we do no recommend using this version in production yet. Please follow the [migration guide](./MIGRATION_GUIDE.md) when updating your application. + [![npm](https://img.shields.io/npm/v/@auth0/auth0-react.svg?style=flat)](https://www.npmjs.com/package/@auth0/auth0-react) [![codecov](https://img.shields.io/codecov/c/github/auth0/auth0-react/master.svg?style=flat)](https://codecov.io/gh/auth0/auth0-react) ![Downloads](https://img.shields.io/npm/dw/@auth0/auth0-react) @@ -23,13 +25,13 @@ Using [npm](https://npmjs.org/) ```bash -npm install @auth0/auth0-react +npm install @auth0/auth0-react@beta ``` Using [yarn](https://yarnpkg.com/) ```bash -yarn add @auth0/auth0-react +yarn add @auth0/auth0-react@beta ``` ### Configure Auth0 @@ -69,7 +71,9 @@ ReactDOM.render( , diff --git a/__tests__/auth-provider.test.tsx b/__tests__/auth-provider.test.tsx index 3c6d8203..e2293bb2 100644 --- a/__tests__/auth-provider.test.tsx +++ b/__tests__/auth-provider.test.tsx @@ -14,7 +14,7 @@ import pkg from '../package.json'; import { createWrapper } from './helpers'; import { Auth0Provider, useAuth0 } from '../src'; -const clientMock = jest.mocked(new Auth0Client({ client_id: '', domain: '' })); +const clientMock = jest.mocked(new Auth0Client({ clientId: '', domain: '' })); describe('Auth0Provider', () => { afterEach(() => { @@ -35,9 +35,11 @@ describe('Auth0Provider', () => { const opts = { clientId: 'foo', domain: 'bar', - redirectUri: 'baz', - maxAge: 'qux', - extra_param: '__test_extra_param__', + authorizationParams: { + redirect_uri: 'baz', + max_age: 'qux', + extra_param: '__test_extra_param__', + }, }; const wrapper = createWrapper(opts); const { waitForNextUpdate } = renderHook(() => useContext(Auth0Context), { @@ -45,11 +47,13 @@ describe('Auth0Provider', () => { }); expect(Auth0Client).toHaveBeenCalledWith( expect.objectContaining({ - client_id: 'foo', + clientId: 'foo', domain: 'bar', - redirect_uri: 'baz', - max_age: 'qux', - extra_param: '__test_extra_param__', + authorizationParams: { + redirect_uri: 'baz', + max_age: 'qux', + extra_param: '__test_extra_param__', + }, }) ); await waitForNextUpdate(); @@ -228,41 +232,6 @@ describe('Auth0Provider', () => { expect(result.current.error).not.toBeDefined(); }); - it('should call through to buildAuthorizeUrl method', async () => { - const wrapper = createWrapper(); - const { waitForNextUpdate, result } = renderHook( - () => useContext(Auth0Context), - { wrapper } - ); - await waitForNextUpdate(); - expect(result.current.buildAuthorizeUrl).toBeInstanceOf(Function); - - await result.current.buildAuthorizeUrl({ - redirectUri: '__redirect_uri__', - }); - expect(clientMock.buildAuthorizeUrl).toHaveBeenCalledWith({ - redirect_uri: '__redirect_uri__', - }); - }); - - it('should call through to buildLogoutUrl method', async () => { - const wrapper = createWrapper(); - const { waitForNextUpdate, result } = renderHook( - () => useContext(Auth0Context), - { wrapper } - ); - await waitForNextUpdate(); - expect(result.current.buildLogoutUrl).toBeInstanceOf(Function); - - const logoutOptions = { - returnTo: '/', - client_id: 'blah', - federated: false, - }; - result.current.buildLogoutUrl(logoutOptions); - expect(clientMock.buildLogoutUrl).toHaveBeenCalledWith(logoutOptions); - }); - it('should login with a popup', async () => { clientMock.getUser.mockResolvedValue(undefined); const wrapper = createWrapper(); @@ -320,10 +289,14 @@ describe('Auth0Provider', () => { await waitForNextUpdate(); expect(result.current.loginWithRedirect).toBeInstanceOf(Function); await result.current.loginWithRedirect({ - redirectUri: '__redirect_uri__', + authorizationParams: { + redirect_uri: '__redirect_uri__', + }, }); expect(clientMock.loginWithRedirect).toHaveBeenCalledWith({ - redirect_uri: '__redirect_uri__', + authorizationParams: { + redirect_uri: '__redirect_uri__', + }, }); }); @@ -346,9 +319,11 @@ describe('Auth0Provider', () => { expect(result.current.user).toBe(user); }); - it('should update state for local logouts', async () => { + it('should update state when using openUrl', async () => { const user = { name: '__test_user__' }; clientMock.getUser.mockResolvedValue(user); + // get logout to return a Promise to simulate async cache. + clientMock.logout.mockResolvedValue(); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -356,22 +331,22 @@ describe('Auth0Provider', () => { ); await waitForNextUpdate(); expect(result.current.isAuthenticated).toBe(true); - expect(result.current.user).toBe(user); - act(() => { - result.current.logout({ localOnly: true }); - }); - expect(clientMock.logout).toHaveBeenCalledWith({ - localOnly: true, + await act(async () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + await result.current.logout({ openUrl: async () => {} }); }); expect(result.current.isAuthenticated).toBe(false); - expect(result.current.user).toBeUndefined(); }); - it('should update state for local logouts with async cache', async () => { + it('should wait for logout with async cache', async () => { const user = { name: '__test_user__' }; + const logoutSpy = jest.fn(); clientMock.getUser.mockResolvedValue(user); // get logout to return a Promise to simulate async cache. - clientMock.logout.mockResolvedValue(); + clientMock.logout.mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + logoutSpy(); + }); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -380,20 +355,14 @@ describe('Auth0Provider', () => { await waitForNextUpdate(); expect(result.current.isAuthenticated).toBe(true); await act(async () => { - await result.current.logout({ localOnly: true }); + await result.current.logout(); }); - expect(result.current.isAuthenticated).toBe(false); + expect(logoutSpy).toHaveBeenCalled(); }); - it('should wait for logout with async cache', async () => { + it('should update state for openUrl false', async () => { const user = { name: '__test_user__' }; - const logoutSpy = jest.fn(); clientMock.getUser.mockResolvedValue(user); - // get logout to return a Promise to simulate async cache. - clientMock.logout.mockImplementation(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)); - logoutSpy(); - }); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -401,10 +370,16 @@ describe('Auth0Provider', () => { ); await waitForNextUpdate(); expect(result.current.isAuthenticated).toBe(true); - await act(async () => { - await result.current.logout(); + expect(result.current.user).toBe(user); + act(() => { + result.current.logout({ openUrl: false }); }); - expect(logoutSpy).toHaveBeenCalled(); + expect(clientMock.logout).toHaveBeenCalledWith({ + openUrl: false, + }); + await waitForNextUpdate(); + expect(result.current.isAuthenticated).toBe(false); + expect(result.current.user).toBeUndefined(); }); it('should provide a getAccessTokenSilently method', async () => { @@ -479,7 +454,7 @@ describe('Auth0Provider', () => { it('should update auth state after getAccessTokenSilently', async () => { clientMock.getTokenSilently.mockReturnThis(); - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + clientMock.getUser.mockResolvedValue({ name: 'foo' }); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -488,7 +463,7 @@ describe('Auth0Provider', () => { await waitForNextUpdate(); expect(result.current.user?.name).toEqual('foo'); - clientMock.getUser.mockResolvedValue({ name: 'bar', updated_at: '2' }); + clientMock.getUser.mockResolvedValue({ name: 'bar' }); await act(async () => { await result.current.getAccessTokenSilently(); }); @@ -497,7 +472,7 @@ describe('Auth0Provider', () => { it('should update auth state after getAccessTokenSilently fails', async () => { clientMock.getTokenSilently.mockReturnThis(); - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + clientMock.getUser.mockResolvedValue({ name: 'foo' }); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -518,7 +493,8 @@ describe('Auth0Provider', () => { it('should ignore same user after getAccessTokenSilently', async () => { clientMock.getTokenSilently.mockReturnThis(); - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + const userObject = { name: 'foo' }; + clientMock.getUser.mockResolvedValue(userObject); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -527,7 +503,7 @@ describe('Auth0Provider', () => { await waitForNextUpdate(); const prevUser = result.current.user; - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + clientMock.getUser.mockResolvedValue(userObject); await act(async () => { await result.current.getAccessTokenSilently(); }); @@ -536,7 +512,7 @@ describe('Auth0Provider', () => { it('should not update getAccessTokenSilently after auth state change', async () => { clientMock.getTokenSilently.mockReturnThis(); - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + clientMock.getUser.mockResolvedValue({ name: 'foo' }); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -545,7 +521,7 @@ describe('Auth0Provider', () => { await waitForNextUpdate(); const memoized = result.current.getAccessTokenSilently; expect(result.current.user?.name).toEqual('foo'); - clientMock.getUser.mockResolvedValue({ name: 'bar', updated_at: '2' }); + clientMock.getUser.mockResolvedValue({ name: 'bar' }); await act(async () => { await result.current.getAccessTokenSilently(); }); @@ -598,7 +574,7 @@ describe('Auth0Provider', () => { it('should update auth state after getAccessTokenWithPopup', async () => { clientMock.getTokenSilently.mockReturnThis(); - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + clientMock.getUser.mockResolvedValue({ name: 'foo' }); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -607,7 +583,7 @@ describe('Auth0Provider', () => { await waitForNextUpdate(); const prevUser = result.current.user; - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '2' }); + clientMock.getUser.mockResolvedValue({ name: 'foo' }); await act(async () => { await result.current.getAccessTokenWithPopup(); }); @@ -616,7 +592,7 @@ describe('Auth0Provider', () => { it('should update auth state after getAccessTokenWithPopup fails', async () => { clientMock.getTokenSilently.mockReturnThis(); - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + clientMock.getUser.mockResolvedValue({ name: 'foo' }); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -639,7 +615,8 @@ describe('Auth0Provider', () => { it('should ignore same auth state after getAccessTokenWithPopup', async () => { clientMock.getTokenSilently.mockReturnThis(); - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + const userObject = { name: 'foo' }; + clientMock.getUser.mockResolvedValue(userObject); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -648,7 +625,7 @@ describe('Auth0Provider', () => { await waitForNextUpdate(); const prevState = result.current; - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + clientMock.getUser.mockResolvedValue(userObject); await act(async () => { await result.current.getAccessTokenWithPopup(); }); @@ -753,7 +730,7 @@ describe('Auth0Provider', () => { it('should update auth state after handleRedirectCallback', async () => { clientMock.handleRedirectCallback.mockReturnThis(); - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + clientMock.getUser.mockResolvedValue({ name: 'foo' }); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -762,7 +739,7 @@ describe('Auth0Provider', () => { await waitForNextUpdate(); const prevUser = result.current.user; - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '2' }); + clientMock.getUser.mockResolvedValue({ name: 'foo' }); await act(async () => { await result.current.handleRedirectCallback(); }); @@ -771,7 +748,7 @@ describe('Auth0Provider', () => { it('should update auth state after handleRedirectCallback fails', async () => { clientMock.handleRedirectCallback.mockReturnThis(); - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + clientMock.getUser.mockResolvedValue({ name: 'foo' }); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -794,7 +771,8 @@ describe('Auth0Provider', () => { it('should ignore same auth state after handleRedirectCallback', async () => { clientMock.handleRedirectCallback.mockReturnThis(); - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + const userObject = { name: 'foo' }; + clientMock.getUser.mockResolvedValue(userObject); const wrapper = createWrapper(); const { waitForNextUpdate, result } = renderHook( () => useContext(Auth0Context), @@ -803,7 +781,7 @@ describe('Auth0Provider', () => { await waitForNextUpdate(); const prevState = result.current; - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + clientMock.getUser.mockResolvedValue(userObject); await act(async () => { await result.current.handleRedirectCallback(); }); @@ -849,7 +827,7 @@ describe('Auth0Provider', () => { it('should not update context value after rerender with no state change', async () => { clientMock.getTokenSilently.mockReturnThis(); - clientMock.getUser.mockResolvedValue({ name: 'foo', updated_at: '1' }); + clientMock.getUser.mockResolvedValue({ name: 'foo' }); const wrapper = createWrapper(); const { waitForNextUpdate, result, rerender } = renderHook( () => useContext(Auth0Context), @@ -895,15 +873,20 @@ describe('Auth0Provider', () => { wrapper, }); - await expect( - auth0ContextRender.result.current.getIdTokenClaims - ).toThrowError('You forgot to wrap your component in .'); + await act(async () => { + await expect( + auth0ContextRender.result.current.getIdTokenClaims + ).toThrowError('You forgot to wrap your component in .'); + }); const customContextRender = renderHook(() => useContext(context), { wrapper, }); - const claims = await customContextRender.result.current.getIdTokenClaims(); + let claims; + await act(async () => { + claims = await customContextRender.result.current.getIdTokenClaims(); + }); expect(clientMock.getIdTokenClaims).toHaveBeenCalled(); expect(claims).toStrictEqual({ claim: '__test_claim__', diff --git a/__tests__/with-authentication-required.test.tsx b/__tests__/with-authentication-required.test.tsx index 5f1ed7c0..28de08b6 100644 --- a/__tests__/with-authentication-required.test.tsx +++ b/__tests__/with-authentication-required.test.tsx @@ -2,11 +2,11 @@ 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'; -const mockClient = jest.mocked(new Auth0Client({ client_id: '', domain: '' })); +const mockClient = jest.mocked(new Auth0Client({ clientId: '', domain: '' })); describe('withAuthenticationRequired', () => { it('should block access to a private component when not authenticated', async () => { @@ -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( - - - - ); - 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( - - - - ); - 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__' }) diff --git a/cypress/integration/smoke.test.ts b/cypress/integration/smoke.test.ts index b33804a4..de867074 100644 --- a/cypress/integration/smoke.test.ts +++ b/cypress/integration/smoke.test.ts @@ -31,6 +31,9 @@ describe('Smoke tests', () => { loginToAuth0(); + // Make sure the table has rendered with data as that is when the page has loaded completely + // and there shouldn't be any issues with the logout button being recreated + cy.get('table tbody tr').should('have.length', 2); cy.url().should('include', '/users'); cy.get('#logout').click(); }); @@ -40,6 +43,9 @@ describe('Smoke tests', () => { loginToAuth0(); + // Make sure the table has rendered with data as that is when the page has loaded completely + // and there shouldn't be any issues with the logout button being recreated + cy.get('table tbody tr').should('have.length', 2); cy.get('table').contains('bob@example.com'); cy.get('#logout').click(); }); diff --git a/docs/classes/OAuthError.html b/docs/classes/OAuthError.html index d2770793..61d63c1f 100644 --- a/docs/classes/OAuthError.html +++ b/docs/classes/OAuthError.html @@ -2,7 +2,7 @@

An OAuth2 error will come from the authorization server and will have at least an error property which will be the error code. And possibly an error_description property

Hierarchy

  • Error
    • OAuthError

Index

Constructors

  • new OAuthError(error: string, error_description?: string): OAuthError

Properties

error: string
error_description?: string
message: string
name: string
stack?: string
prepareStackTrace?: (err: Error, stackTraces: CallSite[]) => any

Type declaration

    • (err: Error, stackTraces: CallSite[]): any
    • +

Hierarchy

  • Error
    • OAuthError

Index

Constructors

  • new OAuthError(error: string, error_description?: string): OAuthError

Properties

error: string
error_description?: string
message: string
name: string
stack?: string
prepareStackTrace?: (err: Error, stackTraces: CallSite[]) => any

Type declaration

stackTraceLimit: number

Methods

  • captureStackTrace(targetObject: object, constructorOpt?: Function): void
isAuthenticated: boolean
isLoading: boolean
user?: TUser

Methods

  • buildAuthorizeUrl(options?: RedirectLoginOptions<any>): Promise<string>
  • const authUrl = await buildAuthorizeUrl();
     

    Builds an /authorize URL for loginWithRedirect using the parameters provided as arguments. Random and secure state and nonce parameters will be auto-generated.

    -

    Parameters

    • Optional options: RedirectLoginOptions<any>

    Returns Promise<string>

  • const logoutUrl = buildLogoutUrl();
     

    returns a URL to the logout endpoint using the parameters provided as arguments.

    Parameters

    Returns string

Returns string

  • const token = await getTokenWithPopup(options, config);
     

    Get an access token interactively.

    @@ -70,18 +70,18 @@ provided as arguments. Random and secure state and nonce parameters will be auto-generated. If the response is successful, results will be valid according to their expiration times.

    -

    Parameters

    Returns Promise<string>

  • handleRedirectCallback(url?: string): Promise<RedirectLoginResult<any>>
  • handleRedirectCallback(url?: string): Promise<RedirectLoginResult<any>>
  • After the browser redirects back to the callback page, call handleRedirectCallback to handle success and error responses from Auth0. If the response is successful, results will be valid according to their expiration times.

    Parameters

    • Optional url: string

      The URL to that should be used to retrieve the state and code values. Defaults to window.location.href if not given.

      -

    Returns Promise<RedirectLoginResult<any>>

Returns Promise<RedirectLoginResult<any>>

  • await loginWithPopup(options, config);
     

    Opens a popup with the /authorize URL using the parameters @@ -91,13 +91,13 @@

    IMPORTANT: This method has to be called from an event handler that was started by the user like a button click, for example, otherwise the popup will be blocked in most browsers.

    -

    Parameters

    Returns Promise<void>

  • await loginWithRedirect(options);
     

    Performs a redirect to /authorize using the parameters provided as arguments. Random and secure state and nonce parameters will be auto-generated.

    -

    Parameters

    Returns Promise<void>

  • auth0.logout({ returnTo: window.location.origin });
     

    Clears the application session and performs a redirect to /v2/logout, using diff --git a/docs/interfaces/Auth0ProviderOptions.html b/docs/interfaces/Auth0ProviderOptions.html index 3278db46..c583b525 100644 --- a/docs/interfaces/Auth0ProviderOptions.html +++ b/docs/interfaces/Auth0ProviderOptions.html @@ -3,33 +3,33 @@

Hierarchy

  • Auth0ProviderOptions

Indexable

[key: string]: any

If you need to send custom parameters to the Authorization Server, make sure to use the original parameter name.

-

Index

Properties

advancedOptions?: { defaultScope?: string }
+

Index

Properties

advancedOptions?: { defaultScope?: string }

Changes to recommended defaults, like defaultScope

Type declaration

  • Optional defaultScope?: string

    The default scope to be included with all requests. If not provided, 'openid profile email' is used. This can be set to null in order to effectively remove the default scopes.

    Note: The openid scope is always applied regardless of this setting.

    -
audience?: string
+
audience?: string

The default audience to be used for requesting API access.

-
authorizeTimeoutInSeconds?: number
+
authorizeTimeoutInSeconds?: number

A maximum number of seconds to wait before declaring background calls to /authorize as failed for timeout Defaults to 60s.

-
cache?: ICache
+
cache?: ICache

Specify a custom cache implementation to use for token storage and retrieval. This setting takes precedence over cacheLocation if they are both specified.

Read more about creating a custom cache

-
cacheLocation?: CacheLocation
+
cacheLocation?: CacheLocation

The location to use when storing cache data. Valid values are memory or localstorage. The default setting is memory.

children?: ReactNode
+
children?: ReactNode

The child nodes your Provider has wrapped

-
clientId: string
+
clientId: string

The Client ID found on your Application settings page

-
connection?: string
+
connection?: string

The name of the connection configured for your application. If null, it will redirect to the Auth0 Login Page and show the Login Widget.

-
context?: Context<Auth0ContextInterface<User>>
+
context?: Context<Auth0ContextInterface<User>>

Context to be used when creating the Auth0Provider, defaults to the internally created context.

This allows multiple Auth0Providers to be nested within the same application, the context value can then be passed to useAuth0, withAuth0, or withAuthenticationRequired to use that specific Auth0Provider to access @@ -43,47 +43,47 @@ used to store data is different

For a sample on using multiple Auth0Providers review the React Account Linking Sample

-
domain: string
+
domain: string

Your Auth0 account domain such as 'example.auth0.com', 'example.eu.auth0.com' or , 'example.mycompany.com' (when using custom domains)

-
invitation?: string
+
invitation?: string

The Id of an invitation to accept. This is available from the user invitation URL that is given when participating in a user invitation flow.

-
issuer?: string
+
issuer?: string

The issuer to be used for validation of JWTs, optionally defaults to the domain above

-
leeway?: number
+
leeway?: number

The value in seconds used to account for clock skew in JWT expirations. Typically, this value is no more than a minute or two at maximum. Defaults to 60s.

-
maxAge?: string | number
+
maxAge?: string | number

Maximum allowable elapsed time (in seconds) since authentication. If the last time the user authenticated is greater than this value, the user must be reauthenticated.

-
organization?: string
+
organization?: string

The Id of an organization to log in to.

This will specify an organization parameter in your user's login request and will add a step to validate the org_id claim in your user's ID Token.

-
redirectUri?: string
+
redirectUri?: string

The default URL where Auth0 will redirect your browser to with the authentication result. It must be whitelisted in the "Allowed Callback URLs" field in your Auth0 Application's settings. If not provided here, it should be provided in the other methods that provide authentication.

-
scope?: string
+
scope?: string

The default scope to be used on authentication requests. The defaultScope defined in the Auth0Client is included along with this scope

-
skipRedirectCallback?: boolean
+
skipRedirectCallback?: boolean

By default, if the page url has code/state params, the SDK will treat them as Auth0's and attempt to exchange the code for a token. In some cases the code might be for something else (another OAuth SDK perhaps). In these instances you can instruct the client to ignore them eg

<Auth0Provider
clientId={clientId}
domain={domain}
skipRedirectCallback={window.location.pathname === '/stripe-oauth-callback'}
>
-
useRefreshTokens?: boolean
+
useRefreshTokens?: boolean

If true, refresh tokens are used to fetch new access tokens from the Auth0 server. If false, the legacy technique of using a hidden iframe and the authorization_code grant with prompt=none is used. The default setting is false.

Note: Use of refresh tokens must be enabled by an administrator on your Auth0 client application.

-

Methods

Methods

-
fragment?: string
+
fragment?: string

Used to add to the URL fragment before redirecting

id_token_hint?: string

Previously issued ID Token.

@@ -39,7 +39,7 @@
  • 'consent': prompt user for consent before processing request
  • 'select_account': prompt user to select an account
  • -
    redirectUri?: string
    +
    redirectUri?: string

    The URL where Auth0 will redirect your browser to with the authentication result. It must be whitelisted in the "Allowed Callback URLs" field in your Auth0 Application's diff --git a/docs/interfaces/WithAuth0Props.html b/docs/interfaces/WithAuth0Props.html index 750d9643..365c7ba0 100644 --- a/docs/interfaces/WithAuth0Props.html +++ b/docs/interfaces/WithAuth0Props.html @@ -1,3 +1,3 @@ WithAuth0Props | @auth0/auth0-react

    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Interface WithAuth0Props

    Components wrapped in withAuth0 will have an additional auth0 prop

    -

    Hierarchy

    • WithAuth0Props

    Index

    Properties

    Properties

    Legend

    • Property
    • Method
    • Constructor
    • Property

    Settings

    Theme

    \ No newline at end of file +

    Hierarchy

    • WithAuth0Props

    Index

    Properties

    Properties

    Legend

    • Property
    • Method
    • Constructor
    • Property

    Settings

    Theme

    \ No newline at end of file diff --git a/docs/interfaces/WithAuthenticationRequiredOptions.html b/docs/interfaces/WithAuthenticationRequiredOptions.html index e7410e79..302f9368 100644 --- a/docs/interfaces/WithAuthenticationRequiredOptions.html +++ b/docs/interfaces/WithAuthenticationRequiredOptions.html @@ -1,25 +1,25 @@ WithAuthenticationRequiredOptions | @auth0/auth0-react
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Interface WithAuthenticationRequiredOptions

    Options for the withAuthenticationRequired Higher Order Component

    -

    Hierarchy

    • WithAuthenticationRequiredOptions

    Index

    Properties

    context?: Context<Auth0ContextInterface<User>>
    +

    Hierarchy

    • WithAuthenticationRequiredOptions

    Index

    Properties

    context?: Context<Auth0ContextInterface<User>>

    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 associated with the default Auth0Context.

    -
    loginOptions?: RedirectLoginOptions<any>
    +
    loginOptions?: RedirectLoginOptions<any>
    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.

    -
    returnTo?: string | (() => string)
    +
    returnTo?: string | (() => string)
    withAuthenticationRequired(Profile, {
    returnTo: '/profile'
    })

    or

    withAuthenticationRequired(Profile, {
    returnTo: () => window.location.hash.substr(1)
    })

    Add a path for the onRedirectCallback handler to return the user to after login.

    -

    Methods

    • claimCheck(claims?: User): boolean

    Methods

    • claimCheck(claims?: User): boolean
    • Check the user object for JWT claims and return a boolean indicating whether or not they are authorized to view the component.

      -

      Parameters

      • Optional claims: User

      Returns boolean

    • onRedirecting(): Element
    • onRedirecting(): Element
    • withAuthenticationRequired(Profile, {
      onRedirecting: () => <div>Redirecting you to the login...</div>
      })

      Render a message to show that the user is being redirected to the login.

      diff --git a/docs/modules.html b/docs/modules.html index f0f04dad..367638b4 100644 --- a/docs/modules.html +++ b/docs/modules.html @@ -1,25 +1,25 @@ -@auth0/auth0-react
      Options
      All
      • Public
      • Public/Protected
      • All
      Menu

      @auth0/auth0-react

      Index

      Type aliases

      AppState: { returnTo?: string }
      +@auth0/auth0-react
      Options
      All
      • Public
      • Public/Protected
      • All
      Menu

      @auth0/auth0-react

      Index

      Type aliases

      AppState: { returnTo?: string }

      The state of the application before the user was redirected to the login page.

      Type declaration

      • [key: string]: any
      • Optional returnTo?: string
      CacheLocation: "memory" | "localstorage"

      The possible locations where tokens can be stored

      -
      Cacheable: WrappedCacheEntry | KeyManifestEntry

      Variables

      Auth0Context: Context<Auth0ContextInterface<User>> = ...
      +
      Cacheable: WrappedCacheEntry | KeyManifestEntry

      Variables

      Auth0Context: Context<Auth0ContextInterface<User>> = ...

      The Auth0 Context

      -

      Functions

      Functions

      • <Auth0Provider
        domain={domain}
        clientId={clientId}
        redirectUri={window.location.origin}>
        <MyApp />
        </Auth0Provider>

        Provides the Auth0Context to its child components.

        -

        Parameters

        Returns Element

      • const {
        // Auth state:
        error,
        isAuthenticated,
        isLoading,
        user,
        // Auth methods:
        getAccessTokenSilently,
        getAccessTokenWithPopup,
        getIdTokenClaims,
        loginWithRedirect,
        loginWithPopup,
        logout,
        } = useAuth0<TUser>();

        Use the useAuth0 hook in your components to access the auth state and methods.

        TUser is an optional type param to provide a type to the user field.

        -

        Type parameters

        Parameters

        Returns Auth0ContextInterface<TUser>

      • class MyComponent extends Component {
        render() {
        // Access the auth context from the `auth0` prop
        const { user } = this.props.auth0;
        return <div>Hello {user.name}!</div>
        }
        }
        // Wrap your class component in withAuth0
        export default withAuth0(MyComponent);

        Wrap your class components in this Higher Order Component to give them access to the Auth0Context.

        Providing a context as the second argument allows you to configure the Auth0Provider the Auth0Context should come from f you have multiple within your application.

        -

        Type parameters

        Parameters

        Returns ComponentType<Omit<P, "auth0">>

      • const MyProtectedComponent = withAuthenticationRequired(MyComponent);
         

        When you wrap your components in this Higher Order Component and an anonymous user visits your component diff --git a/examples/cra-react-router/package.json b/examples/cra-react-router/package.json index 6fad63c9..73095cb7 100644 --- a/examples/cra-react-router/package.json +++ b/examples/cra-react-router/package.json @@ -4,14 +4,8 @@ "private": true, "dependencies": { "@auth0/auth0-react": "file:../..", - "@testing-library/jest-dom": "^5.16.4", - "@testing-library/react": "^13.1.1", - "@testing-library/user-event": "^14.1.1", - "@types/history": "^4.7.11", - "@types/jest": "^27.4.1", "@types/node": "^17.0.29", "@types/react": "^18.0.8", - "@types/react-dom": "^18.0.0", "@types/react-router-dom": "^5.3.3", "react": "file:../../node_modules/react", "react-dom": "file:../../node_modules/react-dom", diff --git a/examples/cra-react-router/src/Nav.tsx b/examples/cra-react-router/src/Nav.tsx index fa307b32..afa8f0a4 100644 --- a/examples/cra-react-router/src/Nav.tsx +++ b/examples/cra-react-router/src/Nav.tsx @@ -36,7 +36,9 @@ export function Nav() { diff --git a/examples/cra-react-router/src/Users.tsx b/examples/cra-react-router/src/Users.tsx index ff849cee..636cbb67 100644 --- a/examples/cra-react-router/src/Users.tsx +++ b/examples/cra-react-router/src/Users.tsx @@ -6,13 +6,14 @@ import { Error } from './Error'; const PORT = process.env.REACT_APP_API_PORT || 3001; export function Users() { - const { loading, error, data: users = [] } = useApi( - `http://localhost:${PORT}/users`, - { - audience: process.env.REACT_APP_AUDIENCE, - scope: 'read:users', - } - ); + const { + loading, + error, + data: users = [], + } = useApi(`http://localhost:${PORT}/users`, { + audience: process.env.REACT_APP_AUDIENCE, + scope: 'profile email read:users', + }); if (loading) { return ; diff --git a/examples/cra-react-router/src/index.tsx b/examples/cra-react-router/src/index.tsx index ad7494e3..b0a1063f 100644 --- a/examples/cra-react-router/src/index.tsx +++ b/examples/cra-react-router/src/index.tsx @@ -28,9 +28,11 @@ ReactDOM.render( diff --git a/examples/cra-react-router/src/setupTests.ts b/examples/cra-react-router/src/setupTests.ts deleted file mode 100644 index 74b1a275..00000000 --- a/examples/cra-react-router/src/setupTests.ts +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom/extend-expect'; diff --git a/examples/cra-react-router/src/use-api.ts b/examples/cra-react-router/src/use-api.ts index ff1924c2..2471e514 100644 --- a/examples/cra-react-router/src/use-api.ts +++ b/examples/cra-react-router/src/use-api.ts @@ -16,7 +16,9 @@ export const useApi = ( (async () => { try { const { audience, scope, ...fetchOptions } = options; - const accessToken = await getAccessTokenSilently({ audience, scope }); + const accessToken = await getAccessTokenSilently({ + authorizationParams: { audience, scope }, + }); const res = await fetch(url, { ...fetchOptions, headers: { diff --git a/examples/gatsby-app/gatsby-browser.js b/examples/gatsby-app/gatsby-browser.js index 4a7a86f0..2a5caad7 100644 --- a/examples/gatsby-app/gatsby-browser.js +++ b/examples/gatsby-app/gatsby-browser.js @@ -12,10 +12,12 @@ export const wrapRootElement = ({ element }) => { {element} diff --git a/examples/gatsby-app/gatsby-config.js b/examples/gatsby-app/gatsby-config.js index 072592e2..f7ef8487 100644 --- a/examples/gatsby-app/gatsby-config.js +++ b/examples/gatsby-app/gatsby-config.js @@ -1,8 +1,3 @@ module.exports = { - plugins: [ - { - resolve: 'gatsby-plugin-create-client-paths', - options: { prefixes: ['/*'] }, - }, - ], + trailingSlash: 'never', }; diff --git a/examples/gatsby-app/package.json b/examples/gatsby-app/package.json index 3a053f0c..07e9d765 100644 --- a/examples/gatsby-app/package.json +++ b/examples/gatsby-app/package.json @@ -5,19 +5,10 @@ "version": "0.1.0", "dependencies": { "@auth0/auth0-react": "file:../..", - "bootstrap": "^4.5.0", - "gatsby": "^2.22.15", - "gatsby-image": "^2.4.5", - "gatsby-plugin-create-client-paths": "^2.3.4", - "gatsby-plugin-manifest": "^2.4.9", - "gatsby-plugin-offline": "^3.2.7", - "gatsby-plugin-react-helmet": "^3.3.2", - "gatsby-source-filesystem": "^2.3.8", - "gatsby-transformer-sharp": "^2.5.3", - "prop-types": "^15.7.2", + "bootstrap": "^5.0.0", + "gatsby": "^5.0.1", "react": "file:../../node_modules/react", - "react-dom": "file:../../node_modules/react-dom", - "react-helmet": "^6.0.0" + "react-dom": "file:../../node_modules/react-dom" }, "devDependencies": { "prettier": "2.0.5" diff --git a/examples/gatsby-app/src/components/Loading.js b/examples/gatsby-app/src/components/Loading.js index 0f7b5d67..b76fcd85 100644 --- a/examples/gatsby-app/src/components/Loading.js +++ b/examples/gatsby-app/src/components/Loading.js @@ -4,7 +4,7 @@ export function Loading() { return (

        - Loading... + Loading...
        ); diff --git a/examples/gatsby-app/src/components/Nav.js b/examples/gatsby-app/src/components/Nav.js index ddbaa939..786a7d09 100644 --- a/examples/gatsby-app/src/components/Nav.js +++ b/examples/gatsby-app/src/components/Nav.js @@ -7,47 +7,53 @@ export function Nav() { const pathname = typeof window !== 'undefined' && window.location.pathname; return ( - ); } diff --git a/examples/gatsby-app/src/components/Users.js b/examples/gatsby-app/src/components/Users.js index e515e591..f8a8fb6e 100644 --- a/examples/gatsby-app/src/components/Users.js +++ b/examples/gatsby-app/src/components/Users.js @@ -1,18 +1,19 @@ -import { useApi } from '../hooks/use-api'; import React from 'react'; +import { useApi } from '../hooks/use-api'; import { Loading } from './Loading'; import { Error } from './Error'; const PORT = process.env.GATSBY_API_PORT || 3001; export function Users() { - const { loading, error, data: users = [] } = useApi( - `http://localhost:${PORT}/users`, - { - audience: process.env.GATSBY_AUDIENCE, - scope: 'read:users', - } - ); + const { + loading, + error, + data: users = [], + } = useApi(`http://localhost:${PORT}/users`, { + audience: process.env.GATSBY_AUDIENCE, + scope: 'profile email read:users', + }); if (loading) { return ; diff --git a/examples/gatsby-app/src/hooks/use-api.js b/examples/gatsby-app/src/hooks/use-api.js index 6c2a2099..1d909562 100644 --- a/examples/gatsby-app/src/hooks/use-api.js +++ b/examples/gatsby-app/src/hooks/use-api.js @@ -13,7 +13,9 @@ export const useApi = (url, options) => { (async () => { try { const { audience, scope, ...fetchOptions } = options; - const accessToken = await getAccessTokenSilently({ audience, scope }); + const accessToken = await getAccessTokenSilently({ + authorizationParams: { audience, scope }, + }); const res = await fetch(url, { ...fetchOptions, headers: { diff --git a/examples/gatsby-app/src/pages/index.js b/examples/gatsby-app/src/pages/index.js index 80c00cd4..a837aa8c 100644 --- a/examples/gatsby-app/src/pages/index.js +++ b/examples/gatsby-app/src/pages/index.js @@ -1,13 +1,9 @@ import React from 'react'; -import { Router } from '@reach/router'; -import { useAuth0, withAuthenticationRequired } from '@auth0/auth0-react'; +import { useAuth0 } from '@auth0/auth0-react'; import { Nav } from '../components/Nav'; -import { Users } from '../components/Users'; import { Loading } from '../components/Loading'; import { Error } from '../components/Error'; -const ProtectedRoute = withAuthenticationRequired(Users); - const IndexPage = () => { const { isLoading, error } = useAuth0(); @@ -19,9 +15,6 @@ const IndexPage = () => { <>