Repository: auth0/nextjs-auth0
Pull request #79 (2020-02-07 15:57:37)

Add domain when clearing cookie
By submitting a PR to this repository, you agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). Please see the [contributing guidelines](https://github.com/auth0/open-source-template/blob/master/CONTRIBUTING.md) for how to create and submit a high-quality PR for this repo.

### Description

I ran into a problem when setting the `cookieDomain` option in the session config. The cookie is set correctly when logging in, but because the domain is missing when clearing the cookie in the logout handler it seems that the cookie is not cleared and I remain logged in. Although if I try logging in again Auth0 presents me with the login page.

It looks like an easy fix to add the `cookieDomain` when clearing the cookie, but I'm not sure if this is the right approach.

### Testing

- [x] This change adds test coverage for new/changed/fixed functionality

### Checklist

- [x] I have added documentation for new/changed functionality in this PR or in auth0.com/docs
- [ ] All active GitHub checks for tests, formatting, and security are passing
- [x] The correct base branch is being used, if not `master`



Patch:
diff --git a/README.md b/README.md
index 3dd180b2..49face77 100644
--- a/README.md
+++ b/README.md
@@ -135,6 +135,7 @@ You can optionally send extra parameters to Auth0 to influence the transaction,
 - Filling in the user's email address
 - Exposing information to the custom login page (eg: to show the signup tab)
 - Using a custom `state`
+- Redirecting the user to a `redirectTo` url after the transaction is finished
 
 ```js
 import auth0 from '../../utils/auth0';
@@ -148,7 +149,8 @@ export default async function login(req, res) {
         scope: 'some other scope',
         state: 'a custom state',
         foo: 'bar'
-      }
+      },
+      redirectTo: '/custom-url'
     });
   } catch (error) {
     console.error(error);
diff --git a/package-lock.json b/package-lock.json
index d6d8284e..fdd0c9dc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1736,9 +1736,9 @@
       }
     },
     "acorn": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz",
-      "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
+      "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
       "dev": true
     },
     "acorn-globals": {
@@ -1752,9 +1752,9 @@
       },
       "dependencies": {
         "acorn": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz",
-          "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==",
+          "version": "6.4.1",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
+          "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
           "dev": true
         }
       }
@@ -7074,9 +7074,9 @@
       },
       "dependencies": {
         "acorn": {
-          "version": "5.7.3",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
-          "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
+          "version": "5.7.4",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz",
+          "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==",
           "dev": true
         }
       }
@@ -11412,9 +11412,9 @@
       },
       "dependencies": {
         "acorn": {
-          "version": "6.3.0",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz",
-          "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==",
+          "version": "6.4.1",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
+          "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
           "dev": true
         },
         "eslint-scope": {
diff --git a/src/handlers/callback.ts b/src/handlers/callback.ts
index de166b99..900ec156 100644
--- a/src/handlers/callback.ts
+++ b/src/handlers/callback.ts
@@ -46,8 +46,8 @@ export default function callbackHandler(
     // Create the session.
     await sessionStore.save(req, res, session);
 
-    // Redirect to the homepage.
-    const redirectTo = (options && options.redirectTo) || '/';
+    // Redirect to the homepage or custom url.
+    const redirectTo = (options && options.redirectTo) || cookies['a0:redirectTo'] || '/';
     res.writeHead(302, {
       Location: redirectTo
     });
diff --git a/src/handlers/login.ts b/src/handlers/login.ts
index ee89eae2..7cc0d096 100644
--- a/src/handlers/login.ts
+++ b/src/handlers/login.ts
@@ -37,6 +37,7 @@ export interface AuthorizationParameters {
 
 export interface LoginOptions {
   authParams?: AuthorizationParameters;
+  redirectTo?: string;
 }
 
 export default function loginHandler(settings: IAuth0Settings, clientProvider: IOidcClientFactory) {
@@ -51,6 +52,7 @@ export default function loginHandler(settings: IAuth0Settings, clientProvider: I
 
     const opt = options || {};
     const { state = base64url(randomBytes(48)), ...authParams } = (opt && opt.authParams) || {};
+    const { redirectTo } = opt;
 
     // Create the authorization url.
     const client = await clientProvider();
@@ -70,6 +72,11 @@ export default function loginHandler(settings: IAuth0Settings, clientProvider: I
         name: 'a0:state',
         value: state,
         maxAge: 60 * 60
+      },
+      {
+        name: 'a0:redirectTo',
+        value: redirectTo || '/',
+        maxAge: 60 * 60
       }
     ]);
 
diff --git a/src/handlers/logout.ts b/src/handlers/logout.ts
index 1a12a3b3..4e2c8c6c 100644
--- a/src/handlers/logout.ts
+++ b/src/handlers/logout.ts
@@ -33,7 +33,8 @@ export default function logoutHandler(settings: IAuth0Settings, sessionSettings:
         name: sessionSettings.cookieName,
         value: '',
         maxAge: -1,
-        path: sessionSettings.cookiePath
+        path: sessionSettings.cookiePath,
+        domain: sessionSettings.cookieDomain
       }
     ]);
 


Test patch:
diff --git a/tests/handlers/callback.test.ts b/tests/handlers/callback.test.ts
index 5acdc0f2..5b4f9d88 100644
--- a/tests/handlers/callback.test.ts
+++ b/tests/handlers/callback.test.ts
@@ -220,7 +220,7 @@ describe('callback handler', () => {
           url: `${httpServer.getUrl()}?state=foo&code=bar`,
           followRedirect: false,
           headers: {
-            cookie: 'a0:state=foo;'
+            cookie: 'a0:state=foo;a0:redirectTo=/custom-url;'
           }
         });
 
@@ -260,6 +260,11 @@ describe('callback handler', () => {
         expect(cookie['Max-Age']).toBe('3600');
         expect(cookie.Expires).toBe(new Date(time.getTime() + 3600 * 1000).toUTCString());
       });
+
+      test('should redirect to cookie url', async () => {
+        expect(responseStatus).toBe(302);
+        expect(responseHeaders.location).toBe('/custom-url');
+      });
     });
   });
 
diff --git a/tests/handlers/login.test.ts b/tests/handlers/login.test.ts
index 2515eb64..59229d39 100644
--- a/tests/handlers/login.test.ts
+++ b/tests/handlers/login.test.ts
@@ -17,7 +17,7 @@ describe('login handler', () => {
 
   beforeEach(done => {
     discovery(withoutApi);
-    loginOptions = null;
+    loginOptions = { redirectTo: '/custom-url' };
     loginHandler = login(withoutApi, getClient(withoutApi));
     httpServer = new HttpServer((req, res) => loginHandler(req, res, loginOptions));
     httpServer.start(done);
@@ -37,6 +37,18 @@ describe('login handler', () => {
     expect(state).toBeTruthy();
   });
 
+  test('should create a redirectTo cookie', async () => {
+    const { headers } = await getAsync({
+      url: httpServer.getUrl(),
+      followRedirect: false
+    });
+
+    const state = parse(headers['set-cookie'][0]);
+    const redirectTo = parse(headers['set-cookie'][1]);
+    expect(state).toBeTruthy();
+    expect(redirectTo['a0:redirectTo']).toEqual('/custom-url');
+  });
+
   test('should redirect to the identity provider', async () => {
     const { statusCode, headers } = await getAsync({
       url: httpServer.getUrl(),
diff --git a/tests/handlers/logout.test.ts b/tests/handlers/logout.test.ts
index c2ca8c2b..5852d082 100644
--- a/tests/handlers/logout.test.ts
+++ b/tests/handlers/logout.test.ts
@@ -53,3 +53,40 @@ describe('logout handler', () => {
     });
   });
 });
+
+describe('logout handler with cookieDomain', () => {
+  const cookieDomain = 'www.acme.com';
+  let httpServer: HttpServer;
+
+  beforeAll(done => {
+    httpServer = new HttpServer(
+      logout(withoutApi, new CookieSessionStoreSettings({ ...withoutApi.session, cookieDomain }))
+    );
+    httpServer.start(done);
+  });
+
+  afterAll(done => {
+    httpServer.stop(done);
+  });
+
+  test('should delete the state and session', async () => {
+    const { headers } = await getAsync({
+      url: httpServer.getUrl(),
+      headers: {
+        cookie: ['a0:state=foo', 'a0:session=bar'].join('; ')
+      },
+      followRedirect: false
+    });
+
+    const [stateCookie, sessionCookie] = headers['set-cookie'];
+    expect(parse(stateCookie)).toMatchObject({
+      'a0:state': '',
+      'Max-Age': '-1'
+    });
+    expect(parse(sessionCookie)).toMatchObject({
+      'a0:session': '',
+      'Max-Age': '-1',
+      Domain: cookieDomain
+    });
+  });
+});