Repository: auth0/nextjs-auth0
Pull request #8 (2019-09-24 16:59:54)

Add support to store the refresh token
### Description

The goal of this PR is to store the `refresh_token` in the session, in addition to the `access_token` and `id_token`

### References

https://github.com/auth0/nextjs-auth0/issues/4
https://github.com/auth0/nextjs-auth0/issues/7



Patch:
diff --git a/README.md b/README.md
index 7b5a902c..898b91c9 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,9 @@ export default useAuth0({
     // Store the id_token in the session. Defaults to false.
     storeIdToken: false,
     // Store the access_token in the session. Defaults to false.
-    storeAccessToken: false
+    storeAccessToken: false,
+    // Store the refresh_token in the session. Defaults to false.
+    storeRefreshToken: false
   },
   httpClient: {
     // Optionally configure the timeout for the HTTP client.
diff --git a/src/handlers/callback.ts b/src/handlers/callback.ts
index 09e6a34b..bc692bcb 100644
--- a/src/handlers/callback.ts
+++ b/src/handlers/callback.ts
@@ -62,6 +62,7 @@ export default function callbackHandler(
       },
       idToken: tokenSet.id_token,
       accessToken: tokenSet.access_token,
+      refreshToken: tokenSet.refresh_token,
       createdAt: Date.now()
     };
 
diff --git a/src/session/cookie-store/index.ts b/src/session/cookie-store/index.ts
index 073b6d02..c1c03c78 100644
--- a/src/session/cookie-store/index.ts
+++ b/src/session/cookie-store/index.ts
@@ -41,7 +41,7 @@ export default class CookieSessionStore implements ISessionStore {
   async save(_: IncomingMessage, res: ServerResponse, session: ISession): Promise<void> {
     const { cookieSecret, cookieName, cookiePath, cookieLifetime } = this.settings;
 
-    const { idToken, accessToken, user, createdAt } = session;
+    const { idToken, accessToken, refreshToken, user, createdAt } = session;
     const persistedSession = new Session(user, createdAt);
 
     if (this.settings.storeIdToken && idToken) {
@@ -52,6 +52,10 @@ export default class CookieSessionStore implements ISessionStore {
       persistedSession.accessToken = accessToken;
     }
 
+    if (this.settings.storeRefreshToken && refreshToken) {
+      persistedSession.refreshToken = refreshToken;
+    }
+
     const encryptedSession = await Iron.seal(persistedSession, cookieSecret, Iron.defaults);
     setCookie(res, {
       name: cookieName,
diff --git a/src/session/cookie-store/settings.ts b/src/session/cookie-store/settings.ts
index b48449e6..1bc9513f 100644
--- a/src/session/cookie-store/settings.ts
+++ b/src/session/cookie-store/settings.ts
@@ -33,6 +33,12 @@ export interface ICookieSessionStoreSettings {
    * Defaults to 'false'
    */
   storeAccessToken?: boolean;
+
+  /**
+   * Save the refresh_token in the cookie.
+   * Defaults to 'false'
+   */
+  storeRefreshToken?: boolean;
 }
 
 export default class CookieSessionStoreSettings {
@@ -48,6 +54,8 @@ export default class CookieSessionStoreSettings {
 
   readonly storeAccessToken: boolean;
 
+  readonly storeRefreshToken: boolean;
+
   constructor(settings: ICookieSessionStoreSettings) {
     this.cookieSecret = settings.cookieSecret;
     if (!this.cookieSecret || !this.cookieSecret.length) {
@@ -72,5 +80,6 @@ export default class CookieSessionStoreSettings {
 
     this.storeIdToken = settings.storeIdToken || false;
     this.storeAccessToken = settings.storeAccessToken || false;
+    this.storeRefreshToken = settings.storeRefreshToken || false;
   }
 }
diff --git a/src/session/session.ts b/src/session/session.ts
index 01d1b998..95496039 100644
--- a/src/session/session.ts
+++ b/src/session/session.ts
@@ -14,6 +14,11 @@ export interface ISession {
    */
   readonly accessToken?: string | undefined;
 
+  /**
+   * The refresh token.
+   */
+  readonly refreshToken?: string | undefined;
+
   /**
    * The time on which the session was created.
    */
@@ -34,6 +39,8 @@ export default class Session implements ISession {
 
   accessToken?: string | undefined;
 
+  refreshToken?: string | undefined;
+
   createdAt: number;
 
   constructor(user: IClaims, createdAt?: number) {


Test patch:
diff --git a/tests/handlers/profile.test.ts b/tests/handlers/profile.test.ts
index aa457047..bc44f2e8 100644
--- a/tests/handlers/profile.test.ts
+++ b/tests/handlers/profile.test.ts
@@ -49,6 +49,7 @@ describe('profile handler', () => {
       },
       idToken: 'my-id-token',
       accessToken: 'my-access-token',
+      refreshToken: 'my-refresh-token',
       createdAt: Date.now()
     });
 
diff --git a/tests/handlers/session.test.ts b/tests/handlers/session.test.ts
index 01ca2be0..e94aa3cb 100644
--- a/tests/handlers/session.test.ts
+++ b/tests/handlers/session.test.ts
@@ -15,7 +15,8 @@ describe('session handler', () => {
             sub: '123'
           },
           createdAt: now,
-          idToken: 'my-id-token'
+          idToken: 'my-id-token',
+          refreshToken: 'my-refresh-token'
         });
       },
       save(): Promise<void> {
@@ -28,7 +29,8 @@ describe('session handler', () => {
     expect(session).toEqual({
       user: { sub: '123' },
       createdAt: now,
-      idToken: 'my-id-token'
+      idToken: 'my-id-token',
+      refreshToken: 'my-refresh-token'
     });
   });
 });
diff --git a/tests/session/cookie-store.test.ts b/tests/session/cookie-store.test.ts
index b97279e3..5f3d7e90 100644
--- a/tests/session/cookie-store.test.ts
+++ b/tests/session/cookie-store.test.ts
@@ -122,7 +122,7 @@ describe('cookie store', () => {
   });
 
   describe('with storeIdToken', () => {
-    describe('configured', () => {
+    describe('not configured', () => {
       const store = getStore({});
 
       test('should not store the id_token', async () => {
@@ -150,7 +150,7 @@ describe('cookie store', () => {
       });
     });
 
-    describe('not configured', () => {
+    describe('configured', () => {
       const store = getStore({
         storeIdToken: true
       });
@@ -181,4 +181,67 @@ describe('cookie store', () => {
       });
     });
   });
+
+  describe('with storeRefreshToken', () => {
+    describe('not configured', () => {
+      const store = getStore({});
+
+      test('should not store the refresh_token', async () => {
+        const { req, res, setHeaderFn } = getRequestResponse();
+        const now = Date.now();
+
+        await store.save(req, res, {
+          user: {
+            sub: '123'
+          },
+          createdAt: now,
+          idToken: 'my-id-token',
+          refreshToken: 'my-refresh-token'
+        });
+
+        const [, cookie] = setHeaderFn.mock.calls[0];
+        req.headers = {
+          cookie: `a0:session=${parse(cookie)['a0:session']}`
+        };
+
+        const session = await store.read(req);
+        expect(session).toEqual({
+          createdAt: now,
+          user: { sub: '123' }
+        });
+      });
+    });
+
+    describe('configured', () => {
+      const store = getStore({
+        storeRefreshToken: true
+      });
+
+      test('should store the refresh_token', async () => {
+        const { req, res, setHeaderFn } = getRequestResponse();
+        const now = Date.now();
+
+        await store.save(req, res, {
+          user: {
+            sub: '123'
+          },
+          createdAt: now,
+          idToken: 'my-id-token',
+          refreshToken: 'my-refresh-token'
+        });
+
+        const [, cookie] = setHeaderFn.mock.calls[0];
+        req.headers = {
+          cookie: `a0:session=${parse(cookie)['a0:session']}`
+        };
+
+        const session = await store.read(req);
+        expect(session).toEqual({
+          createdAt: now,
+          refreshToken: 'my-refresh-token',
+          user: { sub: '123' }
+        });
+      });
+    });
+  });
 });