Repository: auth0/nextjs-auth0
Pull request #151 (2020-08-04 10:11:46)

Allow setting insecure cookie 
### Description

Currently, the cookie set is always Secure unless running on localhost or when `NODE_ENV` is not `production`.
When deploying code to a non-https site that has `NODE_ENV=production`, setting the cookie will fail.
Our use-case is deploying PRs to a dynamically created environment, where we test that everything is working as in production (thus, we have `NODE_ENV=production`), but the environment is a random url, and so cannot have https (at least easily).

The proposed solution is to allow setting an environment variable that turns off setting the cookie as secure, `AUTH0_ALLOW_INSECURE_COOKIE=true`

### Testing

- [x] This change adds test coverage for new/changed/fixed functionality

### Checklist

- [ ] 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
- [ ] The correct base branch is being used, if not `master`



Patch:
diff --git a/README.md b/README.md
index 6479ddcb..8fa22466 100644
--- a/README.md
+++ b/README.md
@@ -276,7 +276,7 @@ Profile.getInitialProps = async ({ req, res }) => {
 The user's access token can be stored in the user's session. However, this token will expire after some amount of time. It is possible to use a refresh token to retrieve a new access token. A token cache is available that will handle all of the checks and necessary refresh logic for you:
 
 ```js
-const tokenCache = await auth0.tokenCache(req, res);
+const tokenCache = auth0.tokenCache(req, res);
 const { accessToken } = await tokenCache.getAccessToken();
 ```
 
@@ -289,7 +289,7 @@ If for some reason it's not possible to get a new access token an error will be
 You can also require a scope to be present in the requested access token. If you need to call an endpoint which requires the `delete:file` scope you can require this as such:
 
 ```js
-const tokenCache = await auth0.tokenCache(req, res);
+const tokenCache = auth0.tokenCache(req, res);
 const { accessToken } = await tokenCache.getAccessToken({
   scope: [`delete:file`]
 });
@@ -304,6 +304,17 @@ For this functionality to work correctly you'll need to persist the access token
   storeRefreshToken: true
 ```
 
+The token cache will serve a token from the cache for as long as this token is valid. If you want to force the library to retrieve a new `access_token` you can provide the `refresh` option as follows:
+
+```js
+const { accessToken } = await tokenCache.getAccessToken({
+  scope: [`delete:file`],
+  refresh: true
+});
+```
+
+This is useful in cases when the user profile changed and you want the `access_token` to contain the updated claims.
+
 ### Calling an API
 
 It's a common pattern to use Next.js API Routes and proxy them to external APIs. When doing so these APIs typically require an `access_token` to be provided. These APIs can then be configured in Auth0.
@@ -314,7 +325,7 @@ In order to get an access_token for an API you'll need to configure the `audienc
 import { initAuth0 } from '@auth0/nextjs-auth0';
 
 export default initAuth0({
-  domain: '<AUTH0_DOMAIN>'
+  domain: '<AUTH0_DOMAIN>',
   clientId: '<AUTH0_CLIENT_ID>',
   clientSecret: '<AUTH0_CLIENT_SECRET>',
   audience: 'https://api.mycompany.com/',
@@ -337,7 +348,7 @@ import auth0 from '../../utils/auth0';
 
 export default async function getCustomers(req, res) {
   try {
-    const tokenCache = await auth0.tokenCache(req, res);
+    const tokenCache = auth0.tokenCache(req, res);
     const { accessToken } = await tokenCache.getAccessToken();
 
     const apiClient = new MyApiClient(accessToken);
diff --git a/examples/basic-example/package-lock.json b/examples/basic-example/package-lock.json
index e15a2e0d..3464109a 100644
--- a/examples/basic-example/package-lock.json
+++ b/examples/basic-example/package-lock.json
@@ -2833,9 +2833,9 @@
       "integrity": "sha512-QKMcpfA/fCOnqFHsZvKr2haQQb3eXkDI17zT+4hHxJJThyN5nShcG6q1VR8vRiE/2GCJM+0p3PzinYknkdsBYg=="
     },
     "elliptic": {
-      "version": "6.5.2",
-      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
-      "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
+      "version": "6.5.3",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
+      "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
       "requires": {
         "bn.js": "^4.4.0",
         "brorand": "^1.0.1",
@@ -2847,9 +2847,9 @@
       },
       "dependencies": {
         "bn.js": {
-          "version": "4.11.8",
-          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
-          "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+          "version": "4.11.9",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+          "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
         }
       }
     },
diff --git a/package-lock.json b/package-lock.json
index 9cf63471..e5dc3e16 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4264,9 +4264,9 @@
       "dev": true
     },
     "elliptic": {
-      "version": "6.5.2",
-      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
-      "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
+      "version": "6.5.3",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
+      "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
       "dev": true,
       "requires": {
         "bn.js": "^4.4.0",
@@ -4279,9 +4279,9 @@
       },
       "dependencies": {
         "bn.js": {
-          "version": "4.11.8",
-          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
-          "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+          "version": "4.11.9",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
+          "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
           "dev": true
         }
       }
diff --git a/src/handlers/session.ts b/src/handlers/session.ts
index 4e8da6f2..4d4ae688 100644
--- a/src/handlers/session.ts
+++ b/src/handlers/session.ts
@@ -1,10 +1,10 @@
-import { NextApiRequest } from 'next';
+import { IncomingMessage } from 'http';
 
 import { ISession } from '../session/session';
 import { ISessionStore } from '../session/store';
 
 export default function sessionHandler(sessionStore: ISessionStore) {
-  return (req: NextApiRequest): Promise<ISession | null | undefined> => {
+  return (req: IncomingMessage): Promise<ISession | null | undefined> => {
     if (!req) {
       throw new Error('Request is not available');
     }
diff --git a/src/instance.ts b/src/instance.ts
index 05715bfc..fbdb34a4 100644
--- a/src/instance.ts
+++ b/src/instance.ts
@@ -1,5 +1,5 @@
 import { NextApiRequest, NextApiResponse } from 'next';
-
+import { IncomingMessage } from 'http';
 import { ISession } from './session/session';
 import { LoginOptions } from './handlers/login';
 import { ITokenCache } from './tokens/token-cache';
@@ -32,7 +32,7 @@ export interface ISignInWithAuth0 {
   /**
    * Session handler which returns the current session
    */
-  getSession: (req: NextApiRequest) => Promise<ISession | null | undefined>;
+  getSession: (req: IncomingMessage) => Promise<ISession | null | undefined>;
 
   /**
    * Handle to require authentication for an API route.
diff --git a/src/tokens/session-token-cache.ts b/src/tokens/session-token-cache.ts
index 48740082..50afce58 100644
--- a/src/tokens/session-token-cache.ts
+++ b/src/tokens/session-token-cache.ts
@@ -73,7 +73,10 @@ export default class SessionTokenCache implements ITokenCache {
     // Check if the token has expired.
     // There is an edge case where we might have some clock skew where our code assumes the token is still valid.
     // Adding a skew of 1 minute to compensate.
-    if (session.refreshToken && session.accessTokenExpiresAt * 1000 - 60000 < Date.now()) {
+    if (
+      (session.refreshToken && session.accessTokenExpiresAt * 1000 - 60000 < Date.now()) ||
+      (session.refreshToken && accessTokenRequest && accessTokenRequest.refresh)
+    ) {
       const client = await this.clientProvider();
       const tokenSet = await client.refresh(session.refreshToken);
 
diff --git a/src/tokens/token-cache.ts b/src/tokens/token-cache.ts
index d9044839..304c2f8f 100644
--- a/src/tokens/token-cache.ts
+++ b/src/tokens/token-cache.ts
@@ -1,5 +1,6 @@
 export interface AccessTokenRequest {
   scopes?: Array<string>;
+  refresh?: boolean;
 }
 
 export interface AccessTokenResponse {
diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts
index 73e70eb7..d2b1bb9b 100644
--- a/src/utils/cookies.ts
+++ b/src/utils/cookies.ts
@@ -67,6 +67,10 @@ function isSecureEnvironment(req: IncomingMessage): boolean {
     return false;
   }
 
+  if (/^true$/i.test(process.env.AUTH0_ALLOW_INSECURE_COOKIE as string)) {
+    return false;
+  }
+
   const host = (req.headers.host.indexOf(':') > -1 && req.headers.host.split(':')[0]) || req.headers.host;
   if (['localhost', '127.0.0.1'].indexOf(host) > -1) {
     return false;


Test patch:
diff --git a/tests/tokens/session-token-cache.test.ts b/tests/tokens/session-token-cache.test.ts
index d3212574..4ae01676 100644
--- a/tests/tokens/session-token-cache.test.ts
+++ b/tests/tokens/session-token-cache.test.ts
@@ -178,6 +178,48 @@ describe('SessionTokenCache', () => {
           }
         });
       });
+    
+      test('should retrieve a new access token if force refresh is set', async () => {
+        expect.assertions(2);
+
+        const { cache, getSession } = getTokenCache({
+          expiresIn: 6500,
+          accessToken: 'ey123',
+          refreshToken: 'abc'
+        });
+
+        discovery(withApi);
+        jwksEndpoint(withApi, keystore.toJWKS());
+        refreshTokenExchange(
+          withApi,
+          'abc',
+          keystore.get(),
+          {
+            email: 'john@test.com',
+            name: 'john doe',
+            sub: '123'
+          },
+          'new-token'
+        );
+
+        const result = await cache.getAccessToken({refresh:true});
+        expect(result.accessToken).toMatch('new-token');
+
+        const session = await getSession();
+        expect(session).toStrictEqual({
+          accessToken: 'new-token',
+          refreshToken: 'abc',
+          idToken: expect.any(String),
+          accessTokenExpiresAt: expect.any(Number),
+          accessTokenScope: 'read:foo write:foo',
+          createdAt: expect.any(Number),
+          user: {
+            email: 'john@test.com',
+            name: 'john doe',
+            sub: '123'
+          }
+        });
+      });
     });
   });
 });
diff --git a/tests/utils/cookies.test.ts b/tests/utils/cookies.test.ts
index 402d7103..19e81ddc 100644
--- a/tests/utils/cookies.test.ts
+++ b/tests/utils/cookies.test.ts
@@ -120,24 +120,72 @@ describe('cookies', () => {
           ['Set-Cookie', [`foo=bar; Max-Age=1000; Path=/; Expires=${timeString}; HttpOnly`]]
         ]);
       });
+    });
+
+    describe('when not running on localhost', () => {
+      test('should set a secure cookie on the response', async () => {
+        const { req, res } = getRequestResponse();
+        req.headers.host = 'www.acme.com';
+
+        setCookie(req, res, {
+          name: 'foo',
+          value: 'bar',
+          maxAge: 1000,
+          path: '/'
+        });
+
+        expect(res.setHeader.mock.calls).toEqual([
+          ['Set-Cookie', [`foo=bar; Max-Age=1000; Path=/; Expires=${timeString}; HttpOnly; Secure`]]
+        ]);
+      });
+    });
+
+    describe("when AUTH0_ALLOW_INSECURE_COOKIE is 'true'", () => {
+      const initialEnvVar = process.env.AUTH0_ALLOW_INSECURE_COOKIE;
+      beforeEach(() => {
+        process.env.AUTH0_ALLOW_INSECURE_COOKIE = 'true';
+      });
+
+      afterEach(() => {
+        process.env.AUTH0_ALLOW_INSECURE_COOKIE = initialEnvVar;
+      });
+
+      test('should not set a secure cookie on the response', async () => {
+        const { req, res } = getRequestResponse();
+        req.headers.host = 'www.acme.com';
 
-      describe('when not running on localhost', () => {
-        test('should set a secure cookie on the response', async () => {
-          const { req, res } = getRequestResponse();
-          req.headers.host = 'www.acme.com';
-
-          setCookie(req, res, {
-            name: 'foo',
-            value: 'bar',
-            maxAge: 1000,
-            path: '/'
-          });
-
-          expect(res.setHeader.mock.calls).toEqual([
-            ['Set-Cookie', [`foo=bar; Max-Age=1000; Path=/; Expires=${timeString}; HttpOnly; Secure`]]
-          ]);
+        setCookie(req, res, {
+          name: 'foo',
+          value: 'bar',
+          maxAge: 1000,
+          path: '/'
         });
+
+        expect(res.setHeader.mock.calls).toEqual([
+          ['Set-Cookie', [`foo=bar; Max-Age=1000; Path=/; Expires=${timeString}; HttpOnly`]]
+        ]);
+      });
+    });
+  });
+
+  describe('when not running in production', () => {
+    beforeEach(() => {
+      process.env.NODE_ENV = 'development';
+    });
+    test('should not set a secure cookie on the response', async () => {
+      const { req, res } = getRequestResponse();
+      req.headers.host = 'www.acme.com';
+
+      setCookie(req, res, {
+        name: 'foo',
+        value: 'bar',
+        maxAge: 1000,
+        path: '/'
       });
+
+      expect(res.setHeader.mock.calls).toEqual([
+        ['Set-Cookie', [`foo=bar; Max-Age=1000; Path=/; Expires=${timeString}; HttpOnly`]]
+      ]);
     });
   });
 });