diff --git a/README.md b/README.md index 7a5e42249..7f22333eb 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ export default async function login(req, res) { } ``` -> Note: This route supports providing `redirectTo` in the querystring, eg: (`/api/login?redirectTo=/profile`). The user will automatically be redirect to this URL after signing in. +> Note: This route supports providing `redirectTo` in the query string, eg: (`/api/login?redirectTo=/profile`). The user will automatically be redirect to this URL after signing in. This will redirect the user to Auth0. After the transaction is completed Auth0 will redirect the user back to your application. This is why the callback route (`/pages/api/callback.js`) needs to be created which will create a session cookie: @@ -204,6 +204,8 @@ export default async function logout(req, res) { } ``` +Note that the third parameter of `handleLogout` accepts an optional `returnTo` to allow request-time configuration of where to redirect the user to on logout. + ### User Profile If you want to expose a route which returns the user profile to the client you can create an additional route (eg: `/pages/api/me.js`): diff --git a/src/handlers/logout.ts b/src/handlers/logout.ts index 6f1328a34..561cec220 100644 --- a/src/handlers/logout.ts +++ b/src/handlers/logout.ts @@ -6,12 +6,12 @@ import { IOidcClientFactory } from '../utils/oidc-client'; import CookieSessionStoreSettings from '../session/cookie-store/settings'; import { ISessionStore } from '../session/store'; -function createLogoutUrl(settings: IAuth0Settings): string { - return ( - `https://${settings.domain}/v2/logout?` + - `client_id=${settings.clientId}` + - `&returnTo=${encodeURIComponent(settings.postLogoutRedirectUri)}` - ); +export interface LogoutOptions { + returnTo?: string; +} + +function createLogoutUrl(settings: IAuth0Settings, returnToUrl: string): string { + return `https://${settings.domain}/v2/logout?client_id=${settings.clientId}&returnTo=${returnToUrl}`; } export default function logoutHandler( @@ -20,7 +20,7 @@ export default function logoutHandler( clientProvider: IOidcClientFactory, store: ISessionStore ) { - return async (req: NextApiRequest, res: NextApiResponse): Promise => { + return async (req: NextApiRequest, res: NextApiResponse, options?: LogoutOptions): Promise => { if (!req) { throw new Error('Request is not available'); } @@ -31,17 +31,18 @@ export default function logoutHandler( const session = await store.read(req); let endSessionUrl; + const returnToUrl = encodeURIComponent(options?.returnTo || settings.postLogoutRedirectUri); try { const client = await clientProvider(); endSessionUrl = client.endSessionUrl({ id_token_hint: session ? session.idToken : undefined, - post_logout_redirect_uri: encodeURIComponent(settings.postLogoutRedirectUri) + post_logout_redirect_uri: returnToUrl }); } catch (err) { if (/end_session_endpoint must be configured/.exec(err)) { // Use default url if end_session_endpoint is not configured - endSessionUrl = createLogoutUrl(settings); + endSessionUrl = createLogoutUrl(settings, returnToUrl); } else { throw err; } diff --git a/src/instance.ts b/src/instance.ts index d4380a217..05715bfce 100644 --- a/src/instance.ts +++ b/src/instance.ts @@ -5,6 +5,7 @@ import { LoginOptions } from './handlers/login'; import { ITokenCache } from './tokens/token-cache'; import { CallbackOptions } from './handlers/callback'; import { ProfileOptions } from './handlers/profile'; +import { LogoutOptions } from './handlers/logout'; import { IApiRoute } from './handlers/require-authentication'; export interface ISignInWithAuth0 { @@ -19,9 +20,9 @@ export interface ISignInWithAuth0 { handleCallback: (req: NextApiRequest, res: NextApiResponse, options?: CallbackOptions) => Promise; /** - * Logout handler which will clear the local session and the Auth0 session + * Logout handler which will clear the local session and the Auth0 session. */ - handleLogout: (req: NextApiRequest, res: NextApiResponse) => Promise; + handleLogout: (req: NextApiRequest, res: NextApiResponse, options?: LogoutOptions) => Promise; /** * Profile handler which return profile information about the user. diff --git a/tests/handlers/logout.test.ts b/tests/handlers/logout.test.ts index 16aa4e1e0..cbb4155fc 100644 --- a/tests/handlers/logout.test.ts +++ b/tests/handlers/logout.test.ts @@ -3,7 +3,7 @@ import { parse } from 'cookie'; import { promisify } from 'util'; import HttpServer from '../helpers/server'; -import logout from '../../src/handlers/logout'; +import logout, { LogoutOptions } from '../../src/handlers/logout'; import { withoutApi } from '../helpers/default-settings'; import CookieSessionStoreSettings from '../../src/session/cookie-store/settings'; import { discovery } from '../helpers/oidc-nocks'; @@ -34,11 +34,18 @@ const sessionStore: ISessionStore = { describe('logout handler', () => { let httpServer: HttpServer; + let logoutHandler: any; + let logoutOptions: LogoutOptions | null; beforeEach((done) => { - httpServer = new HttpServer( - logout(withoutApi, new CookieSessionStoreSettings(withoutApi.session), getClient(withoutApi), sessionStore) + logoutHandler = logout( + withoutApi, + new CookieSessionStoreSettings(withoutApi.session), + getClient(withoutApi), + sessionStore ); + logoutOptions = {}; + httpServer = new HttpServer((req, res) => logoutHandler(req, res, logoutOptions)); httpServer.start(done); }); @@ -62,6 +69,26 @@ describe('logout handler', () => { ); }); + test('should return to the custom path', async () => { + const customReturnTo = 'https://www.foo.bar'; + logoutOptions = { + returnTo: customReturnTo + }; + discovery(withoutApi); + + const { statusCode, headers } = await getAsync({ + url: httpServer.getUrl(), + followRedirect: false + }); + + expect(statusCode).toBe(302); + expect(headers.location).toBe( + `https://${withoutApi.domain}/v2/logout?client_id=${withoutApi.clientId}&returnTo=${encodeURIComponent( + customReturnTo + )}` + ); + }); + test('should use end_session_endpoint if available', async () => { discovery(withoutApi, { end_session_endpoint: 'https://my-end-session-endpoint/logout' });