From 462aeaa7dc4a6e89546b1851c1d1903b4e1d85bc Mon Sep 17 00:00:00 2001 From: zia grosvenor Date: Tue, 1 Dec 2020 16:58:28 +0000 Subject: [PATCH 1/2] Add set session handler --- src/handlers/index.ts | 2 + src/handlers/set-session.ts | 22 +++++++++ src/instance.browser.ts | 3 ++ src/instance.node.ts | 1 + src/instance.ts | 6 +++ tests/handlers/set-session.test.ts | 79 ++++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+) create mode 100644 src/handlers/set-session.ts create mode 100644 tests/handlers/set-session.test.ts diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 0d17cd566..3a7577a61 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -3,6 +3,7 @@ import LogoutHandler from './logout'; import CallbackHandler from './callback'; import ProfileHandler from './profile'; import SessionHandler from './session'; +import SetSessionHandler from './set-session'; import RequireAuthentication from './require-authentication'; import TokenCache from './token-cache'; @@ -12,6 +13,7 @@ export default { LogoutHandler, ProfileHandler, SessionHandler, + SetSessionHandler, RequireAuthentication, TokenCache }; diff --git a/src/handlers/set-session.ts b/src/handlers/set-session.ts new file mode 100644 index 000000000..9fd30e132 --- /dev/null +++ b/src/handlers/set-session.ts @@ -0,0 +1,22 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { TokenSetParameters, TokenSet } from 'openid-client'; +import { ISessionStore } from '../session/store'; +import getSessionFromTokenSet from '../utils/session'; + +export default function setSessionHandler(sessionStore: ISessionStore) { + return async (req: NextApiRequest, res: NextApiResponse, tokenSetParameters: TokenSetParameters): Promise => { + if (!res) { + throw new Error('Response is not available'); + } + + if (!req) { + throw new Error('Request is not available'); + } + + // Get the claims without any OIDC specific claim. + const session = getSessionFromTokenSet(new TokenSet(tokenSetParameters)); + + // Create the session. + await sessionStore.save(req, res, session); + }; +} diff --git a/src/instance.browser.ts b/src/instance.browser.ts index a35e4900a..16b3db28a 100644 --- a/src/instance.browser.ts +++ b/src/instance.browser.ts @@ -20,6 +20,9 @@ export default function createDummyBrowserInstance(): ISignInWithAuth0 & { isBro getSession: (): Promise => { throw new Error('The getSession method can only be used from the server side'); }, + setSession: (): Promise => { + throw new Error('The setSession method can only be used from the server side'); + }, requireAuthentication: () => (): Promise => { throw new Error('The requireAuthentication method can only be used from the server side'); }, diff --git a/src/instance.node.ts b/src/instance.node.ts index 1281c3590..361e4df6c 100644 --- a/src/instance.node.ts +++ b/src/instance.node.ts @@ -38,6 +38,7 @@ export default function createInstance(settings: IAuth0Settings): ISignInWithAut handleCallback: handlers.CallbackHandler(settings, clientProvider, store), handleProfile: handlers.ProfileHandler(store, clientProvider), getSession: handlers.SessionHandler(store), + setSession: handlers.SetSessionHandler(store), requireAuthentication: handlers.RequireAuthentication(store), tokenCache: handlers.TokenCache(clientProvider, store) }; diff --git a/src/instance.ts b/src/instance.ts index fbdb34a48..891c20a18 100644 --- a/src/instance.ts +++ b/src/instance.ts @@ -1,5 +1,6 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { IncomingMessage } from 'http'; +import { TokenSetParameters } from 'openid-client'; import { ISession } from './session/session'; import { LoginOptions } from './handlers/login'; import { ITokenCache } from './tokens/token-cache'; @@ -34,6 +35,11 @@ export interface ISignInWithAuth0 { */ getSession: (req: IncomingMessage) => Promise; + /** + * Set session handler which sets the current session with a token set. + */ + setSession: (req: NextApiRequest, res: NextApiResponse, tokenSetParameters: TokenSetParameters) => Promise; + /** * Handle to require authentication for an API route. */ diff --git a/tests/handlers/set-session.test.ts b/tests/handlers/set-session.test.ts new file mode 100644 index 000000000..1a39f8642 --- /dev/null +++ b/tests/handlers/set-session.test.ts @@ -0,0 +1,79 @@ +import base64url from 'base64url'; +import handlers from '../../src/handlers'; +import { ISessionStore } from '../../src/session/store'; +import getRequestResponse from '../helpers/http'; + +describe('set session handler', () => { + let store: ISessionStore; + + const tokenSetParams = { + access_token: 'my-access-token', + refresh_token: 'my-refresh-token', + id_token: `jwt-header.${base64url.encode( + JSON.stringify({ + email: 'foo@bar.com', + email_verified: false, + name: 'Foo Bar', + nickname: 'foobar', + picture: 'http://example.com/image', + sub: 'user-id', + updated_at: '2020-12-01T12:15:06.383Z' + }) + )}.my-verify-signature`, + scope: 'openid profile email offline_access', + expires_in: 2592000, + token_type: 'Bearer' + }; + + beforeEach(() => { + store = { + read: jest.fn().mockResolvedValue({}), + save: jest.fn().mockResolvedValue({}) + }; + }); + + test('should require a truthy request object', async () => { + const { res } = getRequestResponse(); + const sessionHandler = handlers.SetSessionHandler(store); + + await expect(sessionHandler(null as any, res, tokenSetParams)).rejects.toEqual( + new Error('Request is not available') + ); + }); + + test('should require a truthy response object', async () => { + const { req } = getRequestResponse(); + const sessionHandler = handlers.SetSessionHandler(store); + + await expect(sessionHandler(req, null as any, tokenSetParams)).rejects.toEqual( + new Error('Response is not available') + ); + }); + + test('should set the session', async () => { + const { req, res } = getRequestResponse(); + + const setSessionHandler = handlers.SetSessionHandler(store); + + await setSessionHandler(req, res, tokenSetParams); + + expect(store.save).toHaveBeenCalledWith(req, res, { + accessToken: 'my-access-token', + accessTokenExpiresAt: expect.any(Number), + accessTokenScope: 'openid profile email offline_access', + createdAt: expect.any(Number), + idToken: + 'jwt-header.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiRm9vIEJhciIsIm5pY2tuYW1lIjoiZm9vYmFyIiwicGljdHVyZSI6Imh0dHA6Ly9leGFtcGxlLmNvbS9pbWFnZSIsInN1YiI6InVzZXItaWQiLCJ1cGRhdGVkX2F0IjoiMjAyMC0xMi0wMVQxMjoxNTowNi4zODNaIn0.my-verify-signature', + refreshToken: 'my-refresh-token', + user: { + email: 'foo@bar.com', + email_verified: false, + name: 'Foo Bar', + nickname: 'foobar', + picture: 'http://example.com/image', + sub: 'user-id', + updated_at: '2020-12-01T12:15:06.383Z' + } + }); + }); +}); From ca4bd751bb49185aa8ad9d345dd159cdf60c251f Mon Sep 17 00:00:00 2001 From: zia grosvenor Date: Tue, 1 Dec 2020 16:58:36 +0000 Subject: [PATCH 2/2] Add documentation --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/README.md b/README.md index 7a8d54d0f..45dfa0bcb 100644 --- a/README.md +++ b/README.md @@ -425,6 +425,53 @@ If the user is authenticated then your API route will simply execute, but if the } ``` +### Setting the session with a token set + +In case you want to set session tokens programmatically you can use the `setSession` method. + +This is useful for example if you need interoperability between `@auth0/nextjs-auth0` and an existing legacy sign up endpoint which uses the password grant type for silent authentication. + +If you want to expose a route which creates a new user and then silently authenticates them (eg: `/pages/api/signup.js`): + +```js +// Importing the `auth0` package to use the password grant type +import { AuthenticationClient } from 'auth0'; +import auth0 from '../../utils/auth0'; +import { createUserInUpstreamService } from '../../utils/signup'; + +const auth0AuthClient = new AuthenticationClient({ + domain: process.env.NEXT_PUBLIC_AUTH0_DOMAIN, + clientId: process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID, + clientSecret: process.env.AUTH0_CLIENT_SECRET +}); + +export default async function signup(req, res) { + try { + // POST http://api.acme.com/signup will create + // the user in the ACME database and in Auth0 + await createUserInUpstreamService(req.body); + + // Once the user is created successfully then silently authenticate + // them using the password grant type + const tokens = await auth0AuthClient.oauth.passwordGrant({ + username: req.body.username, + password: req.body.password, + scope: 'openid profile email offline_access' + }); + + // Set the session with the tokens from the password grant + await auth0.setSession(req, res, tokens); + + res.status(201).end(); + } catch (error) { + console.error(error); + res.status(error.status || 500).end(error.message); + } +} +``` + +NOTE: For the above example to work you will need to enable the password grant type on your Auth0 client application settings dashboard. + ## Documentation ### Cookies