Repository: auth0/nextjs-auth0
Pull request #268 (2021-02-02 03:45:32)

Simplified configuration of custom login URL [SDK-2264]
### Description

This PR makes it possible to set a custom login URL for the server-side version of `withPageAuthRequired` with an environment variable. This way, the developer only has to configure it once, instead of in every call site of `withPageAuthRequired`.

Also, the server-side version of `withPageAuthRequired` wasn't taking the `returnTo` parameter that the client-side takes. This was corrected and both versions offer the same public-facing configuration options.

### References

Supercedes https://github.com/auth0/nextjs-auth0/pull/253

### Testing

- [X] This change adds test coverage for new/changed/fixed functionality

### Checklist

- [ ] I have added documentation for new/changed functionality in this PR or in auth0.com/docs
- [X] All active GitHub checks for tests, formatting, and security are passing
- [X] The correct base branch is being used, if not `master`


vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/auth0/nextjs-auth0/pnwjfs5a9](https://vercel.com/auth0/nextjs-auth0/pnwjfs5a9)  
✅ Preview: *Canceled*


adamjmcgrath: Looks good 👍 Just need to figure out a different solution to that special case with the named `withPageAuthRequired` export

Patch:
diff --git a/src/config.ts b/src/config.ts
index 40d6e67d..0d5edaeb 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -278,7 +278,11 @@ export interface AuthorizationParameters extends OidcAuthorizationParameters {
 /**
  * @ignore
  */
-export type NextConfig = Pick<BaseConfig, 'routes' | 'identityClaimFilter'>;
+export interface NextConfig extends Pick<BaseConfig, 'identityClaimFilter'> {
+  routes: {
+    login: string;
+  };
+}
 
 /**
  * ## Configuration properties.
@@ -317,8 +321,9 @@ export type NextConfig = Pick<BaseConfig, 'routes' | 'identityClaimFilter'>;
  * - `AUTH0_IDP_LOGOUT`: See {@link idpLogout}
  * - `AUTH0_ID_TOKEN_SIGNING_ALG`: See {@link idTokenSigningAlg}
  * - `AUTH0_LEGACY_SAME_SITE_COOKIE`: See {@link legacySameSiteCookie}
- * - `AUTH0_POST_LOGOUT_REDIRECT`: See {@link BaseConfig.routes}
+ * - `NEXT_PUBLIC_AUTH0_LOGIN`: See {@link NextConfig.routes}
  * - `AUTH0_CALLBACK`: See {@link BaseConfig.routes}
+ * - `AUTH0_POST_LOGOUT_REDIRECT`: See {@link BaseConfig.routes}
  * - `AUTH0_AUDIENCE`: See {@link BaseConfig.authorizationParams}
  * - `AUTH0_SCOPE`: See {@link BaseConfig.authorizationParams}
  * - `AUTH0_SESSION_NAME`: See {@link SessionConfig.name}
@@ -397,8 +402,8 @@ export const getConfig = (params?: ConfigParameters): { baseConfig: BaseConfig;
     AUTH0_IDP_LOGOUT,
     AUTH0_ID_TOKEN_SIGNING_ALG,
     AUTH0_LEGACY_SAME_SITE_COOKIE,
-    AUTH0_POST_LOGOUT_REDIRECT,
     AUTH0_CALLBACK,
+    AUTH0_POST_LOGOUT_REDIRECT,
     AUTH0_AUDIENCE,
     AUTH0_SCOPE,
     AUTH0_SESSION_NAME,
@@ -463,11 +468,19 @@ export const getConfig = (params?: ConfigParameters): { baseConfig: BaseConfig;
 
   const nextConfig = {
     routes: {
-      ...baseConfig.routes
+      ...baseConfig.routes,
       // Other NextConfig Routes go here
+      login: params?.routes?.login || getLoginUrl()
     },
     identityClaimFilter: baseConfig.identityClaimFilter
   };
 
   return { baseConfig, nextConfig };
 };
+
+/**
+ * @ignore
+ */
+export const getLoginUrl = (): string => {
+  return process.env.NEXT_PUBLIC_AUTH0_LOGIN || '/api/auth/login';
+};
diff --git a/src/frontend/use-user.tsx b/src/frontend/use-user.tsx
index c56d1d86..3eec6c72 100644
--- a/src/frontend/use-user.tsx
+++ b/src/frontend/use-user.tsx
@@ -33,7 +33,7 @@ export type UserContext = {
 /**
  * Configure the {@link UserProvider} component.
  *
- * If you have any server side rendered pages (eg. using `getServerSideProps`), you should get the user from the server
+ * If you have any server-side rendered pages (eg. using `getServerSideProps`), you should get the user from the server
  * side session and pass it to the `<UserProvider>` component via `pageProps` - this will refill the {@link useUser}
  * hook with the {@link UserProfile} object. eg
  *
@@ -43,8 +43,8 @@ export type UserContext = {
  * import { UserProvider } from '@auth0/nextjs-auth0';
  *
  * export default function App({ Component, pageProps }) {
- *   // If you've used `withAuth`, pageProps.user can pre-populate the hook
- *   // if you haven't used `withAuth`, pageProps.user is undefined so the hook
+ *   // If you've used `withPageAuthRequired`, pageProps.user can pre-populate the hook
+ *   // if you haven't used `withPageAuthRequired`, pageProps.user is undefined so the hook
  *   // fetches the user from the API route
  *   const { user } = pageProps;
  *
@@ -87,7 +87,7 @@ const User = createContext<UserContext>({
 });
 
 /**
- * The `useUser` hook, which will get you the {@link UserProfile} object from the server side session by requesting it
+ * The `useUser` hook, which will get you the {@link UserProfile} object from the server-side session by requesting it
  * from the {@link HandleProfile} API Route handler.
  *
  * ```js
diff --git a/src/handlers/profile.ts b/src/handlers/profile.ts
index 9df236d5..23988054 100644
--- a/src/handlers/profile.ts
+++ b/src/handlers/profile.ts
@@ -26,9 +26,9 @@ export type HandleProfile = (req: NextApiRequest, res: NextApiResponse, options?
  * @ignore
  */
 export default function profileHandler(
-  sessionCache: SessionCache,
   getClient: ClientFactory,
-  getAccessToken: GetAccessToken
+  getAccessToken: GetAccessToken,
+  sessionCache: SessionCache
 ): HandleProfile {
   return async (req, res, options): Promise<void> => {
     assertReqRes(req, res);
diff --git a/src/helpers/with-page-auth-required.ts b/src/helpers/with-page-auth-required.ts
index 9e0063be..1e2f6a60 100644
--- a/src/helpers/with-page-auth-required.ts
+++ b/src/helpers/with-page-auth-required.ts
@@ -35,7 +35,7 @@ export type GetServerSidePropsResultWithSession = GetServerSidePropsResult<{
 export type PageRoute = (cts: GetServerSidePropsContext) => Promise<GetServerSidePropsResultWithSession>;
 
 /**
- * If you have a custom login url (the default is `/api/auth/login`) you should specify it in `loginUrl`.
+ * If you have a custom returnTo url you should specify it in `returnTo`.
  *
  * You can pass in your own `getServerSideProps` method, the props returned from thsi will be merged with the
  * user props, eg:
@@ -49,16 +49,16 @@ export type PageRoute = (cts: GetServerSidePropsContext) => Promise<GetServerSid
  * }
  *
  * export const getServerSideProps = withPageAuthRequired({
- *   loginUrl: '/api/auth/login',
+ *   returnTo: '/foo',
  *   async getServerSideProps(ctx) {
- *     return { props: { customProp: 'foo' } };
+ *     return { props: { customProp: 'bar' } };
  *   }
  * });
  * ```
  *
  * @category Server
  */
-export type WithPageAuthRequiredOptions = { getServerSideProps?: GetServerSideProps; loginUrl?: string };
+export type WithPageAuthRequiredOptions = { getServerSideProps?: GetServerSideProps; returnTo?: string };
 
 /**
  * Wrap your `getServerSideProps` with this method to make sure the user is authenticated before visiting the page.
@@ -90,7 +90,7 @@ export type WithPageAuthRequired = {
 /**
  * @ignore
  */
-export default function withPageAuthRequiredFactory(getSession: GetSession): WithPageAuthRequired {
+export default function withPageAuthRequiredFactory(loginUrl: string, getSession: GetSession): WithPageAuthRequired {
   return (
     optsOrComponent: WithPageAuthRequiredOptions | ComponentType = {},
     csrOpts?: WithPageAuthRequiredCSROptions
@@ -98,7 +98,7 @@ export default function withPageAuthRequiredFactory(getSession: GetSession): Wit
     if (typeof optsOrComponent === 'function') {
       return withPageAuthRequiredCSR(optsOrComponent, csrOpts);
     }
-    const { getServerSideProps, loginUrl = '/api/auth/login' } = optsOrComponent;
+    const { getServerSideProps, returnTo } = optsOrComponent;
     return async (ctx: GetServerSidePropsContext): Promise<GetServerSidePropsResultWithSession> => {
       assertCtx(ctx);
       const session = getSession(ctx.req, ctx.res);
@@ -106,7 +106,7 @@ export default function withPageAuthRequiredFactory(getSession: GetSession): Wit
         // 10 - redirect
         // 9.5.4 - unstable_redirect
         // 9.4 - res.setHeaders
-        return { redirect: { destination: `${loginUrl}?returnTo=${ctx.resolvedUrl}`, permanent: false } };
+        return { redirect: { destination: `${loginUrl}?returnTo=${returnTo || ctx.resolvedUrl}`, permanent: false } };
       }
       let ret: any = { props: {} };
       if (getServerSideProps) {
diff --git a/src/index.ts b/src/index.ts
index c31f5152..db6a3b7c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -47,7 +47,7 @@ import {
 } from './helpers';
 import { InitAuth0, SignInWithAuth0 } from './instance';
 import version from './version';
-import { getConfig, ConfigParameters } from './config';
+import { getConfig, getLoginUrl, ConfigParameters } from './config';
 
 let instance: SignInWithAuth0;
 
@@ -73,13 +73,13 @@ export const initAuth0: InitAuth0 = (params) => {
 
   // Init Next layer (with next config)
   const getSession = sessionFactory(sessionCache);
-  const getAccessToken = accessTokenFactory(getClient, nextConfig, sessionCache);
+  const getAccessToken = accessTokenFactory(nextConfig, getClient, sessionCache);
   const withApiAuthRequired = withApiAuthRequiredFactory(sessionCache);
-  const withPageAuthRequired = withPageAuthRequiredFactory(getSession);
+  const withPageAuthRequired = withPageAuthRequiredFactory(nextConfig.routes.login, getSession);
   const handleLogin = loginHandler(baseHandleLogin);
   const handleLogout = logoutHandler(baseHandleLogout);
   const handleCallback = callbackHandler(baseHandleCallback);
-  const handleProfile = profileHandler(sessionCache, getClient, getAccessToken);
+  const handleProfile = profileHandler(getClient, getAccessToken, sessionCache);
   const handleAuth = handlerFactory({ handleLogin, handleLogout, handleCallback, handleProfile });
 
   return {
@@ -99,7 +99,7 @@ export const getSession: GetSession = (...args) => getInstance().getSession(...a
 export const getAccessToken: GetAccessToken = (...args) => getInstance().getAccessToken(...args);
 export const withApiAuthRequired: WithApiAuthRequired = (...args) => getInstance().withApiAuthRequired(...args);
 export const withPageAuthRequired: WithPageAuthRequired = (...args: any[]): any =>
-  withPageAuthRequiredFactory(getSession)(...args);
+  withPageAuthRequiredFactory(getLoginUrl(), getSession)(...args);
 export const handleLogin: HandleLogin = (...args) => getInstance().handleLogin(...args);
 export const handleLogout: HandleLogout = (...args) => getInstance().handleLogout(...args);
 export const handleCallback: HandleCallback = (...args) => getInstance().handleCallback(...args);
diff --git a/src/session/get-access-token.ts b/src/session/get-access-token.ts
index 1926594d..9c911b7a 100644
--- a/src/session/get-access-token.ts
+++ b/src/session/get-access-token.ts
@@ -51,8 +51,8 @@ export type GetAccessToken = (
  * @ignore
  */
 export default function accessTokenFactory(
-  getClient: ClientFactory,
   config: NextConfig,
+  getClient: ClientFactory,
   sessionCache: SessionCache
 ): GetAccessToken {
   return async (req, res, accessTokenRequest): Promise<GetAccessTokenResult> => {


Test patch:
diff --git a/tests/config.test.ts b/tests/config.test.ts
index d4ea8882..3095d579 100644
--- a/tests/config.test.ts
+++ b/tests/config.test.ts
@@ -88,7 +88,11 @@ describe('config params', () => {
         'at_hash',
         'c_hash'
       ],
-      routes: { callback: '/api/auth/callback', postLogoutRedirect: '' }
+      routes: {
+        login: '/api/auth/login',
+        callback: '/api/auth/callback',
+        postLogoutRedirect: ''
+      }
     });
   });
 
diff --git a/tests/fixtures/test-app/pages/_document.tsx b/tests/fixtures/test-app/pages/_document.tsx
index 9ee76ef7..808ff08a 100644
--- a/tests/fixtures/test-app/pages/_document.tsx
+++ b/tests/fixtures/test-app/pages/_document.tsx
@@ -1,7 +1,6 @@
 import React from 'react';
 import Document from 'next/document';
 
-// TODO: Stubbing out the document to resolve "Invalid hook call"
 class MyDocument extends Document {
   static getInitialProps(ctx): any {
     return Document.getInitialProps(ctx);
diff --git a/tests/helpers/with-page-auth-required.test.ts b/tests/helpers/with-page-auth-required.test.ts
index 6794df37..5765e8e6 100644
--- a/tests/helpers/with-page-auth-required.test.ts
+++ b/tests/helpers/with-page-auth-required.test.ts
@@ -25,16 +25,16 @@ describe('with-page-auth-required ssr', () => {
     expect(data).toMatch(/<div>Blank Document<\/div>/);
   });
 
-  test('use a custom login url', async () => {
-    const baseUrl = await setup(withoutApi, { withPageAuthRequiredOptions: { loginUrl: '/api/foo' } });
+  test('accept a custom returnTo url', async () => {
+    const baseUrl = await setup(withoutApi, { withPageAuthRequiredOptions: { returnTo: '/foo' } });
     const {
       res: { statusCode, headers }
     } = await get(baseUrl, '/protected', { fullResponse: true });
     expect(statusCode).toBe(307);
-    expect(headers.location).toBe('/api/foo?returnTo=/protected');
+    expect(headers.location).toBe('/api/auth/login?returnTo=/foo');
   });
 
-  test('use custom server side props', async () => {
+  test('accept custom server-side props', async () => {
     const spy = jest.fn().mockReturnValue({ props: {} });
     const baseUrl = await setup(withoutApi, {
       withPageAuthRequiredOptions: {
@@ -49,7 +49,18 @@ describe('with-page-auth-required ssr', () => {
     expect(spy).toHaveBeenCalledWith(expect.objectContaining({ req: expect.anything(), res: expect.anything() }));
   });
 
-  test('is a noop when invoked as a client side protection from the server', async () => {
+  test('use a custom login url', async () => {
+    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('is a noop when invoked as a client-side protection from the server', async () => {
     const baseUrl = await setup(withoutApi);
     const cookieJar = await login(baseUrl);
     const {
diff --git a/tests/session/session.test.ts b/tests/session/session.test.ts
index 5b845683..faed75bb 100644
--- a/tests/session/session.test.ts
+++ b/tests/session/session.test.ts
@@ -12,7 +12,7 @@ describe('session', () => {
     expect(
       fromTokenSet(new TokenSet({ id_token: makeIdToken({ foo: 'bar', bax: 'qux' }) }), {
         identityClaimFilter: ['baz'],
-        routes: { callback: '', postLogoutRedirect: '' }
+        routes: { login: '', callback: '', postLogoutRedirect: '' }
       }).user
     ).toEqual({
       aud: '__test_client_id__',