Repository: auth0/auth0-spa-js
Pull request #521 (2020-07-10 17:51:52)

[SDK-1790] use refresh_tokens with multiple audiences
### Description

`useRefreshTokens: true` should be able to handle to handle multiple audiences, with the caveat that the first time you get an RT for a new audience you will have to rely on the iframe fallback, or if you can't because of ITP, do it interactively via popup.

Updated the playground for easier testing

![image](https://user-images.githubusercontent.com/1299658/87183616-5e5e7b80-c2de-11ea-98da-7447d8f2d38a.png)


### References

See: #512 

### Testing

An example of using RT with multiple audiences in an ITP enabled browser:

```js
const client = new Auth0Client({
  ...
  audience: 'http://api1/users',
  scope: 'read:users',
});

// get access token for existing audience using RT grant
const accessToken1 = await getTokenSilently({
  audience: 'http://api1/users',
  scope: 'read:users',
  ignoreCache: true,
});

// get access token for a new audience which falls back to iframe, because no RT
try {
  const accessToken1 = await getTokenSilently({
    audience: 'http://api2/books',
    scope: 'read:books'
  }); 
// fails because of ITP
} catch (e) {
  // get's access token and an RT interactively
  const accessToken2 = await getTokenWithPopup({
    audience: 'http://api2/books',
    scope: 'read:books'
  });
}

// Can now get access token with RT grant for the second audience silently
const accessToken3 = await getTokenSilently({
    ignoreCache: true,
    audience: 'http://api2/books',
    scope: 'read:books'
  });
```

- [ ] 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
- [x] 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/cypress/integration/getTokenSilently.js b/cypress/integration/getTokenSilently.js
index 0b04c31..d3b366f 100644
--- a/cypress/integration/getTokenSilently.js
+++ b/cypress/integration/getTokenSilently.js
@@ -1,10 +1,10 @@
 import { shouldBe, whenReady } from '../support/utils';
 
-describe('getTokenSilently', function() {
+describe('getTokenSilently', function () {
   beforeEach(cy.resetTests);
   afterEach(cy.logout);
 
-  it('returns an error when not logged in', function(done) {
+  it('returns an error when not logged in', function (done) {
     whenReady().then(win =>
       win.auth0.getTokenSilently().catch(error => {
         shouldBe('login_required', error.error);
@@ -48,7 +48,8 @@ describe('getTokenSilently', function() {
           cy.toggleSwitch('local-storage');
 
           cy.login().then(() => {
-            cy.reload().wait(5000);
+            cy.reload();
+            cy.get('#loaded', { timeout: 5000 });
 
             cy.get('[data-cy=get-token]')
               .click()
@@ -67,29 +68,100 @@ describe('getTokenSilently', function() {
           });
         });
       });
+    });
+  });
+
+  describe('when using refresh tokens', () => {
+    it('retrieves an access token using a refresh token', () => {
+      cy.server();
+
+      return whenReady().then(win => {
+        cy.toggleSwitch('local-storage');
+        cy.toggleSwitch('use-cache');
+        cy.toggleSwitch('refresh-tokens');
+
+        cy.login().then(() => {
+          cy.route({
+            method: 'POST',
+            url: '**/oauth/token'
+          }).as('tokenApiCheck');
+
+          cy.get('[data-cy=get-token]')
+            .should('have.length', 1)
+            .click()
+            .wait(500)
+            .get('[data-cy=access-token]')
+            .should('have.length', 2);
+
+          cy.wait('@tokenApiCheck').then(xhr => {
+            console.log(xhr);
+            assert.equal(
+              xhr.request.body.grant_type,
+              'refresh_token',
+              'used a refresh_token to get an access_token'
+            );
+          });
+        });
+      });
+    });
+
+    it('retrieves an access token for another audience using a refresh token', () => {
+      cy.server();
+
+      return whenReady().then(win => {
+        cy.toggleSwitch('local-storage');
+        cy.toggleSwitch('use-cache');
+        cy.toggleSwitch('refresh-tokens');
+
+        cy.login().then(() => {
+          cy.route({
+            method: 'POST',
+            url: '**/oauth/token'
+          }).as('tokenApiCheck');
+
+          cy.get('[data-cy=get-token]')
+            .click()
+            .wait(500)
+            .get('[data-cy=access-token]')
+            .should('have.length', 2);
+
+          cy.wait('@tokenApiCheck').then(xhr => {
+            console.log(xhr);
+            assert.equal(
+              xhr.request.body.grant_type,
+              'refresh_token',
+              'used a refresh_token to get an access_token'
+            );
+          });
+
+          cy.get('[data-cy=get-token-1]')
+            .click()
+            .wait(500)
+            .get('[data-cy=access-token-1]')
+            .should('have.length', 1);
+
+          cy.wait('@tokenApiCheck').then(xhr => {
+            console.log(xhr);
+            assert.equal(
+              xhr.request.body.grant_type,
+              'authorization_code',
+              'get a refresh_token for a new audience with an iframe'
+            );
+          });
+
+          cy.get('[data-cy=get-token-1]')
+            .click()
+            .wait(500)
+            .get('[data-cy=access-token-1]')
+            .should('have.length', 2);
 
-      describe('when using refresh tokens', () => {
-        /**
-         * This test will fail with a 'consent_required' error when running on localhost, but the fact that it does
-         * proves that the iframe method was attempted even though we're supposed to be using refresh tokens.
-         */
-        it.skip('attempts to retrieve an access token by falling back to the iframe method', () => {
-          return whenReady().then(win => {
-            cy.toggleSwitch('local-storage');
-            cy.toggleSwitch('use-cache');
-
-            cy.login().then(() => {
-              cy.toggleSwitch('refresh-tokens').wait(1000);
-              win.localStorage.clear();
-
-              cy.get('[data-cy=get-token]')
-                .click()
-                .wait(500)
-                .get('[data-cy=access-token]')
-                .should('have.length', 1);
-
-              cy.get('[data-cy=error]').should('contain', 'consent_required');
-            });
+          cy.wait('@tokenApiCheck').then(xhr => {
+            console.log(xhr);
+            assert.equal(
+              xhr.request.body.grant_type,
+              'refresh_token',
+              'use a refresh_token to get a new access_token'
+            );
           });
         });
       });
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index b5e3465..a925a36 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -20,9 +20,7 @@ Cypress.Commands.add('login', () => {
     .clear()
     .type('johnfoo+integration@gmail.com');
 
-  cy.get('.auth0-lock-input-password .auth0-lock-input')
-    .clear()
-    .type('1234');
+  cy.get('.auth0-lock-input-password .auth0-lock-input').clear().type('1234');
   cy.get('.auth0-lock-submit').click();
 
   return whenReady().then(() => {
@@ -48,9 +46,7 @@ Cypress.Commands.add('loginNoCallback', () => {
   cy.get('.auth0-lock-input-username .auth0-lock-input')
     .clear()
     .type('johnfoo+integration@gmail.com');
-  cy.get('.auth0-lock-input-password .auth0-lock-input')
-    .clear()
-    .type('1234');
+  cy.get('.auth0-lock-input-password .auth0-lock-input').clear().type('1234');
   cy.get('.auth0-lock-submit').click();
 });
 
@@ -58,4 +54,5 @@ Cypress.Commands.add('resetTests', () => {
   cy.visit('http://localhost:3000');
   cy.get('#reset-config').click();
   cy.get('#logout').click();
+  cy.window().then(win => win.localStorage.clear());
 });
diff --git a/cypress/support/utils.js b/cypress/support/utils.js
index 2c67ab5..f763ef2 100644
--- a/cypress/support/utils.js
+++ b/cypress/support/utils.js
@@ -3,7 +3,5 @@ module.exports.shouldNotBeUndefined = e => expect(e).to.not.be.undefined;
 module.exports.shouldBe = (expected, actual) => expect(actual).to.eq(expected);
 module.exports.shouldNotBe = (expected, actual) =>
   expect(actual).to.not.eq(expected);
-module.exports.whenReady = () => {
-  cy.wait(5000);
-  return cy.get('#loaded').then(() => cy.window());
-};
+module.exports.whenReady = () =>
+  cy.get('#loaded', { timeout: 5000 }).then(() => cy.window());
diff --git a/src/Auth0Client.ts b/src/Auth0Client.ts
index 0b92df9..415337c 100644
--- a/src/Auth0Client.ts
+++ b/src/Auth0Client.ts
@@ -307,6 +307,8 @@ export default class Auth0Client {
 
     const authResult = await oauthToken(
       {
+        audience: params.audience,
+        scope: params.scope,
         baseUrl: this.domainUrl,
         client_id: this.options.client_id,
         code_verifier,
@@ -440,6 +442,8 @@ export default class Auth0Client {
     this.transactionManager.remove(state);
 
     const tokenOptions = {
+      audience: transaction.audience,
+      scope: transaction.scope,
       baseUrl: this.domainUrl,
       client_id: this.options.client_id,
       code_verifier: transaction.code_verifier,
@@ -561,13 +565,9 @@ export default class Auth0Client {
 
       await lock.acquireLock(GET_TOKEN_SILENTLY_LOCK_KEY, 5000);
 
-      // Only get an access token using a refresh token if:
-      // * refresh tokens are enabled
-      // * no audience has been specified to getTokenSilently (we can only get a token for a new audience when using an iframe)
-      const authResult =
-        this.options.useRefreshTokens && !options.audience
-          ? await this._getTokenUsingRefreshToken(getTokenOptions)
-          : await this._getTokenFromIFrame(getTokenOptions);
+      const authResult = this.options.useRefreshTokens
+        ? await this._getTokenUsingRefreshToken(getTokenOptions)
+        : await this._getTokenFromIFrame(getTokenOptions);
 
       this.cache.save({ client_id: this.options.client_id, ...authResult });
 
@@ -720,6 +720,8 @@ export default class Auth0Client {
     const tokenResult = await oauthToken(
       {
         ...customOptions,
+        scope,
+        audience,
         baseUrl: this.domainUrl,
         client_id: this.options.client_id,
         code_verifier,
@@ -781,6 +783,8 @@ export default class Auth0Client {
       tokenResult = await oauthToken(
         {
           ...customOptions,
+          audience,
+          scope,
           baseUrl: this.domainUrl,
           client_id: this.options.client_id,
           grant_type: 'refresh_token',
diff --git a/src/global.ts b/src/global.ts
index 8cb359e..f5ed311 100644
--- a/src/global.ts
+++ b/src/global.ts
@@ -337,6 +337,8 @@ export interface OAuthTokenOptions extends TokenEndpointOptions {
   code_verifier: string;
   code: string;
   redirect_uri: string;
+  audience: string;
+  scope: string;
 }
 
 /**
diff --git a/src/token.worker.ts b/src/token.worker.ts
index 0eb0364..b030e70 100644
--- a/src/token.worker.ts
+++ b/src/token.worker.ts
@@ -1,26 +1,29 @@
 import { MISSING_REFRESH_TOKEN_ERROR_MESSAGE } from './constants';
 
-/**
- * @ignore
- */
-let refreshToken;
+let refreshTokens = {};
+
+const cacheKey = (audience, scope) => `${audience}|${scope}`;
+
+const getRefreshToken = (audience, scope) =>
+  refreshTokens[cacheKey(audience, scope)];
+
+const setRefreshToken = (refreshToken, audience, scope) =>
+  (refreshTokens[cacheKey(audience, scope)] = refreshToken);
+
+const deleteRefreshToken = (audience, scope) =>
+  delete refreshTokens[cacheKey(audience, scope)];
 
-/**
- * @ignore
- */
 const wait: any = time => new Promise(resolve => setTimeout(resolve, time));
 
-/**
- * @ignore
- */
 const messageHandler = async ({
-  data: { url, timeout, ...opts },
+  data: { url, timeout, audience, scope, ...opts },
   ports: [port]
 }) => {
   let json;
   try {
     const body = JSON.parse(opts.body);
     if (!body.refresh_token && body.grant_type === 'refresh_token') {
+      const refreshToken = getRefreshToken(audience, scope);
       if (!refreshToken) {
         throw new Error(MISSING_REFRESH_TOKEN_ERROR_MESSAGE);
       }
@@ -53,10 +56,10 @@ const messageHandler = async ({
     json = await response.json();
 
     if (json.refresh_token) {
-      refreshToken = json.refresh_token;
+      setRefreshToken(json.refresh_token, audience, scope);
       delete json.refresh_token;
     } else {
-      refreshToken = null;
+      deleteRefreshToken(audience, scope);
     }
 
     port.postMessage({
diff --git a/src/utils.ts b/src/utils.ts
index 2edf3c6..b61d625 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -217,11 +217,11 @@ const sendMessage = (message, to) =>
     to.postMessage(message, [messageChannel.port2]);
   });
 
-const switchFetch = async (url, opts, timeout, worker) => {
+const switchFetch = async (url, audience, scope, opts, timeout, worker) => {
   if (worker) {
     // AbortSignal is not serializable, need to implement in the Web Worker
     delete opts.signal;
-    return sendMessage({ url, timeout, ...opts }, worker);
+    return sendMessage({ url, audience, scope, timeout, ...opts }, worker);
   } else {
     const response = await fetch(url, opts);
     return {
@@ -233,6 +233,8 @@ const switchFetch = async (url, opts, timeout, worker) => {
 
 export const fetchWithTimeout = (
   url,
+  audience,
+  scope,
   options,
   worker,
   timeout = DEFAULT_FETCH_TIMEOUT_MS
@@ -248,7 +250,7 @@ export const fetchWithTimeout = (
   let timeoutId;
   // The promise will resolve with one of these two promises (the fetch or the timeout), whichever completes first.
   return Promise.race([
-    switchFetch(url, fetchOptions, timeout, worker),
+    switchFetch(url, audience, scope, fetchOptions, timeout, worker),
     new Promise((_, reject) => {
       timeoutId = setTimeout(() => {
         controller.abort();
@@ -260,12 +262,19 @@ export const fetchWithTimeout = (
   });
 };
 
-const getJSON = async (url, timeout, options, worker) => {
+const getJSON = async (url, timeout, audience, scope, options, worker) => {
   let fetchError, response;
 
   for (let i = 0; i < DEFAULT_SILENT_TOKEN_RETRY_COUNT; i++) {
     try {
-      response = await fetchWithTimeout(url, options, worker, timeout);
+      response = await fetchWithTimeout(
+        url,
+        audience,
+        scope,
+        options,
+        worker,
+        timeout
+      );
       fetchError = null;
       break;
     } catch (e) {
@@ -304,12 +313,14 @@ const getJSON = async (url, timeout, options, worker) => {
 };
 
 export const oauthToken = async (
-  { baseUrl, timeout, ...options }: TokenEndpointOptions,
+  { baseUrl, timeout, audience, scope, ...options }: TokenEndpointOptions,
   worker
 ) =>
   await getJSON(
     `${baseUrl}/oauth/token`,
     timeout,
+    audience || 'default',
+    scope,
     {
       method: 'POST',
       body: JSON.stringify({
diff --git a/static/index.html b/static/index.html
index 6801f3c..25c565b 100644
--- a/static/index.html
+++ b/static/index.html
@@ -13,7 +13,7 @@
 
   <body>
     <div id="app" class="container">
-      <div style="visibility: hidden;">
+      <div v-if="!loading" style="visibility: hidden;">
         <span id="loaded">loaded</span>
       </div>
 
@@ -22,63 +22,92 @@
       <p><strong>Is authenticated:</strong>&nbsp;{{ isAuthenticated }}</p>
 
       <div v-if="!loading">
-        <div class="btn-group mb-3">
-          <button class="btn btn-primary" @click="loginPopup">
-            Login popup
-          </button>
-
-          <button
-            class="btn btn-primary"
-            @click="loginRedirect"
-            id="login_redirect"
-          >
-            Login redirect
-          </button>
-
-          <button
-            class="btn btn-success"
-            @click="loginHandleRedirectCallback"
-            id="handle_redirect_callback"
-          >
-            Login redirect callback
-          </button>
-        </div>
-
-        <div class="btn-group mb-3">
-          <button
-            class="btn btn-secondary"
-            @click="getToken"
-            data-cy="get-token"
-          >
-            Get access token
-          </button>
-
-          <button class="btn btn-secondary" @click="getMultipleTokens">
-            Get multiple tokens (ignoring cache)
-          </button>
-
-          <button class="btn btn-secondary" @click="getTokenPopup">
-            Get access token with a popup
-          </button>
+        <div class="btn-toolbar justify-content-between">
+          <div class="btn-group mb-3">
+            <button class="btn btn-primary" @click="loginPopup">
+              Login popup
+            </button>
+
+            <button
+              class="btn btn-primary"
+              @click="loginRedirect"
+              id="login_redirect"
+            >
+              Login redirect
+            </button>
+
+            <button
+              class="btn btn-success"
+              @click="loginHandleRedirectCallback"
+              id="handle_redirect_callback"
+            >
+              Login redirect callback
+            </button>
+          </div>
 
-          <button class="btn btn-secondary" @click="getTokenAudience">
-            Get access token for a different audience
-          </button>
+          <div class="btn-group mb-3">
+            <button
+              class="btn btn-outline-primary"
+              @click="logout"
+              id="logout"
+              data-cy="logout"
+            >
+              logout (default)
+            </button>
+
+            <button class="btn btn-outline-primary" @click="logoutNoClient">
+              logout (no client id)
+            </button>
+          </div>
         </div>
 
-        <div class="btn-group mb-3">
-          <button
-            class="btn btn-outline-primary"
-            @click="logout"
-            id="logout"
-            data-cy="logout"
-          >
-            logout (default)
-          </button>
-
-          <button class="btn btn-outline-primary" @click="logoutNoClient">
-            logout (no client id)
-          </button>
+        <div
+          v-for="({ audience, scope, access_tokens }, i) in audienceScopes"
+          :set="sfx = i && `-${i}` || ''"
+        >
+          <div class="card mb-3 bg-light">
+            <div class="card-header">
+              <strong>{{audience || 'default'}}</strong>
+              <span
+                v-for="s of scope.split(' ')"
+                class="badge badge-success ml-1"
+                >{{s}}</span
+              >
+            </div>
+            <div class="card-body">
+              <div class="btn-group mb-0">
+                <button
+                  class="btn btn-outline-info"
+                  @click="() => getToken(audience, scope, access_tokens)"
+                  :data-cy="`get-token${sfx}`"
+                >
+                  Get access token
+                </button>
+
+                <button
+                  class="btn btn-outline-info"
+                  @click="() => getTokenPopup(audience, scope, access_tokens)"
+                >
+                  Get access token with a popup
+                </button>
+              </div>
+
+              <div class="card mb-0 mt-3" v-if="access_tokens.length > 0">
+                <div class="card-header">Access Tokens</div>
+                <div class="card-body">
+                  <ul v-for="token in access_tokens">
+                    <li :data-cy="`access-token${sfx}`">
+                      {{token | concat}} (<a
+                        :href="'https://jwt.io?token=' + token"
+                        target="_blank"
+                        >view</a
+                      >)
+                    </li>
+                  </ul>
+                </div>
+              </div>
+            </div>
+          </div>
         </div>
 
         <template v-if="error">
@@ -103,21 +132,6 @@
           </div>
         </div>
 
-        <div class="card mb-3" v-if="access_tokens.length > 0">
-          <div class="card-header">Access Tokens</div>
-          <div class="card-body">
-            <ul v-for="token in access_tokens">
-              <li data-cy="access-token">
-                {{token | concat}} (<a
-                  :href="'https://jwt.io?token=' + token"
-                  target="_blank"
-                  >view</a
-                >)
-              </li>
-            </ul>
-          </div>
-        </div>
-
         <div class="card mb-3" v-if="id_token">
           <div class="card-header">
             ID Token
@@ -246,7 +260,7 @@
 
       Vue.filter('concat', function (value) {
         if (value && value.length > 35) {
-          return value.substr(0, 32) + '...';
+          return value.substr(0, 16) + '…  …' + value.substr(-16, 16);
         }
 
         return value;
@@ -267,12 +281,23 @@
             useCache: data.useCache === false ? false : true,
             useConstructor: data.useConstructor === false ? false : true,
             profile: null,
-            access_tokens: [],
             id_token: '',
             isAuthenticated: false,
             domain: data.domain || defaultDomain,
             clientId: data.clientId || defaultClientId,
             audience: data.audience || defaultAudience,
+            audienceScopes: [
+              {
+                audience: data.audience || defaultAudience,
+                scope: 'openid profile email',
+                access_tokens: []
+              },
+              {
+                audience: `https://${data.domain || defaultDomain}/api/v2/`,
+                scope: 'read:rules',
+                access_tokens: []
+              }
+            ],
             error: null
           };
         },
@@ -367,7 +392,7 @@
             _self.access_tokens = [];
 
             _self.auth0.getTokenSilently().then(function (token) {
-              _self.access_tokens.push(token);
+              _self.audienceScopes[0].access_tokens.push(token);
 
               _self.auth0.getUser().then(function (user) {
                 _self.profile = user;
@@ -413,15 +438,17 @@
               });
             });
           },
-          getToken: function () {
+          getToken: function (audience, scope, access_tokens) {
             var _self = this;
 
             _self.auth0
               .getTokenSilently({
+                audience,
+                scope,
                 ignoreCache: !_self.useCache
               })
               .then(function (token) {
-                _self.access_tokens.push(token);
+                access_tokens.push(token);
                 _self.error = null;
 
                 auth0.isAuthenticated().then(function (isAuthenticated) {
@@ -438,42 +465,13 @@
                 }
               });
           },
-          getMultipleTokens: function () {
-            var _self = this;
-
-            Promise.all([
-              _self.auth0.getTokenSilently({ ignoreCache: true }),
-              _self.auth0.getTokenSilently({ ignoreCache: true })
-            ]).then(function (tokens) {
-              _self.access_tokens = [tokens[0], tokens[1]];
-            });
-          },
-          getTokenPopup: function () {
+          getTokenPopup: function (audience, scope, access_tokens) {
             var _self = this;
 
             _self.auth0
-              .getTokenWithPopup({
-                audience: 'https://brucke.auth0.com/api/v2/',
-                scope: 'read:rules'
-              })
-              .then(function (token) {
-                _self.access_tokens = [token];
-              });
-          },
-          getTokenAudience: function () {
-            var _self = this;
-
-            var differentAudienceOptions = {
-              audience: 'https://brucke.auth0.com/api/v2/',
-              scope: 'read:rules',
-              redirect_uri: 'http://localhost:3000/callback.html',
-              ignoreCache: !_self.useCache
-            };
-
-            _self.auth0
-              .getTokenSilently(differentAudienceOptions)
+              .getTokenWithPopup({ audience, scope })
               .then(function (token) {
-                _self.access_tokens = [token];
+                access_tokens.push(token);
               });
           },
           logout: function () {


Test patch:
diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts
index d620a72..9124709 100644
--- a/__tests__/index.test.ts
+++ b/__tests__/index.test.ts
@@ -439,6 +439,8 @@ describe('Auth0', () => {
 
       expect(utils.oauthToken).toHaveBeenCalledWith(
         {
+          audience: undefined,
+          scope: 'openid profile email',
           baseUrl: 'https://test.auth0.com',
           client_id: TEST_CLIENT_ID,
           code: TEST_CODE,
@@ -474,6 +476,7 @@ describe('Auth0', () => {
       expect(utils.oauthToken).toHaveBeenCalledWith(
         {
           audience: undefined,
+          scope: 'openid profile email',
           baseUrl: 'https://test.auth0.com',
           client_id: TEST_CLIENT_ID,
           code: TEST_CODE,
@@ -491,6 +494,8 @@ describe('Auth0', () => {
       await auth0.loginWithPopup({ audience: 'test-audience' });
       expect(utils.oauthToken).toHaveBeenCalledWith(
         {
+          audience: 'test-audience',
+          scope: 'openid profile email',
           baseUrl: 'https://test.auth0.com',
           client_id: TEST_CLIENT_ID,
           code: TEST_CODE,
@@ -1027,6 +1032,8 @@ describe('Auth0', () => {
 
         expect(utils.oauthToken).toHaveBeenCalledWith(
           {
+            audience: 'default',
+            scope: 'openid profile email',
             baseUrl: 'https://test.auth0.com',
             client_id: TEST_CLIENT_ID,
             code: TEST_CODE,
@@ -1239,6 +1246,8 @@ describe('Auth0', () => {
 
         expect(utils.oauthToken).toHaveBeenCalledWith(
           {
+            audience: 'default',
+            scope: 'openid profile email',
             baseUrl: 'https://test.auth0.com',
             client_id: TEST_CLIENT_ID,
             code: TEST_CODE,
@@ -1605,6 +1614,8 @@ describe('Auth0', () => {
 
           expect(utils.oauthToken).toHaveBeenCalledWith(
             {
+              audience: undefined,
+              scope: 'openid profile email offline_access',
               baseUrl: 'https://test.auth0.com',
               refresh_token: TEST_REFRESH_TOKEN,
               client_id: TEST_CLIENT_ID,
@@ -1672,6 +1683,8 @@ describe('Auth0', () => {
 
           expect(utils.oauthToken).toHaveBeenCalledWith(
             {
+              audience: undefined,
+              scope: 'openid email offline_access',
               baseUrl: 'https://test.auth0.com',
               refresh_token: TEST_REFRESH_TOKEN,
               client_id: TEST_CLIENT_ID,
@@ -1694,23 +1707,6 @@ describe('Auth0', () => {
             }
           });
         });
-
-        it('falls back to the iframe method when an audience is specified', async () => {
-          const { auth0, utils } = await setup({
-            useRefreshTokens: true
-          });
-
-          await auth0.getTokenSilently({
-            audience: 'other-audience',
-            ignoreCache: true
-          });
-
-          expect(utils.runIframe).toHaveBeenCalledWith(
-            `https://test.auth0.com/authorize?${TEST_QUERY_PARAMS}${TEST_AUTH0_CLIENT_QUERY_STRING}`,
-            'https://test.auth0.com',
-            undefined
-          );
-        });
       });
     });
 
@@ -1920,6 +1916,8 @@ describe('Auth0', () => {
 
         expect(utils.oauthToken).toHaveBeenCalledWith(
           {
+            audience: 'test:audience',
+            scope: 'openid profile email test:scope',
             baseUrl: 'https://test.auth0.com',
             client_id: TEST_CLIENT_ID,
             code: TEST_CODE,
diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts
index 3d6321c..7d0347d 100644
--- a/__tests__/utils.test.ts
+++ b/__tests__/utils.test.ts
@@ -20,11 +20,17 @@ import {
   DEFAULT_AUTHORIZE_TIMEOUT_IN_SECONDS,
   DEFAULT_SILENT_TOKEN_RETRY_COUNT
 } from '../src/constants';
+import { MessageChannel } from 'worker_threads';
 import unfetch from 'unfetch';
+// @ts-ignore
+import Worker from '../src/token.worker';
 
+jest.mock('../src/token.worker');
 jest.mock('unfetch');
 const mockUnfetch = <jest.Mock>unfetch;
 (<any>global).TextEncoder = TextEncoder;
+(<any>global).MessageChannel = MessageChannel;
+(<any>global).fetch = mockUnfetch;
 
 afterEach(() => {
   jest.resetAllMocks();
@@ -152,7 +158,7 @@ describe('utils', () => {
         })
       );
       jest.spyOn(window, 'clearTimeout');
-      await fetchWithTimeout('https://test.com/', {}, undefined);
+      await fetchWithTimeout('https://test.com/', null, null, {}, undefined);
       expect(clearTimeout).toBeCalledTimes(1);
     });
   });
@@ -288,6 +294,59 @@ describe('utils', () => {
       expect(mockUnfetch.mock.calls[0][1].signal).not.toBeUndefined();
     });
 
+    it('calls oauth/token with a worker with the correct url', async () => {
+      mockUnfetch.mockReturnValue(
+        new Promise(res =>
+          res({ ok: true, json: () => new Promise(ress => ress(true)) })
+        )
+      );
+      const worker = new Worker();
+      const spy = jest.spyOn(worker, 'postMessage');
+      const body = {
+        redirect_uri: 'http://localhost',
+        grant_type: 'authorization_code',
+        client_id: 'client_idIn',
+        code: 'codeIn',
+        code_verifier: 'code_verifierIn'
+      };
+
+      await oauthToken(
+        {
+          grant_type: 'authorization_code',
+          baseUrl: 'https://test.com',
+          client_id: 'client_idIn',
+          code: 'codeIn',
+          code_verifier: 'code_verifierIn',
+          audience: '__test_audience__',
+          scope: '__test_scope__'
+        },
+        worker
+      );
+
+      expect(mockUnfetch).toBeCalledWith('https://test.com/oauth/token', {
+        body: JSON.stringify(body),
+        headers: { 'Content-type': 'application/json' },
+        method: 'POST',
+        signal: abortController.signal
+      });
+
+      expect(mockUnfetch.mock.calls[0][1].signal).not.toBeUndefined();
+      expect(spy).toHaveBeenCalledWith(
+        {
+          body: JSON.stringify(body),
+          audience: '__test_audience__',
+          scope: '__test_scope__',
+          headers: {
+            'Content-type': 'application/json'
+          },
+          method: 'POST',
+          timeout: 10000,
+          url: 'https://test.com/oauth/token'
+        },
+        expect.arrayContaining([expect.anything()])
+      );
+    });
+
     it('handles error with error response', async () => {
       const theError = {
         error: 'the-error',