Repository: auth0/auth0-spa-js
Pull request #659 (2020-12-01 15:43:32)

[SDK-2172] Add SDK metrics to all API calls
The requests to the /token endpoint did not include the `Auth0-Client` header, making it harder to pull metrics from our data.

This PR ensures the token endpoint gets added the appropriate header in the following situations:

- Get a token after LoginWithPopup
- Get a token after LoginWithRedirect
- Refreshing a token with a worker
- Refreshing a token without a worker


Patch:
diff --git a/src/Auth0Client.ts b/src/Auth0Client.ts
index 4ecc292..8126071 100644
--- a/src/Auth0Client.ts
+++ b/src/Auth0Client.ts
@@ -32,11 +32,10 @@ import {
   MISSING_REFRESH_TOKEN_ERROR_MESSAGE,
   DEFAULT_SCOPE,
   RECOVERABLE_ERRORS,
-  DEFAULT_SESSION_CHECK_EXPIRY_DAYS
+  DEFAULT_SESSION_CHECK_EXPIRY_DAYS,
+  DEFAULT_AUTH0_CLIENT
 } from './constants';
 
-import version from './version';
-
 import {
   Auth0ClientOptions,
   BaseLoginOptions,
@@ -200,14 +199,7 @@ export default class Auth0Client {
 
   private _url(path) {
     const auth0Client = encodeURIComponent(
-      btoa(
-        JSON.stringify(
-          this.options.auth0Client || {
-            name: 'auth0-spa-js',
-            version: version
-          }
-        )
-      )
+      btoa(JSON.stringify(this.options.auth0Client || DEFAULT_AUTH0_CLIENT))
     );
     return `${this.domainUrl}${path}&auth0Client=${auth0Client}`;
   }
@@ -381,7 +373,8 @@ export default class Auth0Client {
         code_verifier,
         code: codeResult.code,
         grant_type: 'authorization_code',
-        redirect_uri: params.redirect_uri
+        redirect_uri: params.redirect_uri,
+        auth0Client: this.options.auth0Client
       } as OAuthTokenOptions,
       this.worker
     );
@@ -517,7 +510,8 @@ export default class Auth0Client {
       client_id: this.options.client_id,
       code_verifier: transaction.code_verifier,
       grant_type: 'authorization_code',
-      code
+      code,
+      auth0Client: this.options.auth0Client
     } as OAuthTokenOptions;
 
     // some old versions of the SDK might not have added redirect_uri to the
@@ -830,7 +824,8 @@ export default class Auth0Client {
         code_verifier,
         code: codeResult.code,
         grant_type: 'authorization_code',
-        redirect_uri: params.redirect_uri
+        redirect_uri: params.redirect_uri,
+        auth0Client: this.options.auth0Client
       } as OAuthTokenOptions,
       this.worker
     );
@@ -899,7 +894,8 @@ export default class Auth0Client {
           grant_type: 'refresh_token',
           refresh_token: cache && cache.refresh_token,
           redirect_uri,
-          ...(timeout && { timeout })
+          ...(timeout && { timeout }),
+          auth0Client: this.options.auth0Client
         } as RefreshTokenOptions,
         this.worker
       );
diff --git a/src/constants.ts b/src/constants.ts
index 30b9952..63e35b6 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,4 +1,5 @@
 import { PopupConfigOptions } from './global';
+import version from './version';
 
 /**
  * @ignore
@@ -58,3 +59,11 @@ export const RECOVERABLE_ERRORS = [
  * @ignore
  */
 export const DEFAULT_SESSION_CHECK_EXPIRY_DAYS = 1;
+
+/**
+ * @ignore
+ */
+export const DEFAULT_AUTH0_CLIENT = {
+  name: 'auth0-spa-js',
+  version: version
+};
diff --git a/src/global.ts b/src/global.ts
index 769d6dc..b05ff51 100644
--- a/src/global.ts
+++ b/src/global.ts
@@ -391,6 +391,7 @@ export interface TokenEndpointOptions {
   client_id: string;
   grant_type: string;
   timeout?: number;
+  auth0Client: any;
   [key: string]: any;
 }
 
diff --git a/src/utils.ts b/src/utils.ts
index 6910b5f..dc0e193 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -10,7 +10,8 @@ import {
   DEFAULT_AUTHORIZE_TIMEOUT_IN_SECONDS,
   DEFAULT_SILENT_TOKEN_RETRY_COUNT,
   DEFAULT_FETCH_TIMEOUT_MS,
-  CLEANUP_IFRAME_TIMEOUT_IN_SECONDS
+  CLEANUP_IFRAME_TIMEOUT_IN_SECONDS,
+  DEFAULT_AUTH0_CLIENT
 } from './constants';
 
 import { PopupTimeoutError, TimeoutError, GenericError } from './errors';
@@ -358,7 +359,14 @@ const getJSON = async (
 };
 
 export const oauthToken = async (
-  { baseUrl, timeout, audience, scope, ...options }: TokenEndpointOptions,
+  {
+    baseUrl,
+    timeout,
+    audience,
+    scope,
+    auth0Client,
+    ...options
+  }: TokenEndpointOptions,
   worker: Worker
 ) =>
   await getJSON(
@@ -373,7 +381,10 @@ export const oauthToken = async (
         ...options
       }),
       headers: {
-        'Content-type': 'application/json'
+        'Content-type': 'application/json',
+        'Auth0-Client': btoa(
+          JSON.stringify(auth0Client || DEFAULT_AUTH0_CLIENT)
+        )
       }
     },
     worker


Test patch:
diff --git a/__tests__/Auth0Client/getTokenSilently.test.ts b/__tests__/Auth0Client/getTokenSilently.test.ts
index 0932cc3..5eae71e 100644
--- a/__tests__/Auth0Client/getTokenSilently.test.ts
+++ b/__tests__/Auth0Client/getTokenSilently.test.ts
@@ -38,6 +38,7 @@ import {
 } from '../constants';
 
 import { releaseLockSpy } from '../../__mocks__/browser-tabs-lock';
+import { DEFAULT_AUTH0_CLIENT } from '../../src/constants';
 
 jest.mock('unfetch');
 jest.mock('es-cookie');
@@ -179,13 +180,19 @@ describe('Auth0Client', () => {
 
       await getTokenSilently(auth0);
 
-      assertPost('https://auth0_domain/oauth/token', {
-        redirect_uri: TEST_REDIRECT_URI,
-        client_id: TEST_CLIENT_ID,
-        code_verifier: TEST_CODE_VERIFIER,
-        grant_type: 'authorization_code',
-        code: TEST_CODE
-      });
+      assertPost(
+        'https://auth0_domain/oauth/token',
+        {
+          redirect_uri: TEST_REDIRECT_URI,
+          client_id: TEST_CLIENT_ID,
+          code_verifier: TEST_CODE_VERIFIER,
+          grant_type: 'authorization_code',
+          code: TEST_CODE
+        },
+        {
+          'Auth0-Client': btoa(JSON.stringify(DEFAULT_AUTH0_CLIENT))
+        }
+      );
     });
 
     it('calls the token endpoint with the correct params when using refresh tokens', async () => {
@@ -201,12 +208,18 @@ describe('Auth0Client', () => {
         ignoreCache: true
       });
 
-      assertPost('https://auth0_domain/oauth/token', {
-        redirect_uri: TEST_REDIRECT_URI,
-        client_id: TEST_CLIENT_ID,
-        grant_type: 'refresh_token',
-        refresh_token: TEST_REFRESH_TOKEN
-      });
+      assertPost(
+        'https://auth0_domain/oauth/token',
+        {
+          redirect_uri: TEST_REDIRECT_URI,
+          client_id: TEST_CLIENT_ID,
+          grant_type: 'refresh_token',
+          refresh_token: TEST_REFRESH_TOKEN
+        },
+        {
+          'Auth0-Client': btoa(JSON.stringify(DEFAULT_AUTH0_CLIENT))
+        }
+      );
     });
 
     it('calls the token endpoint with the correct params when passing redirect uri and using refresh tokens', async () => {
@@ -224,12 +237,18 @@ describe('Auth0Client', () => {
         ignoreCache: true
       });
 
-      assertPost('https://auth0_domain/oauth/token', {
-        redirect_uri,
-        client_id: TEST_CLIENT_ID,
-        grant_type: 'refresh_token',
-        refresh_token: TEST_REFRESH_TOKEN
-      });
+      assertPost(
+        'https://auth0_domain/oauth/token',
+        {
+          redirect_uri,
+          client_id: TEST_CLIENT_ID,
+          grant_type: 'refresh_token',
+          refresh_token: TEST_REFRESH_TOKEN
+        },
+        {
+          'Auth0-Client': btoa(JSON.stringify(DEFAULT_AUTH0_CLIENT))
+        }
+      );
     });
 
     it('calls the token endpoint with the correct params when not providing any redirect uri and using refresh tokens', async () => {
@@ -247,12 +266,18 @@ describe('Auth0Client', () => {
         ignoreCache: true
       });
 
-      assertPost('https://auth0_domain/oauth/token', {
-        redirect_uri: 'http://localhost',
-        client_id: TEST_CLIENT_ID,
-        grant_type: 'refresh_token',
-        refresh_token: TEST_REFRESH_TOKEN
-      });
+      assertPost(
+        'https://auth0_domain/oauth/token',
+        {
+          redirect_uri: 'http://localhost',
+          client_id: TEST_CLIENT_ID,
+          grant_type: 'refresh_token',
+          refresh_token: TEST_REFRESH_TOKEN
+        },
+        {
+          'Auth0-Client': btoa(JSON.stringify(DEFAULT_AUTH0_CLIENT))
+        }
+      );
     });
 
     it('calls the token endpoint with the correct timeout when using refresh tokens', async () => {
@@ -351,6 +376,7 @@ describe('Auth0Client', () => {
 
       jest.spyOn(<any>utils, 'runIframe').mockResolvedValue({
         access_token: TEST_ACCESS_TOKEN,
+        code: TEST_CODE,
         state: TEST_STATE
       });
 
@@ -359,6 +385,19 @@ describe('Auth0Client', () => {
       await getTokenSilently(auth0);
 
       expectToHaveBeenCalledWithAuth0ClientParam(utils.runIframe, auth0Client);
+      assertPost(
+        'https://auth0_domain/oauth/token',
+        {
+          redirect_uri: TEST_REDIRECT_URI,
+          client_id: TEST_CLIENT_ID,
+          code_verifier: TEST_CODE_VERIFIER,
+          grant_type: 'authorization_code',
+          code: TEST_CODE
+        },
+        {
+          'Auth0-Client': btoa(JSON.stringify(auth0Client))
+        }
+      );
     });
 
     it('refreshes the token when cache available without access token', async () => {
@@ -458,6 +497,9 @@ describe('Auth0Client', () => {
           redirect_uri: TEST_REDIRECT_URI,
           refresh_token: TEST_REFRESH_TOKEN
         },
+        {
+          'Auth0-Client': btoa(JSON.stringify(DEFAULT_AUTH0_CLIENT))
+        },
         1
       );
 
@@ -474,13 +516,19 @@ describe('Auth0Client', () => {
 
       await loginWithRedirect(auth0);
 
-      assertPost('https://auth0_domain/oauth/token', {
-        redirect_uri: TEST_REDIRECT_URI,
-        client_id: TEST_CLIENT_ID,
-        code_verifier: TEST_CODE_VERIFIER,
-        grant_type: 'authorization_code',
-        code: TEST_CODE
-      });
+      assertPost(
+        'https://auth0_domain/oauth/token',
+        {
+          redirect_uri: TEST_REDIRECT_URI,
+          client_id: TEST_CLIENT_ID,
+          code_verifier: TEST_CODE_VERIFIER,
+          grant_type: 'authorization_code',
+          code: TEST_CODE
+        },
+        {
+          'Auth0-Client': btoa(JSON.stringify(DEFAULT_AUTH0_CLIENT))
+        }
+      );
 
       mockFetch.mockResolvedValueOnce(
         fetchResponse(true, {
@@ -501,6 +549,9 @@ describe('Auth0Client', () => {
           redirect_uri: TEST_REDIRECT_URI,
           refresh_token: TEST_REFRESH_TOKEN
         },
+        {
+          'Auth0-Client': btoa(JSON.stringify(DEFAULT_AUTH0_CLIENT))
+        },
         1
       );
 
@@ -519,13 +570,19 @@ describe('Auth0Client', () => {
 
       await loginWithRedirect(auth0);
 
-      assertPost('https://auth0_domain/oauth/token', {
-        redirect_uri: TEST_REDIRECT_URI,
-        client_id: TEST_CLIENT_ID,
-        code_verifier: TEST_CODE_VERIFIER,
-        grant_type: 'authorization_code',
-        code: TEST_CODE
-      });
+      assertPost(
+        'https://auth0_domain/oauth/token',
+        {
+          redirect_uri: TEST_REDIRECT_URI,
+          client_id: TEST_CLIENT_ID,
+          code_verifier: TEST_CODE_VERIFIER,
+          grant_type: 'authorization_code',
+          code: TEST_CODE
+        },
+        {
+          'Auth0-Client': btoa(JSON.stringify(DEFAULT_AUTH0_CLIENT))
+        }
+      );
 
       const access_token = await getTokenSilently(auth0, { ignoreCache: true });
 
@@ -537,6 +594,9 @@ describe('Auth0Client', () => {
           redirect_uri: TEST_REDIRECT_URI,
           refresh_token: TEST_REFRESH_TOKEN
         },
+        {
+          'Auth0-Client': btoa(JSON.stringify(DEFAULT_AUTH0_CLIENT))
+        },
         1
       );
 
diff --git a/__tests__/Auth0Client/helpers.ts b/__tests__/Auth0Client/helpers.ts
index fe824e5..8446bac 100644
--- a/__tests__/Auth0Client/helpers.ts
+++ b/__tests__/Auth0Client/helpers.ts
@@ -26,10 +26,15 @@ const authorizationResponse: AuthenticationResult = {
 };
 
 export const assertPostFn = mockFetch => {
-  return (url, body, callNum = 0) => {
+  return (url, body, headers = null, callNum = 0) => {
     const [actualUrl, opts] = mockFetch.mock.calls[callNum];
     expect(url).toEqual(actualUrl);
     expect(body).toEqual(JSON.parse(opts.body));
+    if (headers) {
+      Object.keys(headers).forEach(header =>
+        expect(headers[header]).toEqual(opts.headers[header])
+      );
+    }
   };
 };
 
diff --git a/__tests__/Auth0Client/loginWithPopup.test.ts b/__tests__/Auth0Client/loginWithPopup.test.ts
index a3a5735..55a548a 100644
--- a/__tests__/Auth0Client/loginWithPopup.test.ts
+++ b/__tests__/Auth0Client/loginWithPopup.test.ts
@@ -29,7 +29,11 @@ import {
   TEST_STATE
 } from '../constants';
 
-import { DEFAULT_POPUP_CONFIG_OPTIONS } from '../../src/constants';
+import {
+  DEFAULT_AUTH0_CLIENT,
+  DEFAULT_POPUP_CONFIG_OPTIONS
+} from '../../src/constants';
+import version from '../../src/version';
 
 jest.mock('unfetch');
 jest.mock('es-cookie');
@@ -236,13 +240,19 @@ describe('Auth0Client', () => {
       await loginWithPopup(auth0);
 
       expect(mockWindow.open).toHaveBeenCalled();
-      assertPost('https://auth0_domain/oauth/token', {
-        redirect_uri: TEST_REDIRECT_URI,
-        client_id: TEST_CLIENT_ID,
-        code_verifier: TEST_CODE_VERIFIER,
-        grant_type: 'authorization_code',
-        code: 'my_code'
-      });
+      assertPost(
+        'https://auth0_domain/oauth/token',
+        {
+          redirect_uri: TEST_REDIRECT_URI,
+          client_id: TEST_CLIENT_ID,
+          code_verifier: TEST_CODE_VERIFIER,
+          grant_type: 'authorization_code',
+          code: 'my_code'
+        },
+        {
+          'Auth0-Client': btoa(JSON.stringify(DEFAULT_AUTH0_CLIENT))
+        }
+      );
     });
 
     it('uses default config', async () => {
@@ -313,13 +323,19 @@ describe('Auth0Client', () => {
       await loginWithPopup(auth0, {}, { popup });
 
       expect(mockWindow.open).not.toHaveBeenCalled();
-      assertPost('https://auth0_domain/oauth/token', {
-        redirect_uri: TEST_REDIRECT_URI,
-        client_id: TEST_CLIENT_ID,
-        code_verifier: TEST_CODE_VERIFIER,
-        grant_type: 'authorization_code',
-        code: 'my_code'
-      });
+      assertPost(
+        'https://auth0_domain/oauth/token',
+        {
+          redirect_uri: TEST_REDIRECT_URI,
+          client_id: TEST_CLIENT_ID,
+          code_verifier: TEST_CODE_VERIFIER,
+          grant_type: 'authorization_code',
+          code: 'my_code'
+        },
+        {
+          'Auth0-Client': btoa(JSON.stringify(DEFAULT_AUTH0_CLIENT))
+        }
+      );
     });
 
     it('opens popup with custom auth0Client', async () => {
diff --git a/__tests__/Auth0Client/loginWithRedirect.test.ts b/__tests__/Auth0Client/loginWithRedirect.test.ts
index f948d55..43792ea 100644
--- a/__tests__/Auth0Client/loginWithRedirect.test.ts
+++ b/__tests__/Auth0Client/loginWithRedirect.test.ts
@@ -33,6 +33,7 @@ import {
   TEST_SCOPES,
   TEST_STATE
 } from '../constants';
+import version from '../../src/version';
 
 jest.mock('unfetch');
 jest.mock('es-cookie');
@@ -115,13 +116,24 @@ describe('Auth0Client', () => {
         code_challenge_method: 'S256'
       });
 
-      assertPost('https://auth0_domain/oauth/token', {
-        redirect_uri: TEST_REDIRECT_URI,
-        client_id: TEST_CLIENT_ID,
-        code_verifier: TEST_CODE_VERIFIER,
-        grant_type: 'authorization_code',
-        code: TEST_CODE
-      });
+      assertPost(
+        'https://auth0_domain/oauth/token',
+        {
+          redirect_uri: TEST_REDIRECT_URI,
+          client_id: TEST_CLIENT_ID,
+          code_verifier: TEST_CODE_VERIFIER,
+          grant_type: 'authorization_code',
+          code: TEST_CODE
+        },
+        {
+          'Auth0-Client': btoa(
+            JSON.stringify({
+              name: 'auth0-spa-js',
+              version: version
+            })
+          )
+        }
+      );
     });
 
     it('should log the user in using different default scope', async () => {
diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts
index 3730ac1..0ad8a5e 100644
--- a/__tests__/utils.test.ts
+++ b/__tests__/utils.test.ts
@@ -24,6 +24,7 @@ import { MessageChannel } from 'worker_threads';
 import unfetch from 'unfetch';
 // @ts-ignore
 import Worker from '../src/token.worker';
+import version from '../src/version';
 
 jest.mock('../src/token.worker');
 jest.mock('unfetch');
@@ -274,19 +275,27 @@ describe('utils', () => {
           res({ ok: true, json: () => new Promise(ress => ress(true)) })
         )
       );
+      const auth0Client = {
+        name: 'auth0-spa-js',
+        version: version
+      };
 
       await oauthToken({
         grant_type: 'authorization_code',
         baseUrl: 'https://test.com',
         client_id: 'client_idIn',
         code: 'codeIn',
-        code_verifier: 'code_verifierIn'
+        code_verifier: 'code_verifierIn',
+        auth0Client
       });
 
       expect(mockUnfetch).toBeCalledWith('https://test.com/oauth/token', {
         body:
           '{"redirect_uri":"http://localhost","grant_type":"authorization_code","client_id":"client_idIn","code":"codeIn","code_verifier":"code_verifierIn"}',
-        headers: { 'Content-type': 'application/json' },
+        headers: {
+          'Content-type': 'application/json',
+          'Auth0-Client': btoa(JSON.stringify(auth0Client))
+        },
         method: 'POST',
         signal: abortController.signal
       });
@@ -309,6 +318,10 @@ describe('utils', () => {
         code: 'codeIn',
         code_verifier: 'code_verifierIn'
       };
+      const auth0Client = {
+        name: 'auth0-spa-js',
+        version: version
+      };
 
       await oauthToken(
         {
@@ -318,14 +331,18 @@ describe('utils', () => {
           code: 'codeIn',
           code_verifier: 'code_verifierIn',
           audience: '__test_audience__',
-          scope: '__test_scope__'
+          scope: '__test_scope__',
+          auth0Client
         },
         worker
       );
 
       expect(mockUnfetch).toBeCalledWith('https://test.com/oauth/token', {
         body: JSON.stringify(body),
-        headers: { 'Content-type': 'application/json' },
+        headers: {
+          'Content-type': 'application/json',
+          'Auth0-Client': btoa(JSON.stringify(auth0Client))
+        },
         method: 'POST',
         signal: abortController.signal
       });
@@ -337,7 +354,8 @@ describe('utils', () => {
           audience: '__test_audience__',
           scope: '__test_scope__',
           headers: {
-            'Content-type': 'application/json'
+            'Content-type': 'application/json',
+            'Auth0-Client': btoa(JSON.stringify(auth0Client))
           },
           method: 'POST',
           timeout: 10000,