Repository: auth0/nextjs-auth0
Pull request #840 (2022-10-06 04:25:14)

Add support for configuring the default handlers [SDK-3566]
<!--
❗ For general support or usage questions, use the Auth0 Community forums or raise a support ticket.

By submitting a pull request to this repository, you agree to the terms within the Auth0 Code of Conduct: https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md.
-->

- [X] All new/changed/fixed functionality is covered by tests (or N/A)
- [X] I have added documentation for all new/changed functionality (or N/A)

<!--
❗ All the above items are required. Pull requests with an incomplete or missing checklist will be closed.
-->

### 📋 Changes

Previously it was not possible to configure the default handlers. For example, to pass a `connection` parameter to the login handler, it had to be overridden.

```js
export default handleAuth({
  login: async (req, res) => {
    try {
      await handleLogin(req, res, {
        authorizationParams: { connection: 'github' },
      });
    } catch (error) {
      // ...
    }
  }
});
```

This PR allows to configure default handlers by passing an options object to them. This is the same options object that can be passed when overriding a handler.

```js
export default handleAuth({
  // Works with all default handlers: login, logout, callback, profile
  login: handleLogin({
    authorizationParams: { connection: 'github' }
  })
});
```

Alternatively, it is possible to pass a function that receives the request and returns an options object.

```js
export default handleAuth({
  login: handleLogin((req) => {
    return {
      authorizationParams: { connection: 'github' }
    };
  })
});
```

This PR also adds the ability to create new handlers by configuring the default ones, which is useful e.g. for easily creating a signup handler.

```js
export default handleAuth({
  // Creates /api/auth/signup
  signup: handleLogin({
    authorizationParams: { screen_hint: 'signup' }
  })
});
```

### 📎 References

Fixes https://github.com/auth0/nextjs-auth0/issues/379, https://github.com/auth0/nextjs-auth0/issues/381, https://github.com/auth0/nextjs-auth0/issues/400, https://github.com/auth0/nextjs-auth0/issues/471, https://github.com/auth0/nextjs-auth0/issues/701

### 🎯 Testing

Besides adding unit tests, the changes were tested manually using the kitchen sink example app.


Patch:
diff --git a/EXAMPLES.md b/EXAMPLES.md
index ca449282..e0211413 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -91,21 +91,27 @@ import { myCustomLogger, myCustomErrorReporter } from '../utils';
 
 export default handleAuth({
   async login(req, res) {
-    try {
-      // Add your own custom logger
-      myCustomLogger('Logging in');
-      // Pass custom parameters to login
-      await handleLogin(req, res, {
-        authorizationParams: {
-          custom_param: 'custom'
-        },
-        returnTo: '/custom-page'
-      });
-    } catch (error) {
-      // Add your own custom error handling
-      myCustomErrorReporter(error);
-      res.status(error.status || 400).end(error.message);
+    // Add your own custom logger
+    myCustomLogger('Logging in');
+    // Pass custom parameters to login
+    await handleLogin(req, res, {
+      authorizationParams: {
+        custom_param: 'custom'
+      },
+      returnTo: '/custom-page'
+    });
+  },
+  invite: loginHandler({
+    authorizationParams: (req) => {
+      invitation: req.query.invitation;
     }
+  }),
+  'login-with-google': loginHandler({ authorizationParams: { connection: 'google' } }),
+  'refresh-profile': profileHandler({ refetch: true }),
+  onError(req, res, error) {
+    // Add your own custom error handling
+    myCustomErrorReporter(error);
+    res.status(error.status || 400).end();
   }
 });
 ```
@@ -258,19 +264,13 @@ Get an Access Token by providing your API's audience and scopes. You can pass th
 import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';
 
 export default handleAuth({
-  async login(req, res) {
-    try {
-      await handleLogin(req, res, {
-        authorizationParams: {
-          audience: 'https://api.example.com/products', // or AUTH0_AUDIENCE
-          // Add the `offline_access` scope to also get a Refresh Token
-          scope: 'openid profile email read:products' // or AUTH0_SCOPE
-        }
-      });
-    } catch (error) {
-      res.status(error.status || 400).end(error.message);
+  login: handleLogin({
+    authorizationParams: {
+      audience: 'https://api.example.com/products', // or AUTH0_AUDIENCE
+      // Add the `offline_access` scope to also get a Refresh Token
+      scope: 'openid profile email read:products' // or AUTH0_SCOPE
     }
-  }
+  })
 });
 ```
 
@@ -378,41 +378,16 @@ Pass a custom authorize parameter to the login handler in a custom route.
 If you are using the [New Universal Login Experience](https://auth0.com/docs/universal-login/new-experience) you can pass the `screen_hint` parameter.
 
 ```js
-// api/signup.js
-import { handleLogin } from '@auth0/nextjs-auth0';
-
-export default async function signup(req, res) {
-  try {
-    await handleLogin(req, res, {
-      authorizationParams: {
-        // Note that this can be combined with prompt=login , which indicates if
-        // you want to always show the authentication page or you want to skip
-        // if there’s an existing session.
-        screen_hint: 'signup'
-      }
-    });
-  } catch (error) {
-    res.status(error.status || 400).end(error.message);
-  }
-}
-```
-
-If you are using the [Classic Universal Login Experience](https://auth0.com/docs/universal-login/classic-experience) you can use any custom authorization
-parameter, eg `{ authorizationParams: { action: 'signup' } }` then customize the
-[login template](https://manage.auth0.com/#/login_page) to look for this parameter
-and set the `initialScreen` option of the `Auth0Lock` constructor.
+// pages/api/auth/[...auth0].js
+import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';
 
-```js
-var isSignup = config.extraParams && config.extraParams.action === 'signup';
-var lock = new Auth0Lock(config.clientID, config.auth0Domain, {
-  // [...] all other Lock options
-  // use the value obtained to decide the first screen
-  initialScreen: isSignup ? 'signUp' : 'login'
+export default handleAuth({
+  signup: handleLogin({ authorizationParams: { screen_hint: 'signup' } })
 });
 ```
 
 Users can then sign up using the signup handler.
 
 ```html
-<a href="/api/signup">Sign up</a>
+<a href="/api/auth/signup">Sign up</a>
 ```
diff --git a/V2_MIGRATION_GUIDE.md b/V2_MIGRATION_GUIDE.md
index 44e359dc..81290b54 100644
--- a/V2_MIGRATION_GUIDE.md
+++ b/V2_MIGRATION_GUIDE.md
@@ -8,7 +8,8 @@ Guide to migrating from `1.x` to `2.x`
 - [Profile API route no longer returns a 401](#profile-api-route-no-longer-returns-a-401)
 - [The ID token is no longer stored by default](#the-id-token-is-no-longer-stored-by-default)
 - [Override default error handler](#override-default-error-handler)
-- [afterCallback can write to the response](#afterCallback-can-write-to-the-response)
+- [afterCallback can write to the response](#aftercallback-can-write-to-the-response)
+- [Configure default handlers](#configure-default-handlers)
 
 ## `getSession` now returns a `Promise`
 
@@ -120,6 +121,8 @@ You can choose to store it by setting either the `session.storeIDToken` config p
 
 You can now set the default error handler for the auth routes in a single place.
 
+### Before
+
 ```js
 export default handleAuth({
   async login(req, res) {
@@ -160,7 +163,7 @@ export default handleAuth({
 
 ## `afterCallback` can write to the response
 
-You can now write your own redirect header or terminate the request in `afterCallback`
+You can now write your own redirect header or terminate the request in `afterCallback`.
 
 ### Before
 
@@ -171,14 +174,14 @@ const afterCallback = (req, res, session, state) => {
   } else {
     res.status(401).end('User is not admin');
   }
-}; // 💥Fail with ERR_HTTP_HEADERS_SENT
+}; // 💥 Fails with ERR_HTTP_HEADERS_SENT
 
 const afterCallback = (req, res, session, state) => {
   if (!session.user.isAdmin) {
     res.setHeader('Location', '/admin');
   }
   return session;
-}; // 💥Fail with ERR_HTTP_HEADERS_SENT
+}; // 💥 Fails with ERR_HTTP_HEADERS_SENT
 ```
 
 ### After
@@ -199,3 +202,60 @@ const afterCallback = (req, res, session, state) => {
   return session;
 }; // Redirects to `/admin` if user is admin
 ```
+
+## Configure default handlers
+
+Previously it was not possible to configure the default handlers. For example, to pass a `connection` parameter to the login handler, you had to override it.
+
+### Before
+
+```js
+export default handleAuth({
+  login: async (req, res) => {
+    try {
+      await handleLogin(req, res, {
+        authorizationParams: { connection: 'github' },
+      });
+    } catch (error) {
+      // ...
+    }
+  }
+});
+```
+
+### After
+
+Now you can configure a default handler by passing an options object to it.
+
+```js
+export default handleAuth({
+  login: handleLogin({
+    authorizationParams: { connection: 'github' }
+  })
+});
+```
+
+You can also pass a function that receives the request and returns an options object.
+
+```js
+export default handleAuth({
+  login: handleLogin((req) => {
+    return {
+      authorizationParams: { connection: 'github' }
+    };
+  })
+});
+```
+
+You can even create new handlers by configuring the default ones.
+
+```js
+export default handleAuth({
+  // Creates /api/auth/signup
+  signup: handleLogin({
+    authorizationParams: { screen_hint: 'signup' }
+  })
+});
+```
+
+It is still possible to override the default handlers if needed.
diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts
index c6b001d8..f9949bc0 100644
--- a/src/handlers/auth.ts
+++ b/src/handlers/auth.ts
@@ -1,8 +1,8 @@
+import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
 import { HandleLogin } from './login';
 import { HandleLogout } from './logout';
 import { HandleCallback } from './callback';
 import { HandleProfile } from './profile';
-import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
 import { HandlerError } from '../utils/errors';
 
 /**
@@ -24,28 +24,55 @@ import { HandlerError } from '../utils/errors';
  *     } catch (error) {
  *       // Add you own custom error logging.
  *       errorReporter(error);
- *       res.status(error.status || 500).end(error.message);
+ *       res.status(error.status || 500).end();
  *     }
  *   }
  * });
  * ```
  *
+ * Alternatively, you can customize the default handlers without overriding them. For example:
+ *
+ * ```js
+ * // pages/api/auth/[...auth0].js
+ * import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   login: handleLogin({
+ *     authorizationParams: { customParam: 'foo' } // Pass in custom params
+ *   })
+ * });
+ * ```
+ *
+ * You can also create new handlers by customizing the default ones. For example:
+ *
+ * ```js
+ * // pages/api/auth/[...auth0].js
+ * import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   signup: handleLogin({
+ *     authorizationParams: { screen_hint: 'signup' }
+ *   })
+ * });
+ * ```
+ *
  * @category Server
  */
-export interface Handlers {
-  login: HandleLogin;
-  logout: HandleLogout;
-  callback: HandleCallback;
-  profile: HandleProfile;
-  onError: OnError;
-}
+export type Handlers = ApiHandlers | ErrorHandlers;
+
+type ApiHandlers = {
+  [key: string]: NextApiHandler;
+};
+
+type ErrorHandlers = {
+  onError?: OnError;
+};
 
 /**
  * The main way to use the server SDK.
  *
  * Simply set the environment variables per {@link ConfigParameters} then create the file
- * `pages/api/auth/[...auth0].js`.
- * For example:
+ * `pages/api/auth/[...auth0].js`. For example:
  *
  * ```js
  * // pages/api/auth/[...auth0].js
@@ -63,7 +90,7 @@ export interface Handlers {
  *
  * @category Server
  */
-export type HandleAuth = (userHandlers?: Partial<Handlers>) => NextApiHandler;
+export type HandleAuth = (userHandlers?: Handlers) => NextApiHandler;
 
 export type OnError = (req: NextApiRequest, res: NextApiResponse, error: HandlerError) => Promise<void> | void;
 
@@ -89,12 +116,12 @@ export default function handlerFactory({
   handleCallback: HandleCallback;
   handleProfile: HandleProfile;
 }): HandleAuth {
-  return ({ onError, ...handlers }: Partial<Handlers> = {}): NextApiHandler<void> => {
-    const { login, logout, callback, profile } = {
+  return ({ onError, ...handlers }: Handlers = {}): NextApiHandler<void> => {
+    const customHandlers: ApiHandlers = {
       login: handleLogin,
       logout: handleLogout,
       callback: handleCallback,
-      profile: handleProfile,
+      me: (handlers as ApiHandlers).profile || handleProfile,
       ...handlers
     };
     return async (req, res): Promise<void> => {
@@ -105,20 +132,14 @@ export default function handlerFactory({
       route = Array.isArray(route) ? route[0] : /* c8 ignore next */ route;
 
       try {
-        switch (route) {
-          case 'login':
-            return await login(req, res);
-          case 'logout':
-            return await logout(req, res);
-          case 'callback':
-            return await callback(req, res);
-          case 'me':
-            return await profile(req, res);
-          default:
-            res.status(404).end();
+        const handler = route && customHandlers[route];
+        if (handler) {
+          await handler(req, res);
+        } else {
+          res.status(404).end();
         }
       } catch (error) {
-        await (onError || defaultOnError)(req, res, error);
+        await (onError || defaultOnError)(req, res, error as HandlerError);
         if (!res.writableEnded) {
           // 200 is the default, so we assume it has not been set in the custom error handler if it equals 200
           res.status(res.statusCode === 200 ? 500 : res.statusCode).end();
diff --git a/src/handlers/callback.ts b/src/handlers/callback.ts
index 577c16de..1f0cbff9 100644
--- a/src/handlers/callback.ts
+++ b/src/handlers/callback.ts
@@ -1,3 +1,4 @@
+import { IncomingMessage } from 'http';
 import { strict as assert } from 'assert';
 import { NextApiResponse, NextApiRequest } from 'next';
 import { AuthorizationParameters, HandleCallback as BaseHandleCallback } from '../auth0-session';
@@ -29,7 +30,7 @@ import { CallbackHandlerError, HandlerErrorCause } from '../utils/errors';
  *     try {
  *       await handleCallback(req, res, { afterCallback });
  *     } catch (error) {
- *       res.status(error.status || 500).end(error.message);
+ *       res.status(error.status || 500).end();
  *     }
  *   }
  * });
@@ -52,7 +53,7 @@ import { CallbackHandlerError, HandlerErrorCause } from '../utils/errors';
  *     try {
  *       await handleCallback(req, res, { afterCallback });
  *     } catch (error) {
- *       res.status(error.status || 500).end(error.message);
+ *       res.status(error.status || 500).end();
  *     }
  *   }
  * });
@@ -116,11 +117,76 @@ export interface CallbackOptions {
   organization?: string;
 
   /**
-   * This is useful for sending custom query parameters in the body of the code exchange request for use in rules.
+   * This is useful for sending custom query parameters in the body of the code exchange request
+   * for use in Actions/Rules.
    */
   authorizationParams?: Partial<AuthorizationParameters>;
 }
 
+/**
+ * Options provider for the default callback handler.
+ * Use this to generate options that depend on values from the request.
+ *
+ * @category Server
+ */
+export type CallbackOptionsProvider = (req: NextApiRequest) => CallbackOptions;
+
+/**
+ * Use this to customize the default callback handler without overriding it.
+ * You can still override the handler if needed.
+ *
+ * @example Pass an options object
+ *
+ * ```js
+ * // pages/api/auth/[...auth0].js
+ * import { handleAuth, handleCallback } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   callback: handleCallback({ redirectUri: 'https://example.com' })
+ * });
+ * ```
+ *
+ * @example Pass a function that receives the request and returns an options object
+ *
+ * ```js
+ * // pages/api/auth/[...auth0].js
+ * import { handleAuth, handleCallback } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   callback: handleCallback((req) => {
+ *     return { redirectUri: 'https://example.com' };
+ *   })
+ * });
+ * ```
+ *
+ * This is useful for generating options that depend on values from the request.
+ *
+ * @example Override the callback handler
+ *
+ * ```js
+ * import { handleAuth, handleCallback } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   callback: async (req, res) => {
+ *     try {
+ *       await handleCallback(req, res, {
+ *         redirectUri: 'https://example.com'
+ *       });
+ *     } catch (error) {
+ *       console.error(error);
+ *     }
+ *   }
+ * });
+ * ```
+ *
+ * @category Server
+ */
+export type HandleCallback = {
+  (req: NextApiRequest, res: NextApiResponse, options?: CallbackOptions): Promise<void>;
+  (provider: CallbackOptionsProvider): CallbackHandler;
+  (options: CallbackOptions): CallbackHandler;
+};
+
 /**
  * The handler for the `/api/auth/callback` API route.
  *
@@ -128,7 +194,7 @@ export interface CallbackOptions {
  *
  * @category Server
  */
-export type HandleCallback = (req: NextApiRequest, res: NextApiResponse, options?: CallbackOptions) => Promise<void>;
+export type CallbackHandler = (req: NextApiRequest, res: NextApiResponse, options?: CallbackOptions) => Promise<void>;
 
 /**
  * @ignore
@@ -155,7 +221,7 @@ const idTokenValidator =
  * @ignore
  */
 export default function handleCallbackFactory(handler: BaseHandleCallback, config: NextConfig): HandleCallback {
-  return async (req, res, options = {}): Promise<void> => {
+  const callback: CallbackHandler = async (req: NextApiRequest, res: NextApiResponse, options = {}): Promise<void> => {
     try {
       assertReqRes(req, res);
       return await handler(req, res, {
@@ -166,4 +232,17 @@ export default function handleCallbackFactory(handler: BaseHandleCallback, confi
       throw new CallbackHandlerError(e as HandlerErrorCause);
     }
   };
+  return (
+    reqOrOptions: NextApiRequest | CallbackOptionsProvider | CallbackOptions,
+    res?: NextApiResponse,
+    options?: CallbackOptions
+  ): any => {
+    if (reqOrOptions instanceof IncomingMessage && res) {
+      return callback(reqOrOptions, res, options);
+    }
+    if (typeof reqOrOptions === 'function') {
+      return (req: NextApiRequest, res: NextApiResponse) => callback(req, res, reqOrOptions(req));
+    }
+    return (req: NextApiRequest, res: NextApiResponse) => callback(req, res, reqOrOptions as CallbackOptions);
+  };
 }
diff --git a/src/handlers/login.ts b/src/handlers/login.ts
index 9cd2d5ec..5d1bbc99 100644
--- a/src/handlers/login.ts
+++ b/src/handlers/login.ts
@@ -1,3 +1,4 @@
+import { IncomingMessage } from 'http';
 import { NextApiResponse, NextApiRequest } from 'next';
 import { AuthorizationParameters, HandleLogin as BaseHandleLogin } from '../auth0-session';
 import toSafeRedirect from '../utils/url-helpers';
@@ -21,7 +22,7 @@ import { HandlerErrorCause, LoginHandlerError } from '../utils/errors';
  *     try {
  *       await handleLogin(req, res, { getLoginState });
  *     } catch (error) {
- *       res.status(error.status || 500).end(error.message);
+ *       res.status(error.status || 500).end();
  *     }
  *   }
  * });
@@ -57,6 +58,7 @@ export interface AuthorizationParams extends Partial<AuthorizationParameters> {
    *     }
    *   }
    * });
+   * ```
    */
   connection?: string;
 
@@ -80,6 +82,7 @@ export interface AuthorizationParams extends Partial<AuthorizationParameters> {
    *     }
    *   }
    * });
+   * ```
    */
   connection_scope?: string;
 
@@ -106,13 +109,13 @@ export interface AuthorizationParams extends Partial<AuthorizationParameters> {
    *       }
    *     });
    *   } catch (error) {
-   *     res.status(error.status || 500).end(error.message);
+   *     res.status(error.status || 500).end();
    *   }
    * } ;
    * ```
    *
    * Your invite url can then take the format:
-   * `https://example.com/api/invite?invitation=invitation_id&organization=org_id`
+   * `https://example.com/api/invite?invitation=invitation_id&organization=org_id`.
    */
   invitation?: string;
 
@@ -155,6 +158,74 @@ export interface LoginOptions {
   getLoginState?: GetLoginState;
 }
 
+/**
+ * Options provider for the default login handler.
+ * Use this to generate options that depend on values from the request.
+ *
+ * @category Server
+ */
+export type LoginOptionsProvider = (req: NextApiRequest) => LoginOptions;
+
+/**
+ * Use this to customize the default login handler without overriding it.
+ * You can still override the handler if needed.
+ *
+ * @example Pass an options object
+ *
+ * ```js
+ * // pages/api/auth/[...auth0].js
+ * import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   login: handleLogin({
+ *     authorizationParams: { connection: 'github' }
+ *   })
+ * });
+ * ```
+ *
+ * @example Pass a function that receives the request and returns an options object
+ *
+ * ```js
+ * // pages/api/auth/[...auth0].js
+ * import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   login: handleLogin((req) => {
+ *     return {
+ *       authorizationParams: { connection: 'github' }
+ *     };
+ *   })
+ * });
+ * ```
+ *
+ * This is useful for generating options that depend on values from the request.
+ *
+ * @example Override the login handler
+ *
+ * ```js
+ * import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   login: async (req, res) => {
+ *     try {
+ *       await handleLogin(req, res, {
+ *         authorizationParams: { connection: 'github' }
+ *       });
+ *     } catch (error) {
+ *       console.error(error);
+ *     }
+ *   }
+ * });
+ * ```
+ *
+ * @category Server
+ */
+export type HandleLogin = {
+  (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions): Promise<void>;
+  (provider: LoginOptionsProvider): LoginHandler;
+  (options: LoginOptions): LoginHandler;
+};
+
 /**
  * The handler for the `/api/auth/login` API route.
  *
@@ -162,7 +233,7 @@ export interface LoginOptions {
  *
  * @category Server
  */
-export type HandleLogin = (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions) => Promise<void>;
+export type LoginHandler = (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions) => Promise<void>;
 
 /**
  * @ignore
@@ -172,13 +243,12 @@ export default function handleLoginFactory(
   nextConfig: NextConfig,
   baseConfig: BaseConfig
 ): HandleLogin {
-  return async (req, res, options = {}): Promise<void> => {
+  const login: LoginHandler = async (req: NextApiRequest, res: NextApiResponse, options = {}): Promise<void> => {
     try {
       assertReqRes(req, res);
       if (req.query.returnTo) {
         const dangerousReturnTo = Array.isArray(req.query.returnTo) ? req.query.returnTo[0] : req.query.returnTo;
         const safeBaseUrl = new URL(options.authorizationParams?.redirect_uri || baseConfig.baseURL);
-
         const returnTo = toSafeRedirect(dangerousReturnTo, safeBaseUrl);
 
         options = { ...options, returnTo };
@@ -189,10 +259,22 @@ export default function handleLoginFactory(
           authorizationParams: { organization: nextConfig.organization, ...options.authorizationParams }
         };
       }
-
       return await handler(req, res, options);
     } catch (e) {
       throw new LoginHandlerError(e as HandlerErrorCause);
     }
   };
+  return (
+    reqOrOptions: NextApiRequest | LoginOptionsProvider | LoginOptions,
+    res?: NextApiResponse,
+    options?: LoginOptions
+  ): any => {
+    if (reqOrOptions instanceof IncomingMessage && res) {
+      return login(reqOrOptions, res, options);
+    }
+    if (typeof reqOrOptions === 'function') {
+      return (req: NextApiRequest, res: NextApiResponse) => login(req, res, reqOrOptions(req));
+    }
+    return (req: NextApiRequest, res: NextApiResponse) => login(req, res, reqOrOptions as LoginOptions);
+  };
 }
diff --git a/src/handlers/logout.ts b/src/handlers/logout.ts
index 0500b712..fc51b490 100644
--- a/src/handlers/logout.ts
+++ b/src/handlers/logout.ts
@@ -1,3 +1,4 @@
+import { IncomingMessage } from 'http';
 import { NextApiResponse, NextApiRequest } from 'next';
 import { HandleLogout as BaseHandleLogout } from '../auth0-session';
 import { assertReqRes } from '../utils/assert';
@@ -18,6 +19,70 @@ export interface LogoutOptions {
   returnTo?: string;
 }
 
+/**
+ * Options provider for the default logout handler.
+ * Use this to generate options that depend on values from the request.
+ *
+ * @category Server
+ */
+export type LogoutOptionsProvider = (req: NextApiRequest) => LogoutOptions;
+
+/**
+ * Use this to customize the default logout handler without overriding it.
+ * You can still override the handler if needed.
+ *
+ * @example Pass an options object
+ *
+ * ```js
+ * // pages/api/auth/[...auth0].js
+ * import { handleAuth, handleLogout } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   logout: handleLogout({ returnTo: 'https://example.com' })
+ * });
+ * ```
+ *
+ * @example Pass a function that receives the request and returns an options object
+ *
+ * ```js
+ * // pages/api/auth/[...auth0].js
+ * import { handleAuth, handleLogout } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   logout: handleLogout((req) => {
+ *     return { returnTo: 'https://example.com' };
+ *   })
+ * });
+ * ```
+ *
+ * This is useful for generating options that depend on values from the request.
+ *
+ * @example Override the logout handler
+ *
+ * ```js
+ * import { handleAuth, handleLogout } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   logout: async (req, res) => {
+ *     try {
+ *       await handleLogout(req, res, {
+ *         returnTo: 'https://example.com'
+ *       });
+ *     } catch (error) {
+ *       console.error(error);
+ *     }
+ *   }
+ * });
+ * ```
+ *
+ * @category Server
+ */
+export type HandleLogout = {
+  (req: NextApiRequest, res: NextApiResponse, options?: LogoutOptions): Promise<void>;
+  (provider: LogoutOptionsProvider): LogoutHandler;
+  (options: LogoutOptions): LogoutHandler;
+};
+
 /**
  * The handler for the `/api/auth/logout` API route.
  *
@@ -25,13 +90,13 @@ export interface LogoutOptions {
  *
  * @category Server
  */
-export type HandleLogout = (req: NextApiRequest, res: NextApiResponse, options?: LogoutOptions) => Promise<void>;
+export type LogoutHandler = (req: NextApiRequest, res: NextApiResponse, options?: LogoutOptions) => Promise<void>;
 
 /**
  * @ignore
  */
 export default function handleLogoutFactory(handler: BaseHandleLogout): HandleLogout {
-  return async (req, res, options): Promise<void> => {
+  const logout: LogoutHandler = async (req: NextApiRequest, res: NextApiResponse, options = {}): Promise<void> => {
     try {
       assertReqRes(req, res);
       return await handler(req, res, options);
@@ -39,4 +104,17 @@ export default function handleLogoutFactory(handler: BaseHandleLogout): HandleLo
       throw new LogoutHandlerError(e as HandlerErrorCause);
     }
   };
+  return (
+    reqOrOptions: NextApiRequest | LogoutOptionsProvider | LogoutOptions,
+    res?: NextApiResponse,
+    options?: LogoutOptions
+  ): any => {
+    if (reqOrOptions instanceof IncomingMessage && res) {
+      return logout(reqOrOptions, res, options);
+    }
+    if (typeof reqOrOptions === 'function') {
+      return (req: NextApiRequest, res: NextApiResponse) => logout(req, res, reqOrOptions(req));
+    }
+    return (req: NextApiRequest, res: NextApiResponse) => logout(req, res, reqOrOptions as LogoutOptions);
+  };
 }
diff --git a/src/handlers/profile.ts b/src/handlers/profile.ts
index 7d7fbf28..eba12ba8 100644
--- a/src/handlers/profile.ts
+++ b/src/handlers/profile.ts
@@ -1,3 +1,4 @@
+import { IncomingMessage } from 'http';
 import { NextApiResponse, NextApiRequest } from 'next';
 import { ClientFactory } from '../auth0-session';
 import { SessionCache, Session, fromJson, GetAccessToken } from '../session';
@@ -28,6 +29,68 @@ export type ProfileOptions = {
   afterRefetch?: AfterRefetch;
 };
 
+/**
+ * Options provider for the default profile handler.
+ * Use this to generate options that depend on values from the request.
+ *
+ * @category Server
+ */
+export type ProfileOptionsProvider = (req: NextApiRequest) => ProfileOptions;
+
+/**
+ * Use this to customize the default profile handler without overriding it.
+ * You can still override the handler if needed.
+ *
+ * @example Pass an options object
+ *
+ * ```js
+ * // pages/api/auth/[...auth0].js
+ * import { handleAuth, handleProfile } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   profile: handleProfile({ refetch: true })
+ * });
+ * ```
+ *
+ * @example Pass a function that receives the request and returns an options object
+ *
+ * ```js
+ * // pages/api/auth/[...auth0].js
+ * import { handleAuth, handleProfile } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   profile: handleProfile((req) => {
+ *     return { refetch: true };
+ *   })
+ * });
+ * ```
+ *
+ * This is useful for generating options that depend on values from the request.
+ *
+ * @example Override the profile handler
+ *
+ * ```js
+ * import { handleAuth, handleProfile } from '@auth0/nextjs-auth0';
+ *
+ * export default handleAuth({
+ *   profile: async (req, res) => {
+ *     try {
+ *       await handleProfile(req, res, { refetch: true });
+ *     } catch (error) {
+ *       console.error(error);
+ *     }
+ *   }
+ * });
+ * ```
+ *
+ * @category Server
+ */
+export type HandleProfile = {
+  (req: NextApiRequest, res: NextApiResponse, options?: ProfileOptions): Promise<void>;
+  (provider: ProfileOptionsProvider): ProfileHandler;
+  (options: ProfileOptions): ProfileHandler;
+};
+
 /**
  * The handler for the `/api/auth/me` API route.
  *
@@ -35,7 +98,7 @@ export type ProfileOptions = {
  *
  * @category Server
  */
-export type HandleProfile = (req: NextApiRequest, res: NextApiResponse, options?: ProfileOptions) => Promise<void>;
+export type ProfileHandler = (req: NextApiRequest, res: NextApiResponse, options?: ProfileOptions) => Promise<void>;
 
 /**
  * @ignore
@@ -45,7 +108,7 @@ export default function profileHandler(
   getAccessToken: GetAccessToken,
   sessionCache: SessionCache
 ): HandleProfile {
-  return async (req, res, options): Promise<void> => {
+  const profile: ProfileHandler = async (req: NextApiRequest, res: NextApiResponse, options = {}): Promise<void> => {
     try {
       assertReqRes(req, res);
 
@@ -57,7 +120,7 @@ export default function profileHandler(
       const session = (await sessionCache.get(req, res)) as Session;
       res.setHeader('Cache-Control', 'no-store');
 
-      if (options?.refetch) {
+      if (options.refetch) {
         const { accessToken } = await getAccessToken(req, res);
         if (!accessToken) {
           throw new Error('No access token available to refetch the profile');
@@ -89,4 +152,17 @@ export default function profileHandler(
       throw new ProfileHandlerError(e as HandlerErrorCause);
     }
   };
+  return (
+    reqOrOptions: NextApiRequest | ProfileOptionsProvider | ProfileOptions,
+    res?: NextApiResponse,
+    options?: ProfileOptions
+  ): any => {
+    if (reqOrOptions instanceof IncomingMessage && res) {
+      return profile(reqOrOptions, res, options);
+    }
+    if (typeof reqOrOptions === 'function') {
+      return (req: NextApiRequest, res: NextApiResponse) => profile(req, res, reqOrOptions(req));
+    }
+    return (req: NextApiRequest, res: NextApiResponse) => profile(req, res, reqOrOptions as ProfileOptions);
+  };
 }
diff --git a/src/index.browser.ts b/src/index.browser.ts
index cad7ccec..15c5ad25 100644
--- a/src/index.browser.ts
+++ b/src/index.browser.ts
@@ -52,8 +52,12 @@ export const initAuth0: InitAuth0 = () => instance;
 export const getSession: GetSession = (...args) => instance.getSession(...args);
 export const getAccessToken: GetAccessToken = (...args) => instance.getAccessToken(...args);
 export const withApiAuthRequired: WithApiAuthRequired = (...args) => instance.withApiAuthRequired(...args);
-export const handleLogin: HandleLogin = (...args) => instance.handleLogin(...args);
-export const handleLogout: HandleLogout = (...args) => instance.handleLogout(...args);
-export const handleCallback: HandleCallback = (...args) => instance.handleCallback(...args);
-export const handleProfile: HandleProfile = (...args) => instance.handleProfile(...args);
+export const handleLogin: HandleLogin = ((...args: Parameters<HandleLogin>) =>
+  instance.handleLogin(...args)) as HandleLogin;
+export const handleLogout: HandleLogout = ((...args: Parameters<HandleLogout>) =>
+  instance.handleLogout(...args)) as HandleLogout;
+export const handleCallback: HandleCallback = ((...args: Parameters<HandleCallback>) =>
+  instance.handleCallback(...args)) as HandleCallback;
+export const handleProfile: HandleProfile = ((...args: Parameters<HandleProfile>) =>
+  instance.handleProfile(...args)) as HandleProfile;
 export const handleAuth: HandleAuth = (...args) => instance.handleAuth(...args);
diff --git a/src/index.ts b/src/index.ts
index c258db94..cb8c81fa 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -120,10 +120,14 @@ export const updateUser: UpdateUser = (...args) => getInstance().updateUser(...a
 export const getAccessToken: GetAccessToken = (...args) => getInstance().getAccessToken(...args);
 export const withApiAuthRequired: WithApiAuthRequired = (...args) => getInstance().withApiAuthRequired(...args);
 export const withPageAuthRequired: WithPageAuthRequired = withPageAuthRequiredFactory(getLoginUrl(), getSessionCache);
-export const handleLogin: HandleLogin = (...args) => getInstance().handleLogin(...args);
-export const handleLogout: HandleLogout = (...args) => getInstance().handleLogout(...args);
-export const handleCallback: HandleCallback = (...args) => getInstance().handleCallback(...args);
-export const handleProfile: HandleProfile = (...args) => getInstance().handleProfile(...args);
+export const handleLogin: HandleLogin = ((...args: Parameters<HandleLogin>) =>
+  getInstance().handleLogin(...args)) as HandleLogin;
+export const handleLogout: HandleLogout = ((...args: Parameters<HandleLogout>) =>
+  getInstance().handleLogout(...args)) as HandleLogout;
+export const handleCallback: HandleCallback = ((...args: Parameters<HandleCallback>) =>
+  getInstance().handleCallback(...args)) as HandleCallback;
+export const handleProfile: HandleProfile = ((...args: Parameters<HandleProfile>) =>
+  getInstance().handleProfile(...args)) as HandleProfile;
 export const handleAuth: HandleAuth = (...args) => getInstance().handleAuth(...args);
 
 export {


Test patch:
diff --git a/tests/fixtures/setup.ts b/tests/fixtures/setup.ts
index 45592b52..50b59c11 100644
--- a/tests/fixtures/setup.ts
+++ b/tests/fixtures/setup.ts
@@ -1,5 +1,5 @@
 import { IncomingMessage, ServerResponse } from 'http';
-import { NextApiRequest, NextApiResponse } from 'next';
+import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
 import nock from 'nock';
 import { CookieJar } from 'tough-cookie';
 import {
@@ -20,12 +20,17 @@ import { jwks, makeIdToken } from '../auth0-session/fixtures/cert';
 import { start, stop } from './server';
 import { encodeState } from '../../src/auth0-session/hooks/get-login-state';
 import { post, toSignedCookieJar } from '../auth0-session/fixtures/helpers';
+import { HandleLogin, HandleLogout, HandleCallback, HandleProfile } from '../../src';
 
 export type SetupOptions = {
   idTokenClaims?: Claims;
+  callbackHandler?: HandleCallback;
   callbackOptions?: CallbackOptions;
+  loginHandler?: HandleLogin;
   loginOptions?: LoginOptions;
+  logoutHandler?: HandleLogout;
   logoutOptions?: LogoutOptions;
+  profileHandler?: HandleProfile;
   profileOptions?: ProfileOptions;
   withPageAuthRequiredOptions?: WithPageAuthRequiredOptions;
   getAccessTokenOptions?: AccessTokenRequest;
@@ -45,9 +50,13 @@ export const setup = async (
   config: ConfigParameters,
   {
     idTokenClaims,
+    callbackHandler,
     callbackOptions,
+    logoutHandler,
     logoutOptions,
+    loginHandler,
     loginOptions,
+    profileHandler,
     profileOptions,
     withPageAuthRequiredOptions,
     onError = defaultOnError,
@@ -73,20 +82,12 @@ export const setup = async (
     getAccessToken,
     withApiAuthRequired,
     withPageAuthRequired
-  } = await initAuth0(config);
-  const handlers: Partial<Handlers> = { onError };
-  if (callbackOptions) {
-    handlers.callback = (req, res) => handleCallback(req, res, callbackOptions);
-  }
-  if (loginOptions) {
-    handlers.login = (req, res) => handleLogin(req, res, loginOptions);
-  }
-  if (logoutOptions) {
-    handlers.logout = (req, res) => handleLogout(req, res, logoutOptions);
-  }
-  if (profileOptions) {
-    handlers.profile = (req, res) => handleProfile(req, res, profileOptions);
-  }
+  } = initAuth0(config);
+  const callback: NextApiHandler = (...args) => (callbackHandler || handleCallback)(...args, callbackOptions);
+  const login: NextApiHandler = (...args) => (loginHandler || handleLogin)(...args, loginOptions);
+  const logout: NextApiHandler = (...args) => (logoutHandler || handleLogout)(...args, logoutOptions);
+  const profile: NextApiHandler = (...args) => (profileHandler || handleProfile)(...args, profileOptions);
+  const handlers: Handlers = { onError, callback, login, logout, profile };
   global.handleAuth = handleAuth.bind(null, handlers);
   global.getSession = getSession;
   global.updateUser = updateUser;
diff --git a/tests/fixtures/test-app/pages/api/auth/[...auth0].ts b/tests/fixtures/test-app/pages/api/auth/[...auth0].ts
index ca9702e6..508718b1 100644
--- a/tests/fixtures/test-app/pages/api/auth/[...auth0].ts
+++ b/tests/fixtures/test-app/pages/api/auth/[...auth0].ts
@@ -1,1 +1,2 @@
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 export default (...args: any) => global.handleAuth?.()(...args);
diff --git a/tests/fixtures/test-app/pages/api/session.ts b/tests/fixtures/test-app/pages/api/session.ts
index 9c88f9ed..5fc47247 100644
--- a/tests/fixtures/test-app/pages/api/session.ts
+++ b/tests/fixtures/test-app/pages/api/session.ts
@@ -1,6 +1,6 @@
 import { NextApiRequest, NextApiResponse } from 'next';
 
-export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse) {
+export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
   const json = await global.getSession?.(req, res);
   res.status(200).json(json);
 }
diff --git a/tests/fixtures/test-app/pages/api/update-user.ts b/tests/fixtures/test-app/pages/api/update-user.ts
index e48b4b7e..d791ea4f 100644
--- a/tests/fixtures/test-app/pages/api/update-user.ts
+++ b/tests/fixtures/test-app/pages/api/update-user.ts
@@ -1,6 +1,6 @@
 import { NextApiRequest, NextApiResponse } from 'next';
 
-export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse) {
+export default async function sessionHandler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
   const session = await global.getSession?.(req, res);
   const updated = { ...session?.user, ...req.body?.user };
   await global.updateUser?.(req, res, updated);
diff --git a/tests/handlers/auth.test.ts b/tests/handlers/auth.test.ts
index e74374d3..c08bc81f 100644
--- a/tests/handlers/auth.test.ts
+++ b/tests/handlers/auth.test.ts
@@ -1,9 +1,16 @@
 import { IncomingMessage, ServerResponse } from 'http';
 import { ArgumentsOf } from 'ts-jest';
 import { withoutApi } from '../fixtures/default-settings';
-import { setup, teardown } from '../fixtures/setup';
+import { login, setup, teardown } from '../fixtures/setup';
 import { get } from '../auth0-session/fixtures/helpers';
-import { initAuth0, OnError } from '../../src';
+import { initAuth0, OnError, Session } from '../../src';
+import { LoginHandler, LoginOptions } from '../../src/handlers/login';
+import { LogoutHandler, LogoutOptions } from '../../src/handlers/logout';
+import { CallbackHandler, CallbackOptions } from '../../src/handlers/callback';
+import { ProfileHandler, ProfileOptions } from '../../src/handlers/profile';
+import * as baseLoginHandler from '../../src/auth0-session/handlers/login';
+import * as baseLogoutHandler from '../../src/auth0-session/handlers/logout';
+import * as baseCallbackHandler from '../../src/auth0-session/handlers/callback';
 
 const handlerError = () =>
   expect.objectContaining({
@@ -14,6 +21,28 @@ const handlerError = () =>
 describe('auth handler', () => {
   afterEach(teardown);
 
+  test('return 500 for unexpected error', async () => {
+    const baseUrl = await setup(withoutApi);
+    global.handleAuth = initAuth0(withoutApi).handleAuth;
+    delete global.onError;
+    jest.spyOn(console, 'error').mockImplementation((error) => {
+      delete error.status;
+    });
+    await expect(get(baseUrl, '/api/auth/callback?error=foo&error_description=bar&state=foo')).rejects.toThrow(
+      'Internal Server Error'
+    );
+  });
+
+  test('return 404 for unknown routes', async () => {
+    const baseUrl = await setup(withoutApi);
+    global.handleAuth = initAuth0(withoutApi).handleAuth;
+    await expect(get(baseUrl, '/api/auth/foo')).rejects.toThrow('Not Found');
+  });
+});
+
+describe('custom error handler', () => {
+  afterEach(teardown);
+
   test('accept custom error handler', async () => {
     const onError = jest.fn<void, ArgumentsOf<OnError>>((_req, res) => res.end());
     const baseUrl = await setup(withoutApi, { onError });
@@ -23,8 +52,9 @@ describe('auth handler', () => {
 
   test('use default error handler', async () => {
     const baseUrl = await setup(withoutApi);
-    global.handleAuth = (await initAuth0(withoutApi)).handleAuth;
+    global.handleAuth = initAuth0(withoutApi).handleAuth;
     delete global.onError;
+    // eslint-disable-next-line @typescript-eslint/no-empty-function
     jest.spyOn(console, 'error').mockImplementation(() => {});
     await expect(get(baseUrl, '/api/auth/callback?error=foo&error_description=bar&state=foo')).rejects.toThrow(
       'Bad Request'
@@ -35,7 +65,7 @@ describe('auth handler', () => {
   test('finish response if custom error does not', async () => {
     const onError = jest.fn();
     const baseUrl = await setup(withoutApi);
-    global.handleAuth = (await initAuth0(withoutApi)).handleAuth.bind(null, { onError });
+    global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { onError });
     await expect(
       get(baseUrl, '/api/auth/callback?error=foo&error_description=bar&state=foo', { fullResponse: true })
     ).rejects.toThrow('Internal Server Error');
@@ -45,28 +75,205 @@ describe('auth handler', () => {
   test('finish response with custom error status', async () => {
     const onError = jest.fn<void, ArgumentsOf<OnError>>((_req, res) => res.status(418));
     const baseUrl = await setup(withoutApi);
-    global.handleAuth = (await initAuth0(withoutApi)).handleAuth.bind(null, { onError });
+    global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { onError });
     await expect(
       get(baseUrl, '/api/auth/callback?error=foo&error_description=bar&state=foo', { fullResponse: true })
     ).rejects.toThrow("I'm a Teapot");
     expect(onError).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), handlerError());
   });
+});
 
-  test('return 500 for unexpected error', async () => {
+describe('custom handlers', () => {
+  afterEach(teardown);
+
+  test('accept custom login handler', async () => {
+    const login = jest.fn<Promise<void>, ArgumentsOf<LoginHandler>>(async (_req, res) => {
+      res.end();
+    });
     const baseUrl = await setup(withoutApi);
-    global.handleAuth = (await initAuth0(withoutApi)).handleAuth;
-    delete global.onError;
-    jest.spyOn(console, 'error').mockImplementation((error) => {
-      delete error.status;
+    global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { login });
+    await get(baseUrl, '/api/auth/login');
+    expect(login).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse));
+  });
+
+  test('accept custom logout handler', async () => {
+    const logout = jest.fn<Promise<void>, ArgumentsOf<LogoutHandler>>(async (_req, res) => {
+      res.end();
     });
-    await expect(get(baseUrl, '/api/auth/callback?error=foo&error_description=bar&state=foo')).rejects.toThrow(
-      'Internal Server Error'
+    const baseUrl = await setup(withoutApi);
+    global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { logout });
+    await get(baseUrl, '/api/auth/logout');
+    expect(logout).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse));
+  });
+
+  test('accept custom callback handler', async () => {
+    const callback = jest.fn<Promise<void>, ArgumentsOf<CallbackHandler>>(async (_req, res) => {
+      res.end();
+    });
+    const baseUrl = await setup(withoutApi);
+    global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { callback });
+    await get(baseUrl, '/api/auth/callback');
+    expect(callback).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse));
+  });
+
+  test('accept custom profile handler', async () => {
+    const profile = jest.fn<Promise<void>, ArgumentsOf<ProfileHandler>>(async (_req, res) => {
+      res.end();
+    });
+    const baseUrl = await setup(withoutApi);
+    global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { profile });
+    await get(baseUrl, '/api/auth/me');
+    expect(profile).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse));
+  });
+
+  test('accept custom arbitrary handler', async () => {
+    const signup = jest.fn<Promise<void>, ArgumentsOf<LoginHandler>>(async (_req, res) => {
+      res.end();
+    });
+    const baseUrl = await setup(withoutApi);
+    global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { signup });
+    await get(baseUrl, '/api/auth/signup');
+    expect(signup).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse));
+  });
+});
+
+describe('custom options', () => {
+  afterEach(teardown);
+
+  test('accept custom login options', async () => {
+    const loginHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => {
+      res.end();
+    });
+    jest.spyOn(baseLoginHandler, 'default').mockImplementation(() => loginHandler);
+    const options: LoginOptions = { authorizationParams: { scope: 'openid' } };
+    const baseUrl = await setup(withoutApi);
+    const { handleLogin, handleAuth } = initAuth0(withoutApi);
+    global.handleAuth = handleAuth.bind(null, {
+      login: handleLogin(options)
+    });
+    await get(baseUrl, '/api/auth/login');
+    expect(loginHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), options);
+  });
+
+  test('accept custom logout options', async () => {
+    const logoutHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => {
+      res.end();
+    });
+    jest.spyOn(baseLogoutHandler, 'default').mockImplementation(() => logoutHandler);
+    const options: LogoutOptions = { returnTo: '/foo' };
+    const baseUrl = await setup(withoutApi);
+    const { handleLogout, handleAuth } = initAuth0(withoutApi);
+    global.handleAuth = handleAuth.bind(null, {
+      logout: handleLogout(options)
+    });
+    await get(baseUrl, '/api/auth/logout');
+    expect(logoutHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), options);
+  });
+
+  test('accept custom callback options', async () => {
+    const callbackHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => {
+      res.end();
+    });
+    jest.spyOn(baseCallbackHandler, 'default').mockImplementation(() => callbackHandler);
+    const options: CallbackOptions = { redirectUri: '/foo' };
+    const baseUrl = await setup(withoutApi);
+    const { handleCallback, handleAuth } = initAuth0(withoutApi);
+    global.handleAuth = handleAuth.bind(null, {
+      callback: handleCallback(options)
+    });
+    await get(baseUrl, '/api/auth/callback');
+    expect(callbackHandler).toHaveBeenCalledWith(
+      expect.any(IncomingMessage),
+      expect.any(ServerResponse),
+      expect.objectContaining(options)
     );
   });
 
-  test('return 404 for unknown routes', async () => {
+  test('accept custom profile options', async () => {
+    const afterRefetch = jest.fn(async (_req: IncomingMessage, _res: ServerResponse, session: Session) => session);
+    const options: ProfileOptions = { refetch: true, afterRefetch };
     const baseUrl = await setup(withoutApi);
-    global.handleAuth = (await initAuth0(withoutApi)).handleAuth;
-    await expect(get(baseUrl, '/api/auth/foo')).rejects.toThrow('Not Found');
+    const { handleProfile, handleAuth } = initAuth0(withoutApi);
+    global.handleAuth = handleAuth.bind(null, {
+      profile: handleProfile(options)
+    });
+    const cookieJar = await login(baseUrl);
+    await get(baseUrl, '/api/auth/me', { cookieJar });
+    expect(afterRefetch).toHaveBeenCalled();
+  });
+});
+
+describe('custom options providers', () => {
+  afterEach(teardown);
+
+  test('accept custom login options provider', async () => {
+    const loginHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => {
+      res.end();
+    });
+    jest.spyOn(baseLoginHandler, 'default').mockImplementation(() => loginHandler);
+    const options = { authorizationParams: { scope: 'openid' } };
+    const optionsProvider = jest.fn(() => options);
+    const baseUrl = await setup(withoutApi);
+    const { handleLogin, handleAuth } = initAuth0(withoutApi);
+
+    global.handleAuth = handleAuth.bind(null, {
+      login: handleLogin(optionsProvider)
+    });
+    await get(baseUrl, '/api/auth/login');
+    expect(optionsProvider).toHaveBeenCalled();
+    expect(loginHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), options);
+  });
+
+  test('accept custom logout options provider', async () => {
+    const logoutHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => {
+      res.end();
+    });
+    jest.spyOn(baseLogoutHandler, 'default').mockImplementation(() => logoutHandler);
+    const options: LogoutOptions = { returnTo: '/foo' };
+    const optionsProvider = jest.fn(() => options);
+    const baseUrl = await setup(withoutApi);
+    const { handleLogout, handleAuth } = initAuth0(withoutApi);
+    global.handleAuth = handleAuth.bind(null, {
+      logout: handleLogout(optionsProvider)
+    });
+    await get(baseUrl, '/api/auth/logout');
+    expect(optionsProvider).toHaveBeenCalled();
+    expect(logoutHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), options);
+  });
+
+  test('accept custom callback options provider', async () => {
+    const callbackHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => {
+      res.end();
+    });
+    jest.spyOn(baseCallbackHandler, 'default').mockImplementation(() => callbackHandler);
+    const options: CallbackOptions = { redirectUri: '/foo' };
+    const optionsProvider = jest.fn(() => options);
+    const baseUrl = await setup(withoutApi);
+    const { handleCallback, handleAuth } = initAuth0(withoutApi);
+    global.handleAuth = handleAuth.bind(null, {
+      callback: handleCallback(optionsProvider)
+    });
+    await get(baseUrl, '/api/auth/callback');
+    expect(optionsProvider).toHaveBeenCalled();
+    expect(callbackHandler).toHaveBeenCalledWith(
+      expect.any(IncomingMessage),
+      expect.any(ServerResponse),
+      expect.objectContaining(options)
+    );
+  });
+
+  test('accept custom profile options provider', async () => {
+    const afterRefetch = jest.fn(async (_req: IncomingMessage, _res: ServerResponse, session: Session) => session);
+    const options: ProfileOptions = { refetch: true, afterRefetch };
+    const optionsProvider = jest.fn(() => options);
+    const baseUrl = await setup(withoutApi);
+    const { handleProfile, handleAuth } = initAuth0(withoutApi);
+    global.handleAuth = handleAuth.bind(null, {
+      profile: handleProfile(optionsProvider)
+    });
+    const cookieJar = await login(baseUrl);
+    await get(baseUrl, '/api/auth/me', { cookieJar });
+    expect(optionsProvider).toHaveBeenCalled();
+    expect(afterRefetch).toHaveBeenCalled();
   });
 });
diff --git a/tests/handlers/callback.test.ts b/tests/handlers/callback.test.ts
index 7359bf62..0ab11f32 100644
--- a/tests/handlers/callback.test.ts
+++ b/tests/handlers/callback.test.ts
@@ -1,6 +1,6 @@
 import { CookieJar } from 'tough-cookie';
 import * as jose from 'jose';
-import timekeeper = require('timekeeper');
+import timekeeper from 'timekeeper';
 import { withApi, withoutApi } from '../fixtures/default-settings';
 import { makeIdToken } from '../auth0-session/fixtures/cert';
 import { defaultConfig, get, post, toSignedCookieJar } from '../auth0-session/fixtures/helpers';
@@ -41,7 +41,8 @@ describe('callback handler', () => {
         state: '__test_state__'
       })
     ).rejects.toThrow(
-      'Callback handler failed. CAUSE: The cookie dropped by the login request cannot be found, check the url of the login request, the url of this callback request and your cookie config.'
+      'Callback handler failed. CAUSE: The cookie dropped by the login request cannot be found, check the url of the ' +
+        'login request, the url of this callback request and your cookie config.'
     );
   });