From ae7a189a02eb87cb53c715b523920c1865aa04a9 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Thu, 22 Sep 2022 02:18:33 -0300 Subject: [PATCH 01/20] Add support for configuring built-in API routes --- src/handlers/auth.ts | 32 +++++++------- src/handlers/callback.ts | 7 ++-- src/handlers/login.ts | 8 ++-- tests/fixtures/setup.ts | 22 +++------- tests/handlers/auth.test.ts | 83 ++++++++++++++++++++++++++++++------- 5 files changed, 100 insertions(+), 52 deletions(-) diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts index 4b22ee74c..7c0bb3f15 100644 --- a/src/handlers/auth.ts +++ b/src/handlers/auth.ts @@ -1,7 +1,7 @@ -import { HandleLogin } from './login'; -import { HandleLogout } from './logout'; -import { HandleCallback } from './callback'; -import { HandleProfile } from './profile'; +import { HandleLogin, LoginOptions } from './login'; +import { HandleLogout, LogoutOptions } from './logout'; +import { CallbackOptions, HandleCallback } from './callback'; +import { HandleProfile, ProfileOptions } from './profile'; import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; import { HandlerError } from '../utils/errors'; @@ -24,7 +24,7 @@ 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(); * } * } * }); @@ -33,10 +33,10 @@ import { HandlerError } from '../utils/errors'; * @category Server */ export interface Handlers { - login: HandleLogin; - logout: HandleLogout; - callback: HandleCallback; - profile: HandleProfile; + login: HandleLogin | LoginOptions; + logout: HandleLogout | LogoutOptions; + callback: HandleCallback | CallbackOptions; + profile: HandleProfile | ProfileOptions; onError: OnError; } @@ -107,18 +107,22 @@ export default function handlerFactory({ try { switch (route) { case 'login': - return await login(req, res); + if (typeof login === 'function') return await login(req, res); + return await handleLogin(req, res, login); case 'logout': - return await logout(req, res); + if (typeof logout === 'function') return await logout(req, res); + return await handleLogout(req, res, logout); case 'callback': - return await callback(req, res); + if (typeof callback === 'function') return await callback(req, res); + return await handleCallback(req, res, callback); case 'me': - return await profile(req, res); + if (typeof profile === 'function') return await profile(req, res); + return await handleProfile(req, res, profile); default: res.status(404).end(); } } catch (error) { - await (onError || defaultOnError)(req, res, error); + await (onError || defaultOnError)(req, res, error as HandlerError); if (!res.finished) { // 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 570a4cfa9..4a165f994 100644 --- a/src/handlers/callback.ts +++ b/src/handlers/callback.ts @@ -28,7 +28,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(); * } * } * }); @@ -51,7 +51,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(); * } * } * }); @@ -91,7 +91,8 @@ 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; } diff --git a/src/handlers/login.ts b/src/handlers/login.ts index 9cd2d5ec1..b93ff9de7 100644 --- a/src/handlers/login.ts +++ b/src/handlers/login.ts @@ -21,7 +21,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 +57,7 @@ export interface AuthorizationParams extends Partial { * } * } * }); + * ``` */ connection?: string; @@ -80,6 +81,7 @@ export interface AuthorizationParams extends Partial { * } * } * }); + * ``` */ connection_scope?: string; @@ -106,13 +108,13 @@ export interface AuthorizationParams extends Partial { * } * }); * } 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; diff --git a/tests/fixtures/setup.ts b/tests/fixtures/setup.ts index 0cda47b28..b74a946d4 100644 --- a/tests/fixtures/setup.ts +++ b/tests/fixtures/setup.ts @@ -62,30 +62,20 @@ export const setup = async ( jwksEndpoint(config, jwks); codeExchange(config, await makeIdToken({ iss: 'https://acme.auth0.local/', ...idTokenClaims })); userInfo(config, userInfoToken, userInfoPayload); - const { - handleAuth, - handleCallback, - handleLogin, - handleLogout, - handleProfile, - getSession, - updateUser, - getAccessToken, - withApiAuthRequired, - withPageAuthRequired - } = await initAuth0(config); + const { handleAuth, getSession, updateUser, getAccessToken, withApiAuthRequired, withPageAuthRequired } = + await initAuth0(config); const handlers: Partial = { onError }; if (callbackOptions) { - handlers.callback = (req, res) => handleCallback(req, res, callbackOptions); + handlers.callback = callbackOptions; } if (loginOptions) { - handlers.login = (req, res) => handleLogin(req, res, loginOptions); + handlers.login = loginOptions; } if (logoutOptions) { - handlers.logout = (req, res) => handleLogout(req, res, logoutOptions); + handlers.logout = logoutOptions; } if (profileOptions) { - handlers.profile = (req, res) => handleProfile(req, res, profileOptions); + handlers.profile = profileOptions; } global.handleAuth = handleAuth.bind(null, handlers); global.getSession = getSession; diff --git a/tests/handlers/auth.test.ts b/tests/handlers/auth.test.ts index 1be1d602e..066b53e9a 100644 --- a/tests/handlers/auth.test.ts +++ b/tests/handlers/auth.test.ts @@ -2,8 +2,12 @@ import { IncomingMessage, ServerResponse } from 'http'; import { ArgumentsOf } from 'ts-jest'; import { withoutApi } from '../fixtures/default-settings'; import { setup, teardown } from '../fixtures/setup'; -import { get } from '../auth0-session/fixtures/helpers'; -import { initAuth0, OnError } from '../../src'; +import { get, post } from '../auth0-session/fixtures/helpers'; +import { CallbackOptions, initAuth0, LoginOptions, LogoutOptions, OnError, ProfileOptions } from '../../src'; +import * as loginHandler from '../../src/handlers/login'; +import * as logoutHandler from '../../src/handlers/logout'; +import * as callbackHandler from '../../src/handlers/callback'; +import * as profileHandler from '../../src/handlers/profile'; const handlerError = (status = 400, error = 'foo', error_description = 'bar') => expect.objectContaining({ @@ -14,6 +18,28 @@ const handlerError = (status = 400, error = 'foo', error_description = 'bar') => describe('auth handler', () => { afterEach(teardown); + test('return 500 for unexpected error', async () => { + const baseUrl = await setup(withoutApi); + global.handleAuth = (await 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')).rejects.toThrow( + 'Internal Server Error' + ); + }); + + test('return 404 for unknown routes', async () => { + const baseUrl = await setup(withoutApi); + global.handleAuth = (await 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>((_req, res) => res.end()); const baseUrl = await setup(withoutApi, { onError }); @@ -25,6 +51,7 @@ describe('auth handler', () => { const baseUrl = await setup(withoutApi); global.handleAuth = (await 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')).rejects.toThrow('Bad Request'); expect(console.error).toHaveBeenCalledWith(new Error('Callback handler failed. CAUSE: foo (bar)')); @@ -49,22 +76,46 @@ describe('auth handler', () => { ).rejects.toThrow("I'm a Teapot"); expect(onError).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), handlerError()); }); +}); + +describe('custom options', () => { + const spyHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => { + res.end(); + }); - test('return 500 for unexpected error', async () => { - const baseUrl = await setup(withoutApi); - global.handleAuth = (await 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')).rejects.toThrow( - 'Internal Server Error' - ); + afterEach(teardown); + + test('accept custom login options', async () => { + jest.spyOn(loginHandler, 'default').mockImplementation(() => spyHandler); + const loginOptions: LoginOptions = { + authorizationParams: { scope: 'openid' } + }; + const baseUrl = await setup(withoutApi, { loginOptions }); + await get(baseUrl, '/api/auth/login'); + expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), loginOptions); }); - test('return 404 for unknown routes', async () => { - const baseUrl = await setup(withoutApi); - global.handleAuth = (await initAuth0(withoutApi)).handleAuth; - await expect(get(baseUrl, '/api/auth/foo')).rejects.toThrow('Not Found'); + test('accept custom logout options', async () => { + jest.spyOn(logoutHandler, 'default').mockImplementation(() => spyHandler); + const logoutOptions: LogoutOptions = { returnTo: 'https://example.com' }; + const baseUrl = await setup(withoutApi, { logoutOptions }); + await get(baseUrl, '/api/auth/logout'); + expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), logoutOptions); + }); + + test('accept custom callback options', async () => { + jest.spyOn(callbackHandler, 'default').mockImplementation(() => spyHandler); + const callbackOptions: CallbackOptions = { authorizationParams: { scope: 'openid' } }; + const baseUrl = await setup(withoutApi, { callbackOptions }); + await post(baseUrl, '/api/auth/callback', { body: {} }); + expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), callbackOptions); + }); + + test('accept custom profile options', async () => { + jest.spyOn(profileHandler, 'default').mockImplementation(() => spyHandler); + const profileOptions: ProfileOptions = { refetch: true }; + const baseUrl = await setup(withoutApi, { profileOptions }); + await post(baseUrl, '/api/auth/me', { body: {} }); + expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), profileOptions); }); }); From f5b54b61ed7021b9c582a512727d18db9736fd2b Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Thu, 22 Sep 2022 03:02:50 -0300 Subject: [PATCH 02/20] Document the change in the migraiton guide --- .vscode/launch.json | 16 ++++++++ V2_MIGRATION_GUIDE.md | 41 +++++++++++++++++++ .../pages/api/auth/[auth0].ts | 21 ++++++++++ 3 files changed, 78 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..f65664cb1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "1.0.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Jest: current file", + //"env": { "NODE_ENV": "test" }, + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["${fileBasenameNoExtension}", "--config", "jest.config.js"], + "console": "integratedTerminal", + "disableOptimisticBPs": true, + "runtimeExecutable": "/usr/local/bin/node" + } + ] +} diff --git a/V2_MIGRATION_GUIDE.md b/V2_MIGRATION_GUIDE.md index f2fffd1a2..7b066dbb7 100644 --- a/V2_MIGRATION_GUIDE.md +++ b/V2_MIGRATION_GUIDE.md @@ -8,6 +8,7 @@ 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) +- [Configure built-in handlers without overriding them](#configure-built-in-handlers-without-overriding-them) ## `getSession` now returns a `Promise` @@ -119,6 +120,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) { @@ -156,3 +159,41 @@ export default handleAuth({ } }); ``` + +## Configure built-in handlers without overriding them + +Previously it was not possible to dynamically configure the built-in handlers. For example, to pass a `connection` parameter to the login handler, you had to override it. + +### Before + +```js +export default handleAuth({ + async login(req, res) { + try { + await handleLogin(req, res, { + authorizationParams: { + connection: 'github' + }, + }); + } catch (error) { + // ... + } + } +}); +``` + +### After + +Now you can simply pass an options object to configure the built-in handler instead. + +```js +export default handleAuth({ + login: { + authorizationParams: { + connection: 'github' + } + } +}); +``` + + You can still override any built-in handler if needed. Pass either a custom handler function to override it, or just an options object to configure it. diff --git a/examples/kitchen-sink-example/pages/api/auth/[auth0].ts b/examples/kitchen-sink-example/pages/api/auth/[auth0].ts index 1511a73e0..12dbc2585 100644 --- a/examples/kitchen-sink-example/pages/api/auth/[auth0].ts +++ b/examples/kitchen-sink-example/pages/api/auth/[auth0].ts @@ -1,6 +1,27 @@ import { handleAuth } from '@auth0/nextjs-auth0'; export default handleAuth({ + login: { + authorizationParams: { scope: 'openid email offline_access' }, + getLoginState() { + return { foo: 'bar' }; + } + }, + logout: { returnTo: 'https://example.com/foo' }, + callback: { + async afterCallback(req, res, session) { + console.log('After callback!!'); + return session; + }, + authorizationParams: { scope: 'openid email' } + }, + profile: { + refetch: true, + async afterRefetch(req, res, session) { + console.log('After refetch!!'); + return session; + } + }, onError(req, res, error) { console.error(error); res.status(error.status || 500).end('Check the console for the error'); From b63651341cce823679c16c721578169a99e8524b Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Thu, 22 Sep 2022 03:06:26 -0300 Subject: [PATCH 03/20] Remove extra leading space --- V2_MIGRATION_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/V2_MIGRATION_GUIDE.md b/V2_MIGRATION_GUIDE.md index 7b066dbb7..143d22ff7 100644 --- a/V2_MIGRATION_GUIDE.md +++ b/V2_MIGRATION_GUIDE.md @@ -196,4 +196,4 @@ export default handleAuth({ }); ``` - You can still override any built-in handler if needed. Pass either a custom handler function to override it, or just an options object to configure it. +You can still override any built-in handler if needed. Pass either a custom handler function to override it, or just an options object to configure it. From 0c0a2df254d36aba9764056b3f8fe67e5cfb15ab Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Thu, 22 Sep 2022 03:12:28 -0300 Subject: [PATCH 04/20] Revert test changes --- .vscode/launch.json | 16 -------------- .../pages/api/auth/[auth0].ts | 21 ------------------- 2 files changed, 37 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index f65664cb1..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": "1.0.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Jest: current file", - //"env": { "NODE_ENV": "test" }, - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["${fileBasenameNoExtension}", "--config", "jest.config.js"], - "console": "integratedTerminal", - "disableOptimisticBPs": true, - "runtimeExecutable": "/usr/local/bin/node" - } - ] -} diff --git a/examples/kitchen-sink-example/pages/api/auth/[auth0].ts b/examples/kitchen-sink-example/pages/api/auth/[auth0].ts index 12dbc2585..1511a73e0 100644 --- a/examples/kitchen-sink-example/pages/api/auth/[auth0].ts +++ b/examples/kitchen-sink-example/pages/api/auth/[auth0].ts @@ -1,27 +1,6 @@ import { handleAuth } from '@auth0/nextjs-auth0'; export default handleAuth({ - login: { - authorizationParams: { scope: 'openid email offline_access' }, - getLoginState() { - return { foo: 'bar' }; - } - }, - logout: { returnTo: 'https://example.com/foo' }, - callback: { - async afterCallback(req, res, session) { - console.log('After callback!!'); - return session; - }, - authorizationParams: { scope: 'openid email' } - }, - profile: { - refetch: true, - async afterRefetch(req, res, session) { - console.log('After refetch!!'); - return session; - } - }, onError(req, res, error) { console.error(error); res.status(error.status || 500).end('Check the console for the error'); From ae5e1e7c049e63f5ec0965e2b0e7337c73f93ace Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Thu, 22 Sep 2022 03:15:16 -0300 Subject: [PATCH 05/20] Fix import order --- src/handlers/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts index 7c0bb3f15..fc6e0c43f 100644 --- a/src/handlers/auth.ts +++ b/src/handlers/auth.ts @@ -1,6 +1,6 @@ import { HandleLogin, LoginOptions } from './login'; import { HandleLogout, LogoutOptions } from './logout'; -import { CallbackOptions, HandleCallback } from './callback'; +import { HandleCallback, CallbackOptions } from './callback'; import { HandleProfile, ProfileOptions } from './profile'; import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; import { HandlerError } from '../utils/errors'; From 9364a565afab3c5e74d4936c50b16961e5c32103 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Thu, 22 Sep 2022 03:41:30 -0300 Subject: [PATCH 06/20] Add tests for custom handlers --- tests/fixtures/setup.ts | 30 ++++++++++++--------- tests/handlers/auth.test.ts | 53 ++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 14 deletions(-) diff --git a/tests/fixtures/setup.ts b/tests/fixtures/setup.ts index b74a946d4..1cfb14d02 100644 --- a/tests/fixtures/setup.ts +++ b/tests/fixtures/setup.ts @@ -13,7 +13,11 @@ import { AccessTokenRequest, Claims, OnError, - Handlers + Handlers, + HandleCallback, + HandleLogin, + HandleLogout, + HandleProfile } from '../../src'; import { codeExchange, discovery, jwksEndpoint, userInfo } from './oidc-nocks'; import { jwks, makeIdToken } from '../auth0-session/fixtures/cert'; @@ -23,6 +27,10 @@ import { post, toSignedCookieJar } from '../auth0-session/fixtures/helpers'; export type SetupOptions = { idTokenClaims?: Claims; + loginHandler?: HandleLogin; + logoutHandler?: HandleLogout; + callbackHandler?: HandleCallback; + profileHandler?: HandleProfile; callbackOptions?: CallbackOptions; loginOptions?: LoginOptions; logoutOptions?: LogoutOptions; @@ -45,6 +53,10 @@ export const setup = async ( config: ConfigParameters, { idTokenClaims, + loginHandler, + logoutHandler, + callbackHandler, + profileHandler, callbackOptions, logoutOptions, loginOptions, @@ -65,18 +77,10 @@ export const setup = async ( const { handleAuth, getSession, updateUser, getAccessToken, withApiAuthRequired, withPageAuthRequired } = await initAuth0(config); const handlers: Partial = { onError }; - if (callbackOptions) { - handlers.callback = callbackOptions; - } - if (loginOptions) { - handlers.login = loginOptions; - } - if (logoutOptions) { - handlers.logout = logoutOptions; - } - if (profileOptions) { - handlers.profile = profileOptions; - } + handlers.callback = callbackHandler ?? callbackOptions; + handlers.login = loginHandler ?? loginOptions; + handlers.logout = logoutHandler ?? logoutOptions; + handlers.profile = profileHandler ?? profileOptions; global.handleAuth = handleAuth.bind(null, handlers); global.getSession = getSession; global.updateUser = updateUser; diff --git a/tests/handlers/auth.test.ts b/tests/handlers/auth.test.ts index 066b53e9a..ae59c7421 100644 --- a/tests/handlers/auth.test.ts +++ b/tests/handlers/auth.test.ts @@ -3,7 +3,18 @@ import { ArgumentsOf } from 'ts-jest'; import { withoutApi } from '../fixtures/default-settings'; import { setup, teardown } from '../fixtures/setup'; import { get, post } from '../auth0-session/fixtures/helpers'; -import { CallbackOptions, initAuth0, LoginOptions, LogoutOptions, OnError, ProfileOptions } from '../../src'; +import { + CallbackOptions, + HandleCallback, + HandleLogin, + HandleLogout, + HandleProfile, + initAuth0, + LoginOptions, + LogoutOptions, + OnError, + ProfileOptions +} from '../../src'; import * as loginHandler from '../../src/handlers/login'; import * as logoutHandler from '../../src/handlers/logout'; import * as callbackHandler from '../../src/handlers/callback'; @@ -78,6 +89,46 @@ describe('custom error handler', () => { }); }); +describe('custom handlers', () => { + afterEach(teardown); + + test('accept custom login handler', async () => { + const spyHandler: HandleLogin = jest.fn(async (_req, res) => { + res.end(); + }); + const baseUrl = await setup(withoutApi, { loginHandler: spyHandler }); + await get(baseUrl, '/api/auth/login'); + expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse)); + }); + + test('accept custom logout handler', async () => { + const spyHandler: HandleLogout = jest.fn(async (_req, res) => { + res.end(); + }); + const baseUrl = await setup(withoutApi, { logoutHandler: spyHandler }); + await get(baseUrl, '/api/auth/logout'); + expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse)); + }); + + test('accept custom callback handler', async () => { + const spyHandler: HandleCallback = jest.fn(async (_req, res) => { + res.end(); + }); + const baseUrl = await setup(withoutApi, { callbackHandler: spyHandler }); + await post(baseUrl, '/api/auth/callback', { body: {} }); + expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse)); + }); + + test('accept custom profile handler', async () => { + const spyHandler: HandleProfile = jest.fn(async (_req, res) => { + res.end(); + }); + const baseUrl = await setup(withoutApi, { profileHandler: spyHandler }); + await post(baseUrl, '/api/auth/me', { body: {} }); + expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse)); + }); +}); + describe('custom options', () => { const spyHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => { res.end(); From adaa7de8512081adc1dc02e356bf6b6e8a8fb84e Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Tue, 4 Oct 2022 18:01:57 -0300 Subject: [PATCH 07/20] Implement solution for login handler --- src/handlers/auth.ts | 28 ++++++++++++---------------- src/handlers/login.ts | 38 ++++++++++++++++++++++++++++++++++---- src/index.browser.ts | 3 ++- src/index.ts | 3 ++- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts index fc6e0c43f..832b48ab2 100644 --- a/src/handlers/auth.ts +++ b/src/handlers/auth.ts @@ -1,8 +1,8 @@ -import { HandleLogin, LoginOptions } from './login'; -import { HandleLogout, LogoutOptions } from './logout'; -import { HandleCallback, CallbackOptions } from './callback'; -import { HandleProfile, ProfileOptions } from './profile'; import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; +import { HandleLogin } from './login'; +import { HandleLogout } from './logout'; +import { HandleCallback } from './callback'; +import { HandleProfile } from './profile'; import { HandlerError } from '../utils/errors'; /** @@ -33,10 +33,10 @@ import { HandlerError } from '../utils/errors'; * @category Server */ export interface Handlers { - login: HandleLogin | LoginOptions; - logout: HandleLogout | LogoutOptions; - callback: HandleCallback | CallbackOptions; - profile: HandleProfile | ProfileOptions; + login: HandleLogin; + logout: HandleLogout; + callback: HandleCallback; + profile: HandleProfile; onError: OnError; } @@ -107,17 +107,13 @@ export default function handlerFactory({ try { switch (route) { case 'login': - if (typeof login === 'function') return await login(req, res); - return await handleLogin(req, res, login); + return await login(req, res); case 'logout': - if (typeof logout === 'function') return await logout(req, res); - return await handleLogout(req, res, logout); + return await logout(req, res); case 'callback': - if (typeof callback === 'function') return await callback(req, res); - return await handleCallback(req, res, callback); + return await callback(req, res); case 'me': - if (typeof profile === 'function') return await profile(req, res); - return await handleProfile(req, res, profile); + return await profile(req, res); default: res.status(404).end(); } diff --git a/src/handlers/login.ts b/src/handlers/login.ts index b93ff9de7..3703ed37f 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'; @@ -157,6 +158,11 @@ export interface LoginOptions { getLoginState?: GetLoginState; } +/** + * TODO: Complete + */ +export type LoginOptionsProvider = (req: NextApiRequest) => LoginOptions; + /** * The handler for the `/api/auth/login` API route. * @@ -164,7 +170,20 @@ export interface LoginOptions { * * @category Server */ -export type HandleLogin = (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions) => Promise; +export type LoginHandler = (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions) => Promise; + +/** + * TODO: Complete + */ +export type HandleLogin = { + (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions): Promise; + (provider: LoginOptionsProvider): ( + req: NextApiRequest, + res: NextApiResponse, + options?: LoginOptions + ) => Promise; + (options: LoginOptions): (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions) => Promise; +}; /** * @ignore @@ -174,13 +193,12 @@ export default function handleLoginFactory( nextConfig: NextConfig, baseConfig: BaseConfig ): HandleLogin { - return async (req, res, options = {}): Promise => { + const login: LoginHandler = async (req: NextApiRequest, res: NextApiResponse, options = {}): Promise => { 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 }; @@ -191,10 +209,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/index.browser.ts b/src/index.browser.ts index cad7ccec0..12d76e440 100644 --- a/src/index.browser.ts +++ b/src/index.browser.ts @@ -52,7 +52,8 @@ 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 handleLogin: HandleLogin = ((...args: Parameters) => + instance.handleLogin(...args)) as HandleLogin; export const handleLogout: HandleLogout = (...args) => instance.handleLogout(...args); export const handleCallback: HandleCallback = (...args) => instance.handleCallback(...args); export const handleProfile: HandleProfile = (...args) => instance.handleProfile(...args); diff --git a/src/index.ts b/src/index.ts index f22cb545b..d42a1cfc3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -120,7 +120,8 @@ 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 handleLogin: HandleLogin = ((...args: Parameters) => + instance.handleLogin(...args)) as HandleLogin; export const handleLogout: HandleLogout = (...args) => getInstance().handleLogout(...args); export const handleCallback: HandleCallback = (...args) => getInstance().handleCallback(...args); export const handleProfile: HandleProfile = (...args) => getInstance().handleProfile(...args); From 72a274dc5936d44f7ba7220a35c120f2546022cb Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Tue, 4 Oct 2022 18:12:54 -0300 Subject: [PATCH 08/20] Implement solution for logout handler --- src/handlers/login.ts | 18 +++++++++--------- src/handlers/logout.ts | 36 ++++++++++++++++++++++++++++++++++-- src/index.browser.ts | 3 ++- src/index.ts | 3 ++- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/handlers/login.ts b/src/handlers/login.ts index 3703ed37f..8612145b8 100644 --- a/src/handlers/login.ts +++ b/src/handlers/login.ts @@ -163,15 +163,6 @@ export interface LoginOptions { */ export type LoginOptionsProvider = (req: NextApiRequest) => LoginOptions; -/** - * The handler for the `/api/auth/login` API route. - * - * @throws {@link HandlerError} - * - * @category Server - */ -export type LoginHandler = (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions) => Promise; - /** * TODO: Complete */ @@ -185,6 +176,15 @@ export type HandleLogin = { (options: LoginOptions): (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions) => Promise; }; +/** + * The handler for the `/api/auth/login` API route. + * + * @throws {@link HandlerError} + * + * @category Server + */ +export type LoginHandler = (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions) => Promise; + /** * @ignore */ diff --git a/src/handlers/logout.ts b/src/handlers/logout.ts index 0500b7126..aec338da9 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,24 @@ export interface LogoutOptions { returnTo?: string; } +/** + * TODO: Complete + */ +export type LogoutOptionsProvider = (req: NextApiRequest) => LogoutOptions; + +/** + * TODO: Complete + */ +export type HandleLogout = { + (req: NextApiRequest, res: NextApiResponse, options?: LogoutOptions): Promise; + (provider: LogoutOptionsProvider): ( + req: NextApiRequest, + res: NextApiResponse, + options?: LogoutOptions + ) => Promise; + (options: LogoutOptions): (req: NextApiRequest, res: NextApiResponse, options?: LogoutOptions) => Promise; +}; + /** * The handler for the `/api/auth/logout` API route. * @@ -25,13 +44,13 @@ export interface LogoutOptions { * * @category Server */ -export type HandleLogout = (req: NextApiRequest, res: NextApiResponse, options?: LogoutOptions) => Promise; +export type LogoutHandler = (req: NextApiRequest, res: NextApiResponse, options?: LogoutOptions) => Promise; /** * @ignore */ export default function handleLogoutFactory(handler: BaseHandleLogout): HandleLogout { - return async (req, res, options): Promise => { + const logout: LogoutHandler = async (req: NextApiRequest, res: NextApiResponse, options = {}): Promise => { try { assertReqRes(req, res); return await handler(req, res, options); @@ -39,4 +58,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/index.browser.ts b/src/index.browser.ts index 12d76e440..e44675d9b 100644 --- a/src/index.browser.ts +++ b/src/index.browser.ts @@ -54,7 +54,8 @@ export const getAccessToken: GetAccessToken = (...args) => instance.getAccessTok export const withApiAuthRequired: WithApiAuthRequired = (...args) => instance.withApiAuthRequired(...args); export const handleLogin: HandleLogin = ((...args: Parameters) => instance.handleLogin(...args)) as HandleLogin; -export const handleLogout: HandleLogout = (...args) => instance.handleLogout(...args); +export const handleLogout: HandleLogout = ((...args: Parameters) => + instance.handleLogout(...args)) as HandleLogout; export const handleCallback: HandleCallback = (...args) => instance.handleCallback(...args); export const handleProfile: HandleProfile = (...args) => instance.handleProfile(...args); export const handleAuth: HandleAuth = (...args) => instance.handleAuth(...args); diff --git a/src/index.ts b/src/index.ts index d42a1cfc3..c53d40c63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -122,7 +122,8 @@ export const withApiAuthRequired: WithApiAuthRequired = (...args) => getInstance export const withPageAuthRequired: WithPageAuthRequired = withPageAuthRequiredFactory(getLoginUrl(), getSessionCache); export const handleLogin: HandleLogin = ((...args: Parameters) => instance.handleLogin(...args)) as HandleLogin; -export const handleLogout: HandleLogout = (...args) => getInstance().handleLogout(...args); +export const handleLogout: HandleLogout = ((...args: Parameters) => + instance.handleLogout(...args)) as HandleLogout; export const handleCallback: HandleCallback = (...args) => getInstance().handleCallback(...args); export const handleProfile: HandleProfile = (...args) => getInstance().handleProfile(...args); export const handleAuth: HandleAuth = (...args) => getInstance().handleAuth(...args); From 893d08ab8501f65cd277412a3733512ffa259073 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Tue, 4 Oct 2022 18:18:40 -0300 Subject: [PATCH 09/20] Implement solution for callback handler --- src/handlers/callback.ts | 32 ++++++++++++++++++++++++++++++-- src/handlers/login.ts | 8 ++------ src/handlers/logout.ts | 8 ++------ src/index.browser.ts | 3 ++- src/index.ts | 3 ++- 5 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/handlers/callback.ts b/src/handlers/callback.ts index 4a165f994..476518e6b 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'; @@ -97,6 +98,20 @@ export interface CallbackOptions { authorizationParams?: Partial; } +/** + * TODO: Complete + */ +export type CallbackOptionsProvider = (req: NextApiRequest) => CallbackOptions; + +/** + * TODO: Complete + */ +export type HandleCallback = { + (req: NextApiRequest, res: NextApiResponse, options?: CallbackOptions): Promise; + (provider: CallbackOptionsProvider): CallbackHandler; + (options: CallbackOptions): CallbackHandler; +}; + /** * The handler for the `/api/auth/callback` API route. * @@ -104,7 +119,7 @@ export interface CallbackOptions { * * @category Server */ -export type HandleCallback = (req: NextApiRequest, res: NextApiResponse, options?: CallbackOptions) => Promise; +export type CallbackHandler = (req: NextApiRequest, res: NextApiResponse, options?: CallbackOptions) => Promise; /** * @ignore @@ -131,7 +146,7 @@ const idTokenValidator = * @ignore */ export default function handleCallbackFactory(handler: BaseHandleCallback, config: NextConfig): HandleCallback { - return async (req, res, options = {}): Promise => { + const callback: CallbackHandler = async (req: NextApiRequest, res: NextApiResponse, options = {}): Promise => { try { assertReqRes(req, res); return await handler(req, res, { @@ -142,4 +157,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 8612145b8..7c5a37917 100644 --- a/src/handlers/login.ts +++ b/src/handlers/login.ts @@ -168,12 +168,8 @@ export type LoginOptionsProvider = (req: NextApiRequest) => LoginOptions; */ export type HandleLogin = { (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions): Promise; - (provider: LoginOptionsProvider): ( - req: NextApiRequest, - res: NextApiResponse, - options?: LoginOptions - ) => Promise; - (options: LoginOptions): (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions) => Promise; + (provider: LoginOptionsProvider): LoginHandler; + (options: LoginOptions): LoginHandler; }; /** diff --git a/src/handlers/logout.ts b/src/handlers/logout.ts index aec338da9..1858f05fe 100644 --- a/src/handlers/logout.ts +++ b/src/handlers/logout.ts @@ -29,12 +29,8 @@ export type LogoutOptionsProvider = (req: NextApiRequest) => LogoutOptions; */ export type HandleLogout = { (req: NextApiRequest, res: NextApiResponse, options?: LogoutOptions): Promise; - (provider: LogoutOptionsProvider): ( - req: NextApiRequest, - res: NextApiResponse, - options?: LogoutOptions - ) => Promise; - (options: LogoutOptions): (req: NextApiRequest, res: NextApiResponse, options?: LogoutOptions) => Promise; + (provider: LogoutOptionsProvider): LogoutHandler; + (options: LogoutOptions): LogoutHandler; }; /** diff --git a/src/index.browser.ts b/src/index.browser.ts index e44675d9b..5e114fe19 100644 --- a/src/index.browser.ts +++ b/src/index.browser.ts @@ -56,6 +56,7 @@ export const handleLogin: HandleLogin = ((...args: Parameters) => instance.handleLogin(...args)) as HandleLogin; export const handleLogout: HandleLogout = ((...args: Parameters) => instance.handleLogout(...args)) as HandleLogout; -export const handleCallback: HandleCallback = (...args) => instance.handleCallback(...args); +export const handleCallback: HandleCallback = ((...args: Parameters) => + instance.handleCallback(...args)) as HandleCallback; export const handleProfile: HandleProfile = (...args) => instance.handleProfile(...args); export const handleAuth: HandleAuth = (...args) => instance.handleAuth(...args); diff --git a/src/index.ts b/src/index.ts index c53d40c63..fc2680475 100644 --- a/src/index.ts +++ b/src/index.ts @@ -124,7 +124,8 @@ export const handleLogin: HandleLogin = ((...args: Parameters) => instance.handleLogin(...args)) as HandleLogin; export const handleLogout: HandleLogout = ((...args: Parameters) => instance.handleLogout(...args)) as HandleLogout; -export const handleCallback: HandleCallback = (...args) => getInstance().handleCallback(...args); +export const handleCallback: HandleCallback = ((...args: Parameters) => + instance.handleCallback(...args)) as HandleCallback; export const handleProfile: HandleProfile = (...args) => getInstance().handleProfile(...args); export const handleAuth: HandleAuth = (...args) => getInstance().handleAuth(...args); From e37c82ab003ccb9b2e512d99344d3ec7c6483827 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Tue, 4 Oct 2022 18:25:41 -0300 Subject: [PATCH 10/20] Implement solution for profile handler --- src/handlers/profile.ts | 32 ++++++++++++++++++++++++++++++-- src/index.browser.ts | 3 ++- src/index.ts | 9 +++++---- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/handlers/profile.ts b/src/handlers/profile.ts index 7d7fbf28d..c702b21eb 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,20 @@ export type ProfileOptions = { afterRefetch?: AfterRefetch; }; +/** + * TODO: Complete + */ +export type ProfileOptionsProvider = (req: NextApiRequest) => ProfileOptions; + +/** + * TODO: Complete + */ +export type HandleProfile = { + (req: NextApiRequest, res: NextApiResponse, options?: ProfileOptions): Promise; + (provider: ProfileOptionsProvider): ProfileHandler; + (options: ProfileOptions): ProfileHandler; +}; + /** * The handler for the `/api/auth/me` API route. * @@ -35,7 +50,7 @@ export type ProfileOptions = { * * @category Server */ -export type HandleProfile = (req: NextApiRequest, res: NextApiResponse, options?: ProfileOptions) => Promise; +export type ProfileHandler = (req: NextApiRequest, res: NextApiResponse, options?: ProfileOptions) => Promise; /** * @ignore @@ -45,7 +60,7 @@ export default function profileHandler( getAccessToken: GetAccessToken, sessionCache: SessionCache ): HandleProfile { - return async (req, res, options): Promise => { + const profile: ProfileHandler = async (req: NextApiRequest, res: NextApiResponse, options = {}): Promise => { try { assertReqRes(req, res); @@ -89,4 +104,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 5e114fe19..15c5ad25e 100644 --- a/src/index.browser.ts +++ b/src/index.browser.ts @@ -58,5 +58,6 @@ export const handleLogout: HandleLogout = ((...args: Parameters) = instance.handleLogout(...args)) as HandleLogout; export const handleCallback: HandleCallback = ((...args: Parameters) => instance.handleCallback(...args)) as HandleCallback; -export const handleProfile: HandleProfile = (...args) => instance.handleProfile(...args); +export const handleProfile: HandleProfile = ((...args: Parameters) => + instance.handleProfile(...args)) as HandleProfile; export const handleAuth: HandleAuth = (...args) => instance.handleAuth(...args); diff --git a/src/index.ts b/src/index.ts index fc2680475..d4b50c77a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -121,12 +121,13 @@ export const getAccessToken: GetAccessToken = (...args) => getInstance().getAcce export const withApiAuthRequired: WithApiAuthRequired = (...args) => getInstance().withApiAuthRequired(...args); export const withPageAuthRequired: WithPageAuthRequired = withPageAuthRequiredFactory(getLoginUrl(), getSessionCache); export const handleLogin: HandleLogin = ((...args: Parameters) => - instance.handleLogin(...args)) as HandleLogin; + getInstance().handleLogin(...args)) as HandleLogin; export const handleLogout: HandleLogout = ((...args: Parameters) => - instance.handleLogout(...args)) as HandleLogout; + getInstance().handleLogout(...args)) as HandleLogout; export const handleCallback: HandleCallback = ((...args: Parameters) => - instance.handleCallback(...args)) as HandleCallback; -export const handleProfile: HandleProfile = (...args) => getInstance().handleProfile(...args); + getInstance().handleCallback(...args)) as HandleCallback; +export const handleProfile: HandleProfile = ((...args: Parameters) => + getInstance().handleProfile(...args)) as HandleProfile; export const handleAuth: HandleAuth = (...args) => getInstance().handleAuth(...args); export { From 313fe094bde38f1b7bcb9bea543655e81e8c6835 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Tue, 4 Oct 2022 20:21:12 -0300 Subject: [PATCH 11/20] Fix handler types --- src/handlers/auth.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts index 832b48ab2..dc47b80d9 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 { HandleLogin, LoginHandler } from './login'; +import { HandleLogout, LogoutHandler } from './logout'; +import { CallbackHandler, HandleCallback } from './callback'; +import { HandleProfile, ProfileHandler } from './profile'; import { HandlerError } from '../utils/errors'; /** @@ -33,10 +33,10 @@ import { HandlerError } from '../utils/errors'; * @category Server */ export interface Handlers { - login: HandleLogin; - logout: HandleLogout; - callback: HandleCallback; - profile: HandleProfile; + login: LoginHandler; + logout: LogoutHandler; + callback: CallbackHandler; + profile: ProfileHandler; onError: OnError; } From 0854f8b7bb7de25f559a9bd62f2f7c3e4da4e049 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 5 Oct 2022 01:55:02 -0300 Subject: [PATCH 12/20] Add support for arbitrary custom handlers --- src/handlers/auth.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts index dc47b80d9..f06b69476 100644 --- a/src/handlers/auth.ts +++ b/src/handlers/auth.ts @@ -38,6 +38,7 @@ export interface Handlers { callback: CallbackHandler; profile: ProfileHandler; onError: OnError; + [key: string]: any; } /** @@ -90,7 +91,7 @@ export default function handlerFactory({ handleProfile: HandleProfile; }): HandleAuth { return ({ onError, ...handlers }: Partial = {}): NextApiHandler => { - const { login, logout, callback, profile } = { + const { login, logout, callback, profile, ...customHandlers }: Omit = { login: handleLogin, logout: handleLogout, callback: handleCallback, @@ -115,6 +116,9 @@ export default function handlerFactory({ case 'me': return await profile(req, res); default: + if (route && customHandlers[route]) { + return await customHandlers[route](req, res); + } res.status(404).end(); } } catch (error) { From d1c44d5900b00dae643193d808cdd661edd3cfc8 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 5 Oct 2022 21:51:28 -0300 Subject: [PATCH 13/20] Add unit tests --- src/handlers/profile.ts | 2 +- tests/fixtures/setup.ts | 58 +++++++--- tests/handlers/auth.test.ts | 223 ++++++++++++++++++++++++++---------- 3 files changed, 205 insertions(+), 78 deletions(-) diff --git a/src/handlers/profile.ts b/src/handlers/profile.ts index c702b21eb..de0ec26bb 100644 --- a/src/handlers/profile.ts +++ b/src/handlers/profile.ts @@ -72,7 +72,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'); diff --git a/tests/fixtures/setup.ts b/tests/fixtures/setup.ts index 1cfb14d02..74d481087 100644 --- a/tests/fixtures/setup.ts +++ b/tests/fixtures/setup.ts @@ -13,27 +13,27 @@ import { AccessTokenRequest, Claims, OnError, - Handlers, - HandleCallback, - HandleLogin, - HandleLogout, - HandleProfile + Handlers } from '../../src'; import { codeExchange, discovery, jwksEndpoint, userInfo } from './oidc-nocks'; 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 } from '../../src/handlers/login'; +import { HandleLogout } from '../../src/handlers/logout'; +import { HandleCallback } from '../../src/handlers/callback'; +import { HandleProfile } from '../../src/handlers/profile'; export type SetupOptions = { idTokenClaims?: Claims; - loginHandler?: HandleLogin; - logoutHandler?: HandleLogout; callbackHandler?: HandleCallback; - profileHandler?: HandleProfile; callbackOptions?: CallbackOptions; + loginHandler?: HandleLogin; loginOptions?: LoginOptions; + logoutHandler?: HandleLogout; logoutOptions?: LogoutOptions; + profileHandler?: HandleProfile; profileOptions?: ProfileOptions; withPageAuthRequiredOptions?: WithPageAuthRequiredOptions; getAccessTokenOptions?: AccessTokenRequest; @@ -53,13 +53,13 @@ export const setup = async ( config: ConfigParameters, { idTokenClaims, - loginHandler, - logoutHandler, callbackHandler, - profileHandler, callbackOptions, + logoutHandler, logoutOptions, + loginHandler, loginOptions, + profileHandler, profileOptions, withPageAuthRequiredOptions, onError = defaultOnError, @@ -74,13 +74,35 @@ export const setup = async ( jwksEndpoint(config, jwks); codeExchange(config, await makeIdToken({ iss: 'https://acme.auth0.local/', ...idTokenClaims })); userInfo(config, userInfoToken, userInfoPayload); - const { handleAuth, getSession, updateUser, getAccessToken, withApiAuthRequired, withPageAuthRequired } = - await initAuth0(config); - const handlers: Partial = { onError }; - handlers.callback = callbackHandler ?? callbackOptions; - handlers.login = loginHandler ?? loginOptions; - handlers.logout = logoutHandler ?? logoutOptions; - handlers.profile = profileHandler ?? profileOptions; + const { + handleAuth, + handleCallback, + handleLogin, + handleLogout, + handleProfile, + getSession, + updateUser, + getAccessToken, + withApiAuthRequired, + withPageAuthRequired + } = initAuth0(config); + const callback = callbackHandler || handleCallback; + const login = loginHandler || handleLogin; + const logout = logoutHandler || handleLogout; + const profile = profileHandler || handleProfile; + const handlers: Partial = { onError, callback, login, logout, profile }; + if (callbackOptions) { + handlers.callback = (req, res) => callback(req, res, callbackOptions); + } + if (loginOptions) { + handlers.login = (req, res) => login(req, res, loginOptions); + } + if (logoutOptions) { + handlers.logout = (req, res) => logout(req, res, logoutOptions); + } + if (profileOptions) { + handlers.profile = (req, res) => profile(req, res, profileOptions); + } global.handleAuth = handleAuth.bind(null, handlers); global.getSession = getSession; global.updateUser = updateUser; diff --git a/tests/handlers/auth.test.ts b/tests/handlers/auth.test.ts index ae59c7421..5dbfb3533 100644 --- a/tests/handlers/auth.test.ts +++ b/tests/handlers/auth.test.ts @@ -1,24 +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 { get, post } from '../auth0-session/fixtures/helpers'; -import { - CallbackOptions, - HandleCallback, - HandleLogin, - HandleLogout, - HandleProfile, - initAuth0, - LoginOptions, - LogoutOptions, - OnError, - ProfileOptions -} from '../../src'; -import * as loginHandler from '../../src/handlers/login'; -import * as logoutHandler from '../../src/handlers/logout'; -import * as callbackHandler from '../../src/handlers/callback'; -import * as profileHandler from '../../src/handlers/profile'; +import { login, setup, teardown } from '../fixtures/setup'; +import { get } from '../auth0-session/fixtures/helpers'; +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 = (status = 400, error = 'foo', error_description = 'bar') => expect.objectContaining({ @@ -60,7 +52,7 @@ describe('custom error 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(() => {}); @@ -71,7 +63,7 @@ describe('custom error 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', { fullResponse: true }) ).rejects.toThrow('Internal Server Error'); @@ -81,7 +73,7 @@ describe('custom error handler', () => { test('finish response with custom error status', async () => { const onError = jest.fn>((_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', { fullResponse: true }) ).rejects.toThrow("I'm a Teapot"); @@ -93,80 +85,193 @@ describe('custom handlers', () => { afterEach(teardown); test('accept custom login handler', async () => { - const spyHandler: HandleLogin = jest.fn(async (_req, res) => { + const login = jest.fn, ArgumentsOf>(async (_req, res) => { res.end(); }); - const baseUrl = await setup(withoutApi, { loginHandler: spyHandler }); + const baseUrl = await setup(withoutApi); + global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { login }); await get(baseUrl, '/api/auth/login'); - expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse)); + expect(login).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse)); }); test('accept custom logout handler', async () => { - const spyHandler: HandleLogout = jest.fn(async (_req, res) => { + const logout = jest.fn, ArgumentsOf>(async (_req, res) => { res.end(); }); - const baseUrl = await setup(withoutApi, { logoutHandler: spyHandler }); + const baseUrl = await setup(withoutApi); + global.handleAuth = initAuth0(withoutApi).handleAuth.bind(null, { logout }); await get(baseUrl, '/api/auth/logout'); - expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse)); + expect(logout).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse)); }); test('accept custom callback handler', async () => { - const spyHandler: HandleCallback = jest.fn(async (_req, res) => { + const callback = jest.fn, ArgumentsOf>(async (_req, res) => { res.end(); }); - const baseUrl = await setup(withoutApi, { callbackHandler: spyHandler }); - await post(baseUrl, '/api/auth/callback', { body: {} }); - expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse)); + 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 spyHandler: HandleProfile = jest.fn(async (_req, res) => { + const profile = jest.fn, ArgumentsOf>(async (_req, res) => { res.end(); }); - const baseUrl = await setup(withoutApi, { profileHandler: spyHandler }); - await post(baseUrl, '/api/auth/me', { body: {} }); - expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse)); + 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)); }); -}); -describe('custom options', () => { - const spyHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => { - res.end(); + test('accept custom arbitrary handler', async () => { + const signup = jest.fn, ArgumentsOf>(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 () => { - jest.spyOn(loginHandler, 'default').mockImplementation(() => spyHandler); - const loginOptions: LoginOptions = { - authorizationParams: { scope: 'openid' } - }; - const baseUrl = await setup(withoutApi, { loginOptions }); + const loginHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => { + res.end(); + }); + jest.spyOn(baseLoginHandler, 'default').mockImplementation(() => loginHandler); + const loginOptions: LoginOptions = { authorizationParams: { scope: 'openid' } }; + const baseUrl = await setup(withoutApi); + const { handleLogin, handleAuth } = initAuth0(withoutApi); + global.handleAuth = handleAuth.bind(null, { + login: handleLogin(loginOptions) + }); await get(baseUrl, '/api/auth/login'); - expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), loginOptions); + expect(loginHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), loginOptions); }); test('accept custom logout options', async () => { - jest.spyOn(logoutHandler, 'default').mockImplementation(() => spyHandler); - const logoutOptions: LogoutOptions = { returnTo: 'https://example.com' }; - const baseUrl = await setup(withoutApi, { logoutOptions }); + const logoutHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => { + res.end(); + }); + jest.spyOn(baseLogoutHandler, 'default').mockImplementation(() => logoutHandler); + const logoutOptions: LogoutOptions = { returnTo: '/foo' }; + const baseUrl = await setup(withoutApi); + const { handleLogout, handleAuth } = initAuth0(withoutApi); + global.handleAuth = handleAuth.bind(null, { + logout: handleLogout(logoutOptions) + }); await get(baseUrl, '/api/auth/logout'); - expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), logoutOptions); + expect(logoutHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), logoutOptions); }); test('accept custom callback options', async () => { - jest.spyOn(callbackHandler, 'default').mockImplementation(() => spyHandler); - const callbackOptions: CallbackOptions = { authorizationParams: { scope: 'openid' } }; - const baseUrl = await setup(withoutApi, { callbackOptions }); - await post(baseUrl, '/api/auth/callback', { body: {} }); - expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), callbackOptions); + const callbackHandler = jest.fn(async (_req: IncomingMessage, res: ServerResponse) => { + res.end(); + }); + jest.spyOn(baseCallbackHandler, 'default').mockImplementation(() => callbackHandler); + const callbackOptions: CallbackOptions = { redirectUri: '/foo' }; + const baseUrl = await setup(withoutApi); + const { handleCallback, handleAuth } = initAuth0(withoutApi); + global.handleAuth = handleAuth.bind(null, { + callback: handleCallback(callbackOptions) + }); + await get(baseUrl, '/api/auth/callback'); + expect(callbackHandler).toHaveBeenCalledWith( + expect.any(IncomingMessage), + expect.any(ServerResponse), + expect.objectContaining(callbackOptions) + ); }); test('accept custom profile options', async () => { - jest.spyOn(profileHandler, 'default').mockImplementation(() => spyHandler); - const profileOptions: ProfileOptions = { refetch: true }; - const baseUrl = await setup(withoutApi, { profileOptions }); - await post(baseUrl, '/api/auth/me', { body: {} }); - expect(spyHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), profileOptions); + const afterRefetch = jest.fn(async (_req: IncomingMessage, _res: ServerResponse, session: Session) => session); + const profileOptions: ProfileOptions = { refetch: true, afterRefetch }; + const baseUrl = await setup(withoutApi); + const { handleProfile, handleAuth } = initAuth0(withoutApi); + global.handleAuth = handleAuth.bind(null, { + profile: handleProfile(profileOptions) + }); + 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 loginOptions = { authorizationParams: { scope: 'openid' } }; + const loginOptionsProvider = jest.fn(() => loginOptions); + const baseUrl = await setup(withoutApi); + const { handleLogin, handleAuth } = initAuth0(withoutApi); + + global.handleAuth = handleAuth.bind(null, { + login: handleLogin(loginOptionsProvider) + }); + await get(baseUrl, '/api/auth/login'); + expect(loginOptionsProvider).toHaveBeenCalled(); + expect(loginHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), loginOptions); + }); + + 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 logoutOptions: LogoutOptions = { returnTo: '/foo' }; + const logoutOptionsProvider = jest.fn(() => logoutOptions); + const baseUrl = await setup(withoutApi); + const { handleLogout, handleAuth } = initAuth0(withoutApi); + global.handleAuth = handleAuth.bind(null, { + logout: handleLogout(logoutOptionsProvider) + }); + await get(baseUrl, '/api/auth/logout'); + expect(logoutOptionsProvider).toHaveBeenCalled(); + expect(logoutHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), logoutOptions); + }); + + 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 callbackOptions: CallbackOptions = { redirectUri: '/foo' }; + const callbackOptionsProvider = jest.fn(() => callbackOptions); + const baseUrl = await setup(withoutApi); + const { handleCallback, handleAuth } = initAuth0(withoutApi); + global.handleAuth = handleAuth.bind(null, { + callback: handleCallback(callbackOptionsProvider) + }); + await get(baseUrl, '/api/auth/callback'); + expect(callbackOptionsProvider).toHaveBeenCalled(); + expect(callbackHandler).toHaveBeenCalledWith( + expect.any(IncomingMessage), + expect.any(ServerResponse), + expect.objectContaining(callbackOptions) + ); + }); + + test('accept custom profile options provider', async () => { + const afterRefetch = jest.fn(async (_req: IncomingMessage, _res: ServerResponse, session: Session) => session); + const profileOptions: ProfileOptions = { refetch: true, afterRefetch }; + const profileOptionsProvider = jest.fn(() => profileOptions); + const baseUrl = await setup(withoutApi); + const { handleProfile, handleAuth } = initAuth0(withoutApi); + global.handleAuth = handleAuth.bind(null, { + profile: handleProfile(profileOptionsProvider) + }); + const cookieJar = await login(baseUrl); + await get(baseUrl, '/api/auth/me', { cookieJar }); + expect(profileOptionsProvider).toHaveBeenCalled(); + expect(afterRefetch).toHaveBeenCalled(); }); }); From 87e03bf49ec1f42319d528710cc3083d895b0b2f Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 5 Oct 2022 21:56:05 -0300 Subject: [PATCH 14/20] Simplify test var names --- tests/handlers/auth.test.ts | 60 ++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/handlers/auth.test.ts b/tests/handlers/auth.test.ts index 5dbfb3533..846b50403 100644 --- a/tests/handlers/auth.test.ts +++ b/tests/handlers/auth.test.ts @@ -143,14 +143,14 @@ describe('custom options', () => { res.end(); }); jest.spyOn(baseLoginHandler, 'default').mockImplementation(() => loginHandler); - const loginOptions: LoginOptions = { authorizationParams: { scope: 'openid' } }; + const options: LoginOptions = { authorizationParams: { scope: 'openid' } }; const baseUrl = await setup(withoutApi); const { handleLogin, handleAuth } = initAuth0(withoutApi); global.handleAuth = handleAuth.bind(null, { - login: handleLogin(loginOptions) + login: handleLogin(options) }); await get(baseUrl, '/api/auth/login'); - expect(loginHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), loginOptions); + expect(loginHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), options); }); test('accept custom logout options', async () => { @@ -158,14 +158,14 @@ describe('custom options', () => { res.end(); }); jest.spyOn(baseLogoutHandler, 'default').mockImplementation(() => logoutHandler); - const logoutOptions: LogoutOptions = { returnTo: '/foo' }; + const options: LogoutOptions = { returnTo: '/foo' }; const baseUrl = await setup(withoutApi); const { handleLogout, handleAuth } = initAuth0(withoutApi); global.handleAuth = handleAuth.bind(null, { - logout: handleLogout(logoutOptions) + logout: handleLogout(options) }); await get(baseUrl, '/api/auth/logout'); - expect(logoutHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), logoutOptions); + expect(logoutHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), options); }); test('accept custom callback options', async () => { @@ -173,27 +173,27 @@ describe('custom options', () => { res.end(); }); jest.spyOn(baseCallbackHandler, 'default').mockImplementation(() => callbackHandler); - const callbackOptions: CallbackOptions = { redirectUri: '/foo' }; + const options: CallbackOptions = { redirectUri: '/foo' }; const baseUrl = await setup(withoutApi); const { handleCallback, handleAuth } = initAuth0(withoutApi); global.handleAuth = handleAuth.bind(null, { - callback: handleCallback(callbackOptions) + callback: handleCallback(options) }); await get(baseUrl, '/api/auth/callback'); expect(callbackHandler).toHaveBeenCalledWith( expect.any(IncomingMessage), expect.any(ServerResponse), - expect.objectContaining(callbackOptions) + expect.objectContaining(options) ); }); test('accept custom profile options', async () => { const afterRefetch = jest.fn(async (_req: IncomingMessage, _res: ServerResponse, session: Session) => session); - const profileOptions: ProfileOptions = { refetch: true, afterRefetch }; + const options: ProfileOptions = { refetch: true, afterRefetch }; const baseUrl = await setup(withoutApi); const { handleProfile, handleAuth } = initAuth0(withoutApi); global.handleAuth = handleAuth.bind(null, { - profile: handleProfile(profileOptions) + profile: handleProfile(options) }); const cookieJar = await login(baseUrl); await get(baseUrl, '/api/auth/me', { cookieJar }); @@ -209,17 +209,17 @@ describe('custom options providers', () => { res.end(); }); jest.spyOn(baseLoginHandler, 'default').mockImplementation(() => loginHandler); - const loginOptions = { authorizationParams: { scope: 'openid' } }; - const loginOptionsProvider = jest.fn(() => loginOptions); + 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(loginOptionsProvider) + login: handleLogin(optionsProvider) }); await get(baseUrl, '/api/auth/login'); - expect(loginOptionsProvider).toHaveBeenCalled(); - expect(loginHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), loginOptions); + expect(optionsProvider).toHaveBeenCalled(); + expect(loginHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), options); }); test('accept custom logout options provider', async () => { @@ -227,16 +227,16 @@ describe('custom options providers', () => { res.end(); }); jest.spyOn(baseLogoutHandler, 'default').mockImplementation(() => logoutHandler); - const logoutOptions: LogoutOptions = { returnTo: '/foo' }; - const logoutOptionsProvider = jest.fn(() => logoutOptions); + 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(logoutOptionsProvider) + logout: handleLogout(optionsProvider) }); await get(baseUrl, '/api/auth/logout'); - expect(logoutOptionsProvider).toHaveBeenCalled(); - expect(logoutHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), logoutOptions); + expect(optionsProvider).toHaveBeenCalled(); + expect(logoutHandler).toHaveBeenCalledWith(expect.any(IncomingMessage), expect.any(ServerResponse), options); }); test('accept custom callback options provider', async () => { @@ -244,34 +244,34 @@ describe('custom options providers', () => { res.end(); }); jest.spyOn(baseCallbackHandler, 'default').mockImplementation(() => callbackHandler); - const callbackOptions: CallbackOptions = { redirectUri: '/foo' }; - const callbackOptionsProvider = jest.fn(() => callbackOptions); + 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(callbackOptionsProvider) + callback: handleCallback(optionsProvider) }); await get(baseUrl, '/api/auth/callback'); - expect(callbackOptionsProvider).toHaveBeenCalled(); + expect(optionsProvider).toHaveBeenCalled(); expect(callbackHandler).toHaveBeenCalledWith( expect.any(IncomingMessage), expect.any(ServerResponse), - expect.objectContaining(callbackOptions) + expect.objectContaining(options) ); }); test('accept custom profile options provider', async () => { const afterRefetch = jest.fn(async (_req: IncomingMessage, _res: ServerResponse, session: Session) => session); - const profileOptions: ProfileOptions = { refetch: true, afterRefetch }; - const profileOptionsProvider = jest.fn(() => profileOptions); + 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(profileOptionsProvider) + profile: handleProfile(optionsProvider) }); const cookieJar = await login(baseUrl); await get(baseUrl, '/api/auth/me', { cookieJar }); - expect(profileOptionsProvider).toHaveBeenCalled(); + expect(optionsProvider).toHaveBeenCalled(); expect(afterRefetch).toHaveBeenCalled(); }); }); From 708f039e73f7c60abe40fa322e9e7af36b3d89bc Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 5 Oct 2022 22:00:26 -0300 Subject: [PATCH 15/20] Add missing types in test app --- tests/fixtures/test-app/pages/api/auth/[...auth0].ts | 1 + tests/fixtures/test-app/pages/api/session.ts | 2 +- tests/fixtures/test-app/pages/api/update-user.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/test-app/pages/api/auth/[...auth0].ts b/tests/fixtures/test-app/pages/api/auth/[...auth0].ts index ca9702e6f..508718b18 100644 --- a/tests/fixtures/test-app/pages/api/auth/[...auth0].ts +++ b/tests/fixtures/test-app/pages/api/auth/[...auth0].ts @@ -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 9c88f9eda..5fc472471 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 { 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 e48b4b7e2..d791ea4f0 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 { const session = await global.getSession?.(req, res); const updated = { ...session?.user, ...req.body?.user }; await global.updateUser?.(req, res, updated); From da1f7ad979aea27336b513ff78c17832d451a84b Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 5 Oct 2022 23:17:39 -0300 Subject: [PATCH 16/20] Add API docs --- src/handlers/callback.ts | 54 +++++++++++++++++++++++++++++++++++-- src/handlers/login.ts | 58 ++++++++++++++++++++++++++++++++++++++-- src/handlers/logout.ts | 54 +++++++++++++++++++++++++++++++++++-- src/handlers/profile.ts | 52 +++++++++++++++++++++++++++++++++-- 4 files changed, 210 insertions(+), 8 deletions(-) diff --git a/src/handlers/callback.ts b/src/handlers/callback.ts index 476518e6b..b7a0a725f 100644 --- a/src/handlers/callback.ts +++ b/src/handlers/callback.ts @@ -99,12 +99,62 @@ export interface CallbackOptions { } /** - * TODO: Complete + * Options provider for the built-in callback handler. + * Use this to generate options that depend on values from the request. + * + * @category Server */ export type CallbackOptionsProvider = (req: NextApiRequest) => CallbackOptions; /** - * TODO: Complete + * Use this to customize the built-in 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; diff --git a/src/handlers/login.ts b/src/handlers/login.ts index 7c5a37917..db462532a 100644 --- a/src/handlers/login.ts +++ b/src/handlers/login.ts @@ -159,12 +159,66 @@ export interface LoginOptions { } /** - * TODO: Complete + * Options provider for the built-in login handler. + * Use this to generate options that depend on values from the request. + * + * @category Server */ export type LoginOptionsProvider = (req: NextApiRequest) => LoginOptions; /** - * TODO: Complete + * Use this to customize the built-in 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; diff --git a/src/handlers/logout.ts b/src/handlers/logout.ts index 1858f05fe..8215139d3 100644 --- a/src/handlers/logout.ts +++ b/src/handlers/logout.ts @@ -20,12 +20,62 @@ export interface LogoutOptions { } /** - * TODO: Complete + * Options provider for the built-in logout handler. + * Use this to generate options that depend on values from the request. + * + * @category Server */ export type LogoutOptionsProvider = (req: NextApiRequest) => LogoutOptions; /** - * TODO: Complete + * Use this to customize the built-in 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; diff --git a/src/handlers/profile.ts b/src/handlers/profile.ts index de0ec26bb..04f3729e8 100644 --- a/src/handlers/profile.ts +++ b/src/handlers/profile.ts @@ -30,12 +30,60 @@ export type ProfileOptions = { }; /** - * TODO: Complete + * Options provider for the built-in profile handler. + * Use this to generate options that depend on values from the request. + * + * @category Server */ export type ProfileOptionsProvider = (req: NextApiRequest) => ProfileOptions; /** - * TODO: Complete + * Use this to customize the built-in 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; From fb9440c9c17c6b39019e1f5dbb3446e57cf64378 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 5 Oct 2022 23:32:08 -0300 Subject: [PATCH 17/20] Add more API docs --- src/handlers/auth.ts | 29 +++++++++++++++++++++++++++-- src/handlers/callback.ts | 4 ++-- src/handlers/login.ts | 4 ++-- src/handlers/logout.ts | 4 ++-- src/handlers/profile.ts | 4 ++-- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts index f06b69476..7844e788b 100644 --- a/src/handlers/auth.ts +++ b/src/handlers/auth.ts @@ -30,6 +30,32 @@ import { HandlerError } from '../utils/errors'; * }); * ``` * + * 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 { @@ -45,8 +71,7 @@ export interface Handlers { * 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 diff --git a/src/handlers/callback.ts b/src/handlers/callback.ts index b7a0a725f..a1fb086bd 100644 --- a/src/handlers/callback.ts +++ b/src/handlers/callback.ts @@ -99,7 +99,7 @@ export interface CallbackOptions { } /** - * Options provider for the built-in callback handler. + * Options provider for the default callback handler. * Use this to generate options that depend on values from the request. * * @category Server @@ -107,7 +107,7 @@ export interface CallbackOptions { export type CallbackOptionsProvider = (req: NextApiRequest) => CallbackOptions; /** - * Use this to customize the built-in callback handler without overriding it. + * Use this to customize the default callback handler without overriding it. * You can still override the handler if needed. * * @example Pass an options object diff --git a/src/handlers/login.ts b/src/handlers/login.ts index db462532a..5d1bbc994 100644 --- a/src/handlers/login.ts +++ b/src/handlers/login.ts @@ -159,7 +159,7 @@ export interface LoginOptions { } /** - * Options provider for the built-in login handler. + * Options provider for the default login handler. * Use this to generate options that depend on values from the request. * * @category Server @@ -167,7 +167,7 @@ export interface LoginOptions { export type LoginOptionsProvider = (req: NextApiRequest) => LoginOptions; /** - * Use this to customize the built-in login handler without overriding it. + * Use this to customize the default login handler without overriding it. * You can still override the handler if needed. * * @example Pass an options object diff --git a/src/handlers/logout.ts b/src/handlers/logout.ts index 8215139d3..fc51b490a 100644 --- a/src/handlers/logout.ts +++ b/src/handlers/logout.ts @@ -20,7 +20,7 @@ export interface LogoutOptions { } /** - * Options provider for the built-in logout handler. + * Options provider for the default logout handler. * Use this to generate options that depend on values from the request. * * @category Server @@ -28,7 +28,7 @@ export interface LogoutOptions { export type LogoutOptionsProvider = (req: NextApiRequest) => LogoutOptions; /** - * Use this to customize the built-in logout handler without overriding it. + * Use this to customize the default logout handler without overriding it. * You can still override the handler if needed. * * @example Pass an options object diff --git a/src/handlers/profile.ts b/src/handlers/profile.ts index 04f3729e8..eba12ba84 100644 --- a/src/handlers/profile.ts +++ b/src/handlers/profile.ts @@ -30,7 +30,7 @@ export type ProfileOptions = { }; /** - * Options provider for the built-in profile handler. + * Options provider for the default profile handler. * Use this to generate options that depend on values from the request. * * @category Server @@ -38,7 +38,7 @@ export type ProfileOptions = { export type ProfileOptionsProvider = (req: NextApiRequest) => ProfileOptions; /** - * Use this to customize the built-in profile handler without overriding it. + * Use this to customize the default profile handler without overriding it. * You can still override the handler if needed. * * @example Pass an options object From 54f9a6bbb6aff2f4dff18b66f342df071f0032f5 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Thu, 6 Oct 2022 01:01:57 -0300 Subject: [PATCH 18/20] Update the migration guide --- V2_MIGRATION_GUIDE.md | 47 ++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/V2_MIGRATION_GUIDE.md b/V2_MIGRATION_GUIDE.md index 23ab0a099..81290b54b 100644 --- a/V2_MIGRATION_GUIDE.md +++ b/V2_MIGRATION_GUIDE.md @@ -9,7 +9,7 @@ Guide to migrating from `1.x` to `2.x` - [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) -- [Configure built-in handlers without overriding them](#configure-built-in-handlers-without-overriding-them) +- [Configure default handlers](#configure-default-handlers) ## `getSession` now returns a `Promise` @@ -203,20 +203,18 @@ const afterCallback = (req, res, session, state) => { }; // Redirects to `/admin` if user is admin ``` -## Configure built-in handlers without overriding them +## Configure default handlers -Previously it was not possible to dynamically configure the built-in handlers. For example, to pass a `connection` parameter to the login handler, you had to override it. +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({ - async login(req, res) { + login: async (req, res) => { try { await handleLogin(req, res, { - authorizationParams: { - connection: 'github' - }, + authorizationParams: { connection: 'github' }, }); } catch (error) { // ... @@ -227,16 +225,37 @@ export default handleAuth({ ### After -Now you can simply pass an options object to configure the built-in handler instead. +Now you can configure a default handler by passing an options object to it. ```js export default handleAuth({ - login: { - authorizationParams: { - connection: 'github' - } - } + 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' } + }) }); ``` -You can still override any built-in handler if needed. Pass either a custom handler function to override it, or just an options object to configure it. +It is still possible to override the default handlers if needed. From b02f9b4be2bf34a735c6a89920b16499eb0abb91 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Thu, 6 Oct 2022 10:56:20 +0100 Subject: [PATCH 19/20] Add type to custom handlers --- src/handlers/auth.ts | 52 +++++++++++++++++------------------------ tests/fixtures/setup.ts | 29 ++++++----------------- 2 files changed, 29 insertions(+), 52 deletions(-) diff --git a/src/handlers/auth.ts b/src/handlers/auth.ts index 1c1b5ed38..f9949bc06 100644 --- a/src/handlers/auth.ts +++ b/src/handlers/auth.ts @@ -1,8 +1,8 @@ import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; -import { HandleLogin, LoginHandler } from './login'; -import { HandleLogout, LogoutHandler } from './logout'; -import { CallbackHandler, HandleCallback } from './callback'; -import { HandleProfile, ProfileHandler } from './profile'; +import { HandleLogin } from './login'; +import { HandleLogout } from './logout'; +import { HandleCallback } from './callback'; +import { HandleProfile } from './profile'; import { HandlerError } from '../utils/errors'; /** @@ -58,14 +58,15 @@ import { HandlerError } from '../utils/errors'; * * @category Server */ -export interface Handlers { - login: LoginHandler; - logout: LogoutHandler; - callback: CallbackHandler; - profile: ProfileHandler; - onError: OnError; - [key: string]: any; -} +export type Handlers = ApiHandlers | ErrorHandlers; + +type ApiHandlers = { + [key: string]: NextApiHandler; +}; + +type ErrorHandlers = { + onError?: OnError; +}; /** * The main way to use the server SDK. @@ -89,7 +90,7 @@ export interface Handlers { * * @category Server */ -export type HandleAuth = (userHandlers?: Partial) => NextApiHandler; +export type HandleAuth = (userHandlers?: Handlers) => NextApiHandler; export type OnError = (req: NextApiRequest, res: NextApiResponse, error: HandlerError) => Promise | void; @@ -115,12 +116,12 @@ export default function handlerFactory({ handleCallback: HandleCallback; handleProfile: HandleProfile; }): HandleAuth { - return ({ onError, ...handlers }: Partial = {}): NextApiHandler => { - const { login, logout, callback, profile, ...customHandlers }: Omit = { + return ({ onError, ...handlers }: Handlers = {}): NextApiHandler => { + const customHandlers: ApiHandlers = { login: handleLogin, logout: handleLogout, callback: handleCallback, - profile: handleProfile, + me: (handlers as ApiHandlers).profile || handleProfile, ...handlers }; return async (req, res): Promise => { @@ -131,20 +132,11 @@ 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: - if (route && customHandlers[route]) { - return await customHandlers[route](req, res); - } - 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 as HandlerError); diff --git a/tests/fixtures/setup.ts b/tests/fixtures/setup.ts index 0c4831ffe..50b59c116 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,10 +20,7 @@ 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 } from '../../src/handlers/login'; -import { HandleLogout } from '../../src/handlers/logout'; -import { HandleCallback } from '../../src/handlers/callback'; -import { HandleProfile } from '../../src/handlers/profile'; +import { HandleLogin, HandleLogout, HandleCallback, HandleProfile } from '../../src'; export type SetupOptions = { idTokenClaims?: Claims; @@ -86,23 +83,11 @@ export const setup = async ( withApiAuthRequired, withPageAuthRequired } = initAuth0(config); - const callback = callbackHandler || handleCallback; - const login = loginHandler || handleLogin; - const logout = logoutHandler || handleLogout; - const profile = profileHandler || handleProfile; - const handlers: Partial = { onError, callback, login, logout, profile }; - if (callbackOptions) { - handlers.callback = (req, res) => callback(req, res, callbackOptions); - } - if (loginOptions) { - handlers.login = (req, res) => login(req, res, loginOptions); - } - if (logoutOptions) { - handlers.logout = (req, res) => logout(req, res, logoutOptions); - } - if (profileOptions) { - handlers.profile = (req, res) => profile(req, res, profileOptions); - } + 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; From 13a3a0cfa8260d198ca06ac51b78051c964d46ee Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Thu, 6 Oct 2022 15:45:03 +0100 Subject: [PATCH 20/20] Update examples --- EXAMPLES.md | 87 +++++++++++++++++++---------------------------------- 1 file changed, 31 insertions(+), 56 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index ca4492822..e02114134 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 -Sign up +Sign up ```