Repository: auth0/nextjs-auth0
Pull request #147 (2020-07-21 11:38:19)

Allow force refreshing of the access token
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

> The purpose of this pr is to allow the access token to be refreshed manually
>
> Provide details that support your chosen implementation, including: breaking changes, alternatives considered, changes to the API, etc.
Changes are to AccessTokenRequest interface to allow the option refresh which is not required.
and inside of getAccessToken() where it would check if the token was expired and there is a refresh token it now also checks 
`(session.refreshToken && accessTokenRequest && accessTokenRequest.refresh`

How it can be used is `getAccessToken({refresh:true})`

### References

> Include any links supporting this change such as a:
>
> - https://github.com/auth0/nextjs-auth0/issues/136


### Testing

> Describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors.
> Added a new test for this outcome next with the same code with a change to the time that is used to test the expired access token 

- [ ] 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..694196c8 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`]
 });
@@ -337,7 +337,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 5162ddd2..3464109a 100644
--- a/examples/basic-example/package-lock.json
+++ b/examples/basic-example/package-lock.json
@@ -52,9 +52,9 @@
       }
     },
     "@auth0/nextjs-auth0": {
-      "version": "0.13.0",
-      "resolved": "https://registry.npmjs.org/@auth0/nextjs-auth0/-/nextjs-auth0-0.13.0.tgz",
-      "integrity": "sha512-1UZ/Fjm9mHCZtqMsHPfQex64n8Ili5/hWEd6xSauy44eM6+RqTz/Hy3DQCMtLGV+mh7TATNtz+TIDn63qMlv7A==",
+      "version": "0.15.0",
+      "resolved": "https://registry.npmjs.org/@auth0/nextjs-auth0/-/nextjs-auth0-0.15.0.tgz",
+      "integrity": "sha512-UN1Hsbpet6xMTTFl8kvyylyN8C8sFfR4M97IBY5mPdYq9ydhBckY/1L8LlG/Ie5P7fyoRQgf97XWxIzV8JH6MQ==",
       "requires": {
         "@hapi/iron": "^5.1.4",
         "base64url": "^3.0.1",
@@ -1208,9 +1208,9 @@
       }
     },
     "@types/node": {
-      "version": "14.0.1",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.1.tgz",
-      "integrity": "sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA=="
+      "version": "14.0.23",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.23.tgz",
+      "integrity": "sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw=="
     },
     "@types/q": {
       "version": "1.5.3",
@@ -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=="
         }
       }
     },
@@ -3933,9 +3933,9 @@
       }
     },
     "jose": {
-      "version": "1.27.0",
-      "resolved": "https://registry.npmjs.org/jose/-/jose-1.27.0.tgz",
-      "integrity": "sha512-SxYPCM9pWDaK070CXbxgL4ktVzLlE0yJxevDJtbWxv2WMQwYfpBZLYlG8PhChsiOfOXp6FrceRgTuZh1vZeDlg==",
+      "version": "1.27.2",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-1.27.2.tgz",
+      "integrity": "sha512-zLIwnMa8dh5A2jFo56KvhiXCaW0hFjdNvG0I5GScL8Wro+/r/SnyIYTbnX3fYztPNSfgQp56sDMHUuS9c3e6bw==",
       "requires": {
         "@panva/asn1.js": "^1.0.0"
       }
@@ -4033,9 +4033,9 @@
       }
     },
     "lodash": {
-      "version": "4.17.15",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+      "version": "4.17.19",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
+      "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
     },
     "lodash.memoize": {
       "version": "4.1.2",
@@ -4671,20 +4671,34 @@
       }
     },
     "openid-client": {
-      "version": "3.15.1",
-      "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-3.15.1.tgz",
-      "integrity": "sha512-USoxzLuL08IhRiA+4z5FW25nsLgBM6lOoh+U/XWqyKJzrMbjfmVWNfof7706RgMypyvAFcAPCxPtSFqb+GpHjA==",
+      "version": "3.15.8",
+      "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-3.15.8.tgz",
+      "integrity": "sha512-3GZWg9vO1vrdz60anD1LTuUyyhwpA3eqwf3Ax1Gj3AjPl+cG8aD/6Y4eyDpOlQN7iWQcGLsqMU2xBxlH4r+xEQ==",
       "requires": {
         "@types/got": "^9.6.9",
         "base64url": "^3.0.1",
         "got": "^9.6.0",
-        "jose": "^1.25.2",
-        "lodash": "^4.17.15",
-        "lru-cache": "^5.1.1",
+        "jose": "^1.27.1",
+        "lru-cache": "^6.0.0",
         "make-error": "^1.3.6",
         "object-hash": "^2.0.1",
         "oidc-token-hash": "^5.0.0",
         "p-any": "^3.0.0"
+      },
+      "dependencies": {
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "yallist": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+          "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+        }
       }
     },
     "os-browserify": {
diff --git a/examples/sample-api/package-lock.json b/examples/sample-api/package-lock.json
index 64379411..c861df59 100644
--- a/examples/sample-api/package-lock.json
+++ b/examples/sample-api/package-lock.json
@@ -596,9 +596,9 @@
       "integrity": "sha1-/sfervF+fDoKVeHaBCgD4l2RdF0="
     },
     "lodash": {
-      "version": "4.17.15",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+      "version": "4.17.19",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
+      "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
     },
     "lodash.includes": {
       "version": "4.3.0",
diff --git a/package-lock.json b/package-lock.json
index d6135fe7..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
         }
       }
@@ -7267,9 +7267,9 @@
       }
     },
     "lodash": {
-      "version": "4.17.15",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+      "version": "4.17.19",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
+      "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
     },
     "lodash.memoize": {
       "version": "4.1.2",
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 {


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'
+          }
+        });
+      });
     });
   });
 });