From 01d33b95334bf3745cddcb793fce2be9acf41d0f Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Sat, 23 Jan 2021 20:45:44 -0300 Subject: [PATCH 1/5] Simplify configuration of custom URLs --- EXAMPLES.md | 3 +- src/auth0-session/get-config.ts | 2 +- src/config.ts | 181 ++---------------- src/frontend/index.ts | 1 + src/frontend/use-config.tsx | 22 +++ src/frontend/use-user.tsx | 22 ++- src/frontend/with-page-auth-required.tsx | 28 +-- src/handlers/callback.ts | 4 +- src/handlers/login.ts | 4 +- src/handlers/logout.ts | 4 +- src/helpers/with-page-auth-required.ts | 15 +- src/index.ts | 4 +- src/instance.ts | 2 +- src/session/get-access-token.ts | 4 +- tests/config.test.ts | 6 +- tests/fixtures/frontend.tsx | 17 +- tests/frontend/use-config.test.ts | 54 ++++++ .../{use-user.test.tsx => use-user.test.ts} | 50 ++++- .../frontend/with-page-auth-required.test.tsx | 17 +- tests/helpers/with-page-auth-required.test.ts | 15 +- 20 files changed, 239 insertions(+), 216 deletions(-) create mode 100644 src/frontend/use-config.tsx create mode 100644 tests/frontend/use-config.test.ts rename tests/frontend/{use-user.test.tsx => use-user.test.ts} (71%) diff --git a/EXAMPLES.md b/EXAMPLES.md index 6df9f8e24..021ae3da4 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -132,7 +132,8 @@ export default async function login(req, res) { export default () => Login; ``` -> Note: you will need to specify this custom login URL when calling `withPageAuthRequired` both the [front end version](https://auth0.github.io/nextjs-auth0/interfaces/frontend_with_page_auth_required.withpageauthrequiredoptions.html#loginurl) and [server side version](https://auth0.github.io/nextjs-auth0/modules/helpers_with_page_auth_required.html#withpageauthrequiredoptions) +> Note: If you customise the login URL you will need to set the environment variable `NEXT_PUBLIC_AUTH0_LOGIN` to this custom value for `withPageAuthRequired` to work correctly. +And if you customize the profile URL, you will need to set the `NEXT_PUBLIC_AUTH0_PROFILE` environment variable to this custom value for the `useUser` hook to work properly. ## Protecting a Server Side Rendered (SSR) Page diff --git a/src/auth0-session/get-config.ts b/src/auth0-session/get-config.ts index f8a508928..b2e6acdf6 100644 --- a/src/auth0-session/get-config.ts +++ b/src/auth0-session/get-config.ts @@ -164,7 +164,7 @@ export const get = (params: ConfigParameters = {}): Config => { ...params }; - const { value, error, warning } = paramsSchema.validate(config); + const { value, error, warning } = paramsSchema.validate(config, { allowUnknown: true }); if (error) { throw new TypeError(error.details[0].message); } diff --git a/src/config.ts b/src/config.ts index 55d114731..669ed4428 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ -import { IncomingMessage } from 'http'; import { AuthorizationParameters as OidcAuthorizationParameters } from 'openid-client'; -import { LoginOptions, DeepPartial } from './auth0-session'; + +import { DeepPartial, Config as SessionLayerConfig } from './auth0-session'; /** * ## Configuration properties. @@ -39,6 +39,8 @@ import { LoginOptions, DeepPartial } from './auth0-session'; * - `AUTH0_IDP_LOGOUT`: See {@link idpLogout} * - `AUTH0_ID_TOKEN_SIGNING_ALG`: See {@link idTokenSigningAlg} * - `AUTH0_LEGACY_SAME_SITE_COOKIE`: See {@link legacySameSiteCookie} + * - `NEXT_PUBLIC_AUTH0_LOGIN`: See {@link Config.routes} + * - `NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT`: See {@link Config.routes} * - `AUTH0_POST_LOGOUT_REDIRECT`: See {@link Config.routes} * - `AUTH0_CALLBACK`: See {@link Config.routes} * - `AUTH0_AUDIENCE`: See {@link Config.authorizationParams} @@ -82,180 +84,35 @@ import { LoginOptions, DeepPartial } from './auth0-session'; * * @category Server */ -export interface Config { - /** - * The secret(s) used to derive an encryption key for the user identity in a session cookie and - * to sign the transient cookies used by the login callback. - * Use a single string key or array of keys for an encrypted session cookie. - * Can use env key SECRET instead. - */ - secret: string | Array; - - /** - * Object defining application session cookie attributes. - */ - session: SessionConfig; - - /** - * Boolean value to enable Auth0's logout feature. - */ - auth0Logout: boolean; - - /** - * URL parameters used when redirecting users to the authorization server to log in. - * - * If this property is not provided by your application, its default values will be: - * - * ```js - * { - * response_type: 'code', - * scope: 'openid profile email' - * } - * ``` - * - * New values can be passed in to change what is returned from the authorization server - * depending on your specific scenario. Additional custom parameters can be added as well. - * - * **Note:** You must provide the required parameters if this object is set. - * - * ```js - * { - * response_type: 'code', - * scope: 'openid profile email', - * - * // Additional parameters - * acr_value: "tenant:test-tenant", - * custom_param: "custom-value" - * }; - * ``` - */ - authorizationParams: AuthorizationParameters; - - /** - * The root URL for the application router, eg https://localhost - * Can use env key BASE_URL instead. - * If you provide a domain, we will prefix it with `https://` - This can be useful when assigning it to - * `VERCEL_URL` for preview deploys - */ - baseURL: string; - - /** - * The Client ID for your application. - * Can be read from CLIENT_ID instead. - */ - clientID: string; - - /** - * The Client Secret for your application. - * Required when requesting access tokens. - * Can be read from CLIENT_SECRET instead. - */ - clientSecret?: string; - - /** - * Integer value for the system clock's tolerance (leeway) in seconds for ID token verification.` - * Default is 60 - */ - clockTolerance: number; - - /** - * Integer value for the http timeout in ms for authentication requests. - * Default is 5000 - */ - httpTimeout: number; - - /** - * To opt-out of sending the library and node version to your authorization server - * via the `Auth0-Client` header. Default is `true - */ - enableTelemetry: boolean; - - /** - * @ignore - */ - errorOnRequiredAuth: boolean; - - /** - * @ignore - */ - attemptSilentLogin: boolean; - - /** - * Function that returns an object with URL-safe state values for `res.oidc.login()`. - * Used for passing custom state parameters to your authorization server. - * Can also be passed in to {@link HandleLogin} - * - * ```js - * { - * ... - * getLoginState(req, options) { - * return { - * returnTo: options.returnTo || req.originalUrl, - * customState: 'foo' - * }; - * } - * } - * `` - */ - getLoginState: (req: IncomingMessage, options: LoginOptions) => Record; - - /** - * Array value of claims to remove from the ID token before storing the cookie session. - * Default is `['aud', 'iss', 'iat', 'exp', 'nbf', 'nonce', 'azp', 'auth_time', 's_hash', 'at_hash', 'c_hash' ]` - */ - identityClaimFilter: string[]; - - /** - * Boolean value to log the user out from the identity provider on application logout. Default is `true` - */ - idpLogout: boolean; - - /** - * String value for the expected ID token algorithm. Default is 'RS256' - */ - idTokenSigningAlg: string; - - /** - * REQUIRED. The root URL for the token issuer with no trailing slash. - * This is `https://` plus your Auth0 domain - * Can use env key ISSUER_BASE_URL instead. - */ - issuerBaseURL: string; - - /** - * Set a fallback cookie with no SameSite attribute when response_mode is form_post. - * Default is true - */ - legacySameSiteCookie: boolean; - - /** - * @ignore - */ - authRequired: boolean; - +export interface Config extends SessionLayerConfig { /** - * Boolean value to automatically install the login and logout routes. + * Configuration parameters to override the default authentication URLs. */ routes: { /** - * @ignore + * Relative path to the login handler. */ - login: string | false; + login: string; + + /** + * Either a relative path to the application or a valid URI to an external domain. + * The user will be redirected to this after a login has been performed. + */ + postLoginRedirect: string; /** * @ignore */ - logout: string | false; + logout: string; /** * Either a relative path to the application or a valid URI to an external domain. - * This value must be registered on the authorization server. * The user will be redirected to this after a logout has been performed. */ postLogoutRedirect: string; /** - * Relative path to the application callback to process the response from the authorization server. + * Relative path to the callback handler. */ callback: string; }; @@ -398,6 +255,8 @@ export const getParams = (params?: ConfigParameters): ConfigParameters => { AUTH0_IDP_LOGOUT, AUTH0_ID_TOKEN_SIGNING_ALG, AUTH0_LEGACY_SAME_SITE_COOKIE, + NEXT_PUBLIC_AUTH0_LOGIN, + NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT, AUTH0_POST_LOGOUT_REDIRECT, AUTH0_CALLBACK, AUTH0_AUDIENCE, @@ -457,8 +316,10 @@ export const getParams = (params?: ConfigParameters): ConfigParameters => { } }, routes: { - callback: AUTH0_CALLBACK || '/api/auth/callback', + login: NEXT_PUBLIC_AUTH0_LOGIN || '/api/auth/login', + postLoginRedirect: NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT, postLogoutRedirect: AUTH0_POST_LOGOUT_REDIRECT, + callback: AUTH0_CALLBACK || '/api/auth/callback', ...params?.routes } }; diff --git a/src/frontend/index.ts b/src/frontend/index.ts index c652b9e4b..82ab7529b 100644 --- a/src/frontend/index.ts +++ b/src/frontend/index.ts @@ -1,2 +1,3 @@ +export { default as ConfigProvider, ConfigProviderProps, useConfig } from './use-config'; export { default as UserProvider, UserProviderProps, UserProfile, UserContext, useUser } from './use-user'; export { default as withPageAuthRequired, WithPageAuthRequired } from './with-page-auth-required'; diff --git a/src/frontend/use-config.tsx b/src/frontend/use-config.tsx new file mode 100644 index 000000000..ac586eaba --- /dev/null +++ b/src/frontend/use-config.tsx @@ -0,0 +1,22 @@ +import React, { ReactElement, useContext, createContext } from 'react'; +import { useRouter } from 'next/router'; + +export type ConfigContext = { + loginUrl?: string; + returnTo?: string; +}; + +const Config = createContext({}); + +export type ConfigProviderProps = React.PropsWithChildren; +export type UseConfig = () => ConfigContext; +export const useConfig: UseConfig = () => useContext(Config); + +export default ({ + children, + loginUrl = process.env.NEXT_PUBLIC_AUTH0_LOGIN || '/api/auth/login', + returnTo = process.env.NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT +}: ConfigProviderProps): ReactElement => { + const router = useRouter(); + return {children}; +}; diff --git a/src/frontend/use-user.tsx b/src/frontend/use-user.tsx index dc5e392d9..021c683b3 100644 --- a/src/frontend/use-user.tsx +++ b/src/frontend/use-user.tsx @@ -1,5 +1,7 @@ import React, { ReactElement, useState, useEffect, useCallback, useContext, createContext } from 'react'; +import ConfigProvider, { ConfigContext } from './use-config'; + /** * The user claims returned from the {@link useUser} hook. * @@ -21,12 +23,12 @@ export interface UserProfile { * * @category Client */ -export interface UserContext { +export type UserContext = { user?: UserProfile; error?: Error; isLoading: boolean; checkSession: () => Promise; -} +}; /** * Configure the {@link UserProvider} component. @@ -59,7 +61,7 @@ export interface UserContext { * * @category Client */ -export type UserProviderProps = React.PropsWithChildren<{ user?: UserProfile; profileUrl?: string }>; +export type UserProviderProps = React.PropsWithChildren<{ user?: UserProfile; profileUrl?: string } & ConfigContext>; /** * @ignore @@ -122,11 +124,13 @@ export type UserProvider = (props: UserProviderProps) => ReactElement => { - const [user, setUser] = useState(() => initialUser); + const [user, setUser] = useState(initialUser); const [error, setError] = useState(); - const [isLoading, setIsLoading] = useState(() => !initialUser); + const [isLoading, setIsLoading] = useState(!initialUser); const checkSession = useCallback(async (): Promise => { try { @@ -147,5 +151,9 @@ export default ({ })(); }, [user]); - return {children}; + return ( + + {children} + + ); }; diff --git a/src/frontend/with-page-auth-required.tsx b/src/frontend/with-page-auth-required.tsx index 3b20ab301..a2ebbb663 100644 --- a/src/frontend/with-page-auth-required.tsx +++ b/src/frontend/with-page-auth-required.tsx @@ -1,6 +1,7 @@ import React, { ComponentType, useEffect } from 'react'; import { useRouter } from 'next/router'; +import { useConfig } from './use-config'; import { useUser } from './use-user'; /** @@ -19,25 +20,6 @@ const defaultOnError = (): JSX.Element => <>; * @category Client */ export interface WithPageAuthRequiredOptions { - /** - * ```js - * withPageAuthRequired(Profile, { - * returnTo: '/profile' - * }); - * ``` - * - * Add a path to return the user to after login. - */ - returnTo?: string; - /** - * ```js - * withPageAuthRequired(Profile, { - * loginUrl: '/api/login' - * }); - * ``` - * The path of your custom login API route. - */ - loginUrl?: string; /** * ```js * withPageAuthRequired(Profile, { @@ -81,12 +63,8 @@ export type WithPageAuthRequired =

( const withPageAuthRequired: WithPageAuthRequired = (Component, options = {}) => { return function withPageAuthRequired(props): JSX.Element { const router = useRouter(); - const { - returnTo = router.asPath, - onRedirecting = defaultOnRedirecting, - onError = defaultOnError, - loginUrl = '/api/auth/login' - } = options; + const { onRedirecting = defaultOnRedirecting, onError = defaultOnError } = options; + const { loginUrl, returnTo } = useConfig(); const { user, error, isLoading } = useUser(); useEffect(() => { diff --git a/src/handlers/callback.ts b/src/handlers/callback.ts index 2b2c5f27c..d494a603d 100644 --- a/src/handlers/callback.ts +++ b/src/handlers/callback.ts @@ -1,5 +1,7 @@ import { NextApiResponse, NextApiRequest } from 'next'; -import { ClientFactory, Config, callbackHandler, TransientStore } from '../auth0-session'; + +import { ClientFactory, callbackHandler, TransientStore } from '../auth0-session'; +import { Config } from '../config'; import { Session, SessionCache } from '../session'; import { assertReqRes } from '../utils/assert'; diff --git a/src/handlers/login.ts b/src/handlers/login.ts index f9182793a..c13064721 100644 --- a/src/handlers/login.ts +++ b/src/handlers/login.ts @@ -1,5 +1,7 @@ import { NextApiResponse, NextApiRequest } from 'next'; -import { AuthorizationParameters, ClientFactory, Config, loginHandler, TransientStore } from '../auth0-session'; + +import { AuthorizationParameters, ClientFactory, loginHandler, TransientStore } from '../auth0-session'; +import { Config } from '../config'; import isSafeRedirect from '../utils/url-helpers'; import { assertReqRes } from '../utils/assert'; diff --git a/src/handlers/logout.ts b/src/handlers/logout.ts index 099e7ad9f..9a91df785 100644 --- a/src/handlers/logout.ts +++ b/src/handlers/logout.ts @@ -1,5 +1,7 @@ import { NextApiResponse, NextApiRequest } from 'next'; -import { ClientFactory, Config, logoutHandler } from '../auth0-session'; + +import { ClientFactory, logoutHandler } from '../auth0-session'; +import { Config } from '../config'; import { SessionCache } from '../session'; import { assertReqRes } from '../utils/assert'; diff --git a/src/helpers/with-page-auth-required.ts b/src/helpers/with-page-auth-required.ts index 4ce73a3a0..dba5dcf30 100644 --- a/src/helpers/with-page-auth-required.ts +++ b/src/helpers/with-page-auth-required.ts @@ -1,7 +1,9 @@ +import React, { ComponentType } from 'react'; import { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from 'next'; + +import { Config } from '../config'; import { Claims, Session, SessionCache } from '../session'; import { assertCtx } from '../utils/assert'; -import React, { ComponentType } from 'react'; import { WithPageAuthRequiredOptions as WithPageAuthRequiredCSROptions } from '../frontend/with-page-auth-required'; import { withPageAuthRequired as withPageAuthRequiredCSR } from '../frontend'; @@ -58,7 +60,7 @@ export type PageRoute = (cts: GetServerSidePropsContext) => Promise => { assertCtx(ctx); if (!sessionCache.isAuthenticated(ctx.req, ctx.res)) { // 10 - redirect // 9.5.4 - unstable_redirect // 9.4 - res.setHeaders - return { redirect: { destination: `${loginUrl}?returnTo=${ctx.resolvedUrl}`, permanent: false } }; + return { + redirect: { destination: `${login}?returnTo=${postLoginRedirect || ctx.resolvedUrl}`, permanent: false } + }; } const session = sessionCache.get(ctx.req, ctx.res) as Session; let ret: any = { props: {} }; diff --git a/src/index.ts b/src/index.ts index 106f3fd6e..7789f5e55 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,7 +53,7 @@ function getInstance(): SignInWithAuth0 { } export const initAuth0: InitAuth0 = (params) => { - const config = getConfig(getParams(params)); + const config = getConfig(getParams(params)) as Config; const getClient = clientFactory(config, { name: 'nextjs-auth0', version }); const transientStore = new TransientStore(config); const cookieStore = new CookieStore(config); @@ -61,7 +61,7 @@ export const initAuth0: InitAuth0 = (params) => { const getSession = sessionFactory(sessionCache); const getAccessToken = accessTokenFactory(getClient, config, sessionCache); const withApiAuthRequired = withApiAuthRequiredFactory(sessionCache); - const withPageAuthRequired = withPageAuthRequiredFactory(sessionCache); + const withPageAuthRequired = withPageAuthRequiredFactory(config, sessionCache); const handleLogin = loginHandler(config, getClient, transientStore); const handleLogout = logoutHandler(config, getClient, sessionCache); const handleCallback = callbackHandler(config, getClient, sessionCache, transientStore); diff --git a/src/instance.ts b/src/instance.ts index a52632812..37dfe8336 100644 --- a/src/instance.ts +++ b/src/instance.ts @@ -1,7 +1,7 @@ import { GetSession, GetAccessToken } from './session'; import { WithApiAuthRequired, WithPageAuthRequired } from './helpers'; import { HandleAuth, HandleCallback, HandleLogin, HandleLogout, HandleProfile } from './handlers'; -import { ConfigParameters } from './auth0-session'; +import { ConfigParameters } from './config'; /** * The SDK server instance. diff --git a/src/session/get-access-token.ts b/src/session/get-access-token.ts index a5b5ed992..aa9c1bc69 100644 --- a/src/session/get-access-token.ts +++ b/src/session/get-access-token.ts @@ -1,5 +1,7 @@ import { IncomingMessage, ServerResponse } from 'http'; -import { ClientFactory, Config } from '../auth0-session'; + +import { ClientFactory } from '../auth0-session'; +import { Config } from '../config'; import { AccessTokenError } from '../utils/errors'; import { intersect, match } from '../utils/array'; import { SessionCache, fromTokenSet } from '../session'; diff --git a/tests/config.test.ts b/tests/config.test.ts index 7207c55c4..1dfdb1d59 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -43,8 +43,10 @@ describe('config params', () => { issuerBaseURL: undefined, legacySameSiteCookie: undefined, routes: { - callback: '/api/auth/callback', - postLogoutRedirect: undefined + login: '/api/auth/login', + postLoginRedirect: undefined, + postLogoutRedirect: undefined, + callback: '/api/auth/callback' }, secret: undefined, session: { diff --git a/tests/fixtures/frontend.tsx b/tests/fixtures/frontend.tsx index 688771a71..1448666ad 100644 --- a/tests/fixtures/frontend.tsx +++ b/tests/fixtures/frontend.tsx @@ -1,5 +1,7 @@ import React from 'react'; + import { UserProvider, UserProviderProps, UserProfile } from '../../src'; +import { ConfigProvider, ConfigProviderProps } from '../../src/frontend'; type FetchUserMock = { ok: boolean; @@ -16,8 +18,15 @@ export const user: UserProfile = { updated_at: null }; -export const withUserProvider = ({ user, profileUrl }: UserProviderProps = {}): React.ComponentType => { - return (props: any): React.ReactElement => ; +export const withUserProvider = ({ + user, + profileUrl, + loginUrl, + returnTo +}: UserProviderProps = {}): React.ComponentType => { + return (props: any): React.ReactElement => ( + + ); }; export const fetchUserMock = (): Promise => { @@ -34,3 +43,7 @@ export const fetchUserUnsuccessfulMock = (): Promise => { }; export const fetchUserErrorMock = (): Promise => Promise.reject(new Error('Error')); + +export const withConfigProvider = ({ loginUrl, returnTo }: ConfigProviderProps = {}): React.ComponentType => { + return (props: any): React.ReactElement => ; +}; diff --git a/tests/frontend/use-config.test.ts b/tests/frontend/use-config.test.ts new file mode 100644 index 000000000..a570303f0 --- /dev/null +++ b/tests/frontend/use-config.test.ts @@ -0,0 +1,54 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { withConfigProvider } from '../fixtures/frontend'; +import { useConfig } from '../../src/frontend/use-config'; + +jest.mock('next/router', () => ({ + useRouter: (): any => ({ asPath: '/' }) +})); + +describe('context wrapper', () => { + test('should provide the default login url', async () => { + const { result } = renderHook(() => useConfig(), { + wrapper: withConfigProvider({ loginUrl: '/api/auth/login' }) + }); + + expect(result.current.loginUrl).toEqual('/api/auth/login'); + }); + + test('should provide a custom login url', async () => { + const { result } = renderHook(() => useConfig(), { + wrapper: withConfigProvider({ loginUrl: '/api/custom-url' }) + }); + + expect(result.current.loginUrl).toEqual('/api/custom-url'); + }); + + test('should provide a custom returnTo url', async () => { + const { result } = renderHook(() => useConfig(), { + wrapper: withConfigProvider({ returnTo: '/foo' }) + }); + + expect(result.current.returnTo).toEqual('/foo'); + }); + + test('should provide a custom login url from an environment variable', async () => { + process.env.NEXT_PUBLIC_AUTH0_LOGIN = '/api/custom-url'; + const { result } = renderHook(() => useConfig(), { + wrapper: withConfigProvider() + }); + + expect(result.current.loginUrl).toEqual('/api/custom-url'); + delete process.env.NEXT_PUBLIC_AUTH0_LOGIN; + }); + + test('should provide a custom returnTo url from an environment variable', async () => { + process.env.NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT = '/foo'; + const { result } = renderHook(() => useConfig(), { + wrapper: withConfigProvider() + }); + + expect(result.current.returnTo).toEqual('/foo'); + delete process.env.NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT; + }); +}); diff --git a/tests/frontend/use-user.test.tsx b/tests/frontend/use-user.test.ts similarity index 71% rename from tests/frontend/use-user.test.tsx rename to tests/frontend/use-user.test.ts index 140740b95..7092188bd 100644 --- a/tests/frontend/use-user.test.tsx +++ b/tests/frontend/use-user.test.ts @@ -7,8 +7,13 @@ import { withUserProvider, user } from '../fixtures/frontend'; +import { useConfig } from '../../src/frontend'; import { useUser } from '../../src'; +jest.mock('next/router', () => ({ + useRouter: (): any => ({ asPath: '/' }) +})); + describe('context wrapper', () => { afterEach(() => delete (global as any).fetch); @@ -57,7 +62,7 @@ describe('context wrapper', () => { expect(result.current.isLoading).toEqual(false); }); - test('should use the existing user', async () => { + test('should provide the existing user', async () => { const { result } = renderHook(() => useUser(), { wrapper: withUserProvider({ user }) }); expect(result.current.user).toEqual(user); @@ -65,7 +70,18 @@ describe('context wrapper', () => { expect(result.current.isLoading).toEqual(false); }); - test('should use a custom profileUrl', async () => { + test('should use the default profile url', async () => { + const fetchSpy = jest.fn().mockReturnValue(Promise.resolve()); + (global as any).fetch = fetchSpy; + const { result, waitForValueToChange } = renderHook(() => useUser(), { + wrapper: withUserProvider() + }); + + await waitForValueToChange(() => result.current.isLoading); + expect(fetchSpy).toHaveBeenCalledWith('/api/auth/me'); + }); + + test('should use a custom profile url', async () => { const fetchSpy = jest.fn().mockReturnValue(Promise.resolve()); (global as any).fetch = fetchSpy; const { result, waitForValueToChange } = renderHook(() => useUser(), { @@ -76,12 +92,40 @@ describe('context wrapper', () => { expect(fetchSpy).toHaveBeenCalledWith('/api/custom-url'); }); + test('should use a custom profile url from an environment variable', async () => { + process.env.NEXT_PUBLIC_AUTH0_PROFILE = '/api/custom-url'; + const fetchSpy = jest.fn().mockReturnValue(Promise.resolve()); + (global as any).fetch = fetchSpy; + const { result, waitForValueToChange } = renderHook(() => useUser(), { + wrapper: withUserProvider() + }); + + await waitForValueToChange(() => result.current.isLoading); + expect(fetchSpy).toHaveBeenCalledWith('/api/custom-url'); + delete process.env.NEXT_PUBLIC_AUTH0_PROFILE; + }); + test('should accept a custom login url', async () => { + const { result } = renderHook(() => useConfig(), { + wrapper: withUserProvider({ user, loginUrl: '/api/custom-url' }) + }); + + expect(result.current.loginUrl).toEqual('/api/custom-url'); + }); + + test('should accept a custom returnTo url', async () => { + const { result } = renderHook(() => useConfig(), { + wrapper: withUserProvider({ user, returnTo: '/foo' }) + }); + + expect(result.current.returnTo).toEqual('/foo'); + }); + test('should check the session when logged in', async () => { (global as any).fetch = fetchUserUnsuccessfulMock; const { result, waitForValueToChange } = renderHook(() => useUser(), { wrapper: withUserProvider() }); await waitForValueToChange(() => result.current.isLoading); - expect(result.current.user).toBeUndefined; + expect(result.current.user).toBeUndefined(); (global as any).fetch = fetchUserMock; diff --git a/tests/frontend/with-page-auth-required.test.tsx b/tests/frontend/with-page-auth-required.test.tsx index 82b42fde6..829274ffc 100644 --- a/tests/frontend/with-page-auth-required.test.tsx +++ b/tests/frontend/with-page-auth-required.test.tsx @@ -75,12 +75,25 @@ describe('with-page-auth-required csr', () => { await waitFor(() => expect(screen.getByText('Error')).toBeInTheDocument()); }); - it('should accept a returnTo url', async () => { + it('should use a custom login url', async () => { + process.env.NEXT_PUBLIC_AUTH0_LOGIN = '/api/foo'; (global as any).fetch = fetchUserUnsuccessfulMock; const MyPage = (): JSX.Element => <>Private; - const ProtectedPage = withPageAuthRequired(MyPage, { returnTo: '/foo' }); + const ProtectedPage = withPageAuthRequired(MyPage); + + render(, { wrapper: withUserProvider() }); + await waitFor(() => expect(routerMock.push).toHaveBeenCalledWith(expect.stringContaining('/api/foo'))); + delete process.env.NEXT_PUBLIC_AUTH0_LOGIN; + }); + + it('should use a custom returnTo url', async () => { + process.env.NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT = '/foo'; + (global as any).fetch = fetchUserUnsuccessfulMock; + const MyPage = (): JSX.Element => <>Private; + const ProtectedPage = withPageAuthRequired(MyPage); render(, { wrapper: withUserProvider() }); await waitFor(() => expect(routerMock.push).toHaveBeenCalledWith(expect.stringContaining('?returnTo=/foo'))); + delete process.env.NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT; }); }); diff --git a/tests/helpers/with-page-auth-required.test.ts b/tests/helpers/with-page-auth-required.test.ts index 6794df377..34f54238a 100644 --- a/tests/helpers/with-page-auth-required.test.ts +++ b/tests/helpers/with-page-auth-required.test.ts @@ -26,12 +26,25 @@ describe('with-page-auth-required ssr', () => { }); test('use a custom login url', async () => { - const baseUrl = await setup(withoutApi, { withPageAuthRequiredOptions: { loginUrl: '/api/foo' } }); + process.env.NEXT_PUBLIC_AUTH0_LOGIN = '/api/foo'; + const baseUrl = await setup(withoutApi); const { res: { statusCode, headers } } = await get(baseUrl, '/protected', { fullResponse: true }); expect(statusCode).toBe(307); expect(headers.location).toBe('/api/foo?returnTo=/protected'); + delete process.env.NEXT_PUBLIC_AUTH0_LOGIN; + }); + + test('use a custom returnTo url', async () => { + process.env.NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT = '/foo'; + const baseUrl = await setup(withoutApi); + const { + res: { statusCode, headers } + } = await get(baseUrl, '/protected', { fullResponse: true }); + expect(statusCode).toBe(307); + expect(headers.location).toBe('/api/auth/login?returnTo=/foo'); + delete process.env.NEXT_PUBLIC_AUTH0_POST_LOGIN_REDIRECT; }); test('use custom server side props', async () => { From d02bcb7c7f866894b91d8b0c6547f4a345cd3b6e Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Sat, 23 Jan 2021 20:53:21 -0300 Subject: [PATCH 2/5] Change customise for customize --- EXAMPLES.md | 6 +++--- src/handlers/callback.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 021ae3da4..7285f26ad 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1,7 +1,7 @@ # Examples - [Basic Setup](#basic-setup) -- [Customise handlers behaviour](#customise-handlers-behaviour) +- [Customize handlers behaviour](#customize-handlers-behaviour) - [Use custom auth urls](#use-custom-auth-urls) - [Protecting a Server Side Rendered (SSR) Page](#protecting-a-server-side-rendered-ssr-page) - [Protecting a Client Side Rendered (CSR) Page](#protecting-a-client-side-rendered-csr-page) @@ -78,7 +78,7 @@ export default () => { Have a look at the `basic-example` app [./examples/basic-example](./examples/basic-example). -## Customise handlers behaviour +## Customize handlers behaviour Pass custom parameters to the auth handlers or add your own logging and error handling. @@ -132,7 +132,7 @@ export default async function login(req, res) { export default () => Login; ``` -> Note: If you customise the login URL you will need to set the environment variable `NEXT_PUBLIC_AUTH0_LOGIN` to this custom value for `withPageAuthRequired` to work correctly. +> Note: If you customize the login URL you will need to set the environment variable `NEXT_PUBLIC_AUTH0_LOGIN` to this custom value for `withPageAuthRequired` to work correctly. And if you customize the profile URL, you will need to set the `NEXT_PUBLIC_AUTH0_PROFILE` environment variable to this custom value for the `useUser` hook to work properly. ## Protecting a Server Side Rendered (SSR) Page diff --git a/src/handlers/callback.ts b/src/handlers/callback.ts index d494a603d..a601c0b6b 100644 --- a/src/handlers/callback.ts +++ b/src/handlers/callback.ts @@ -66,7 +66,7 @@ export type AfterCallback = ( ) => Promise | Session; /** - * Options to customise the callback handler. + * Options to customize the callback handler. * * @category Server */ From e8ca5b33e2c6463f869e08944d7d82a09a807ebe Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Sat, 23 Jan 2021 21:12:05 -0300 Subject: [PATCH 3/5] Add missing empty line --- tests/frontend/use-user.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/frontend/use-user.test.ts b/tests/frontend/use-user.test.ts index 7092188bd..09135b368 100644 --- a/tests/frontend/use-user.test.ts +++ b/tests/frontend/use-user.test.ts @@ -104,6 +104,7 @@ describe('context wrapper', () => { expect(fetchSpy).toHaveBeenCalledWith('/api/custom-url'); delete process.env.NEXT_PUBLIC_AUTH0_PROFILE; }); + test('should accept a custom login url', async () => { const { result } = renderHook(() => useConfig(), { wrapper: withUserProvider({ user, loginUrl: '/api/custom-url' }) From 7ccc619ed1be0f2aa76b0a133ec4155cd9afa4e2 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Sat, 23 Jan 2021 21:14:13 -0300 Subject: [PATCH 4/5] Update EXAMPLES.md --- EXAMPLES.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 7285f26ad..58e509559 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -132,8 +132,7 @@ export default async function login(req, res) { export default () => Login; ``` -> Note: If you customize the login URL you will need to set the environment variable `NEXT_PUBLIC_AUTH0_LOGIN` to this custom value for `withPageAuthRequired` to work correctly. -And if you customize the profile URL, you will need to set the `NEXT_PUBLIC_AUTH0_PROFILE` environment variable to this custom value for the `useUser` hook to work properly. +> Note: If you customize the login URL you will need to set the environment variable `NEXT_PUBLIC_AUTH0_LOGIN` to this custom value for `withPageAuthRequired` to work correctly. And if you customize the profile URL, you will need to set the `NEXT_PUBLIC_AUTH0_PROFILE` environment variable to this custom value for the `useUser` hook to work properly. ## Protecting a Server Side Rendered (SSR) Page From 77fedfb15ab52f44c2dc1f0098355f4d1a5d8d65 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Sat, 23 Jan 2021 21:49:34 -0300 Subject: [PATCH 5/5] Avoid unnecessary rerenders --- src/frontend/use-user.tsx | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/frontend/use-user.tsx b/src/frontend/use-user.tsx index 021c683b3..30f00716f 100644 --- a/src/frontend/use-user.tsx +++ b/src/frontend/use-user.tsx @@ -121,6 +121,15 @@ export const useUser: UseUser = () => useContext(User); */ export type UserProvider = (props: UserProviderProps) => ReactElement; +/** + * @ignore + */ +type UserProviderState = { + user?: UserProfile; + error?: Error; + isLoading: boolean; +}; + export default ({ children, user: initialUser, @@ -128,28 +137,28 @@ export default ({ loginUrl, returnTo }: UserProviderProps): ReactElement => { - const [user, setUser] = useState(initialUser); - const [error, setError] = useState(); - const [isLoading, setIsLoading] = useState(!initialUser); + const [state, setState] = useState({ user: initialUser, isLoading: !initialUser }); const checkSession = useCallback(async (): Promise => { try { const response = await fetch(profileUrl); - setUser(response.ok ? await response.json() : undefined); - setError(undefined); + const user = response.ok ? await response.json() : undefined; + setState((previous) => ({ ...previous, user, error: undefined })); } catch (_e) { - setUser(undefined); - setError(new Error(`The request to ${profileUrl} failed`)); + const error = new Error(`The request to ${profileUrl} failed`); + setState((previous) => ({ ...previous, user: undefined, error })); } }, [profileUrl]); useEffect((): void => { - if (user) return; + if (state.user) return; (async (): Promise => { await checkSession(); - setIsLoading(false); + setState((previous) => ({ ...previous, isLoading: false })); })(); - }, [user]); + }, [state.user]); + + const { user, error, isLoading } = state; return (