Repository: auth0/auth0-spa-js (ts)

Issue:
AbortController is not defined
### Describe the problem

Hi! I initially raised this issue in `@auth0/auth0-react` repository (https://github.com/auth0/auth0-react/issues/173), but for visibility, and because this is the actual package having the issue, I post this issue here as well.

On Firefox 52 (and probably other browsers versions that doesn't support AbortController API), the `getAccessTokenSilently` method will throw this error. It seems like this API is used in `@auth0/auth0-spa-js`, and there is a Polyfill for this API, but it isn't working. And it's probably the case for any browser version that doesn't support natively this API (https://developer.mozilla.org/en-US/docs/Web/API/AbortController).

Here is the stack trace I have when catching errors from the `getAccessTokenSilently`:

```
10:34:13.434 Error: AbortController is not defined
Stack trace:
OAuthError@http://localhost:3000/static/js/0.chunk.js:3816:17
["../../node_modules/@auth0/auth0-react/dist/auth0-react.esm.js"]/</normalizeErrorFn/<@http://localhost:3000/static/js/0.chunk.js:3841:14
["../../node_modules/@auth0/auth0-react/dist/auth0-react.esm.js"]/</Auth0Provider/getAccessTokenSilently</</<@http://localhost:3000/static/js/0.chunk.js:4127:19
step@http://localhost:3000/static/js/0.chunk.js:210:12
verb/<@http://localhost:3000/static/js/0.chunk.js:141:14
rejected@http://localhost:3000/static/js/0.chunk.js:103:14
 1 0.chunk.js:83125:18
	["../../node_modules/react-error-overlay/lib/index.js"]/</</</g/console[e] http://localhost:3000/static/js/0.chunk.js:83125:18
	ExportAuth/</< http://localhost:3000/static/js/main.chunk.js:3009:7
```

### What was the expected behavior?

The `getAccessTokenSilently` should not fail because of AbortController API being undefined on browsers versions that don't support this API natively.

### Reproduction

I'm working on a Private instance in Auth0, so I can't share client id, etc. And it seems that I can't make the examples working locally, but the private repository I'm working on is very simple, so I think you should be able to reproduce it by setting up the example and running it on Firefox 52.

### Environment

- **Version of `auth0-spa-js` used:** 1.13.5, used by `@auth0/auth0-react@1.2.0`
- **Which browsers have you tested in?** Firefox 52, Chromium 65, Safari 11
- **Which framework are you using, if applicable (Angular, React, etc):** React 16.13.1
- **Other modules/plugins/libraries that might be involved:**


Pull request:
Do not use AbortController in the worker if not available
This PR excludes the use of `AbortController` inside the web worker if it is
not natively supported by the browser. Currently, this generates errors if it
is not available, but we wouldn't like to polyfill it due to the size.

I have changed the code to allow browsers that support WebWorker but not AbortController to once again use the web worker.

Tested in Safari 10, 11, 12 and Firefox 52.

Fixes #675


Patch:
diff --git a/src/Auth0Client.ts b/src/Auth0Client.ts
index a799454..6c1aff5 100644
--- a/src/Auth0Client.ts
+++ b/src/Auth0Client.ts
@@ -61,7 +61,7 @@ import {
 
 // @ts-ignore
 import TokenWorker from './worker/token.worker.ts';
-import { isIE11, isSafari10, isSafari11, isSafari12_0 } from './user-agent';
+import { isIE11 } from './user-agent';
 import { singlePromise, retryPromise } from './promise-utils';
 
 /**
@@ -92,8 +92,7 @@ const cacheFactory = (location: string) => {
 /**
  * @ignore
  */
-const supportWebWorker = () =>
-  !isIE11() && !isSafari10() && !isSafari11() && !isSafari12_0();
+const supportWebWorker = () => !isIE11();
 
 /**
  * @ignore
diff --git a/src/user-agent.ts b/src/user-agent.ts
index 6184fe9..9246b57 100644
--- a/src/user-agent.ts
+++ b/src/user-agent.ts
@@ -1,10 +1,1 @@
 export const isIE11 = () => /Trident.*rv:11\.0/.test(navigator.userAgent);
-
-export const isSafari10 = () =>
-  /AppleWebKit.*Version\/10/.test(navigator.userAgent);
-
-export const isSafari11 = () =>
-  /AppleWebKit.*Version\/11/.test(navigator.userAgent);
-
-export const isSafari12_0 = () =>
-  /AppleWebKit.*Version\/12\.0/.test(navigator.userAgent);
diff --git a/src/worker/token.worker.ts b/src/worker/token.worker.ts
index d29d30f..ed64fc7 100644
--- a/src/worker/token.worker.ts
+++ b/src/worker/token.worker.ts
@@ -46,15 +46,19 @@ const messageHandler = async ({
       });
     }
 
-    const abortController = new AbortController();
-    const { signal } = abortController;
+    let abortController: AbortController;
+
+    if (typeof AbortController === 'function') {
+      abortController = new AbortController();
+      fetchOptions.signal = abortController.signal;
+    }
 
     let response: any;
 
     try {
       response = await Promise.race([
         wait(timeout),
-        fetch(fetchUrl, { ...fetchOptions, signal })
+        fetch(fetchUrl, { ...fetchOptions })
       ]);
     } catch (error) {
       // fetch error, reject `sendMessage` using `error` key so that we retry.
@@ -67,7 +71,12 @@ const messageHandler = async ({
 
     if (!response) {
       // If the request times out, abort it and let `fetchWithTimeout` raise the error.
-      abortController.abort();
+      if (abortController) abortController.abort();
+
+      port.postMessage({
+        error: "Timeout when executing 'fetch'"
+      });
+
       return;
     }
 


Test patch:
diff --git a/__tests__/Auth0Client/getTokenSilently.test.ts b/__tests__/Auth0Client/getTokenSilently.test.ts
index 9c8b92a..c44b9ee 100644
--- a/__tests__/Auth0Client/getTokenSilently.test.ts
+++ b/__tests__/Auth0Client/getTokenSilently.test.ts
@@ -619,27 +619,9 @@ describe('Auth0Client', () => {
           supported: false
         },
         {
-          name: 'Safari 10',
+          name: 'Chrome',
           userAgent:
-            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8',
-          supported: false
-        },
-        {
-          name: 'Safari 11',
-          userAgent:
-            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/604.1.28 (KHTML, like Gecko) Version/11.0 Safari/604.1.28',
-          supported: false
-        },
-        {
-          name: 'Safari 12',
-          userAgent:
-            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.1 Safari/605.1.15',
-          supported: false
-        },
-        {
-          name: 'Safari 12.1',
-          userAgent:
-            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Safari/605.1.15',
+            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36',
           supported: true
         }
       ].forEach(({ name, userAgent, supported }) =>
@@ -647,6 +629,7 @@ describe('Auth0Client', () => {
           supported ? 'with' : 'without'
         } the worker, when ${name}`, async () => {
           const originalUserAgent = window.navigator.userAgent;
+
           Object.defineProperty(window.navigator, 'userAgent', {
             value: userAgent,
             configurable: true
diff --git a/__tests__/api.test.ts b/__tests__/api.test.ts
index a7f0736..b1a8e59 100644
--- a/__tests__/api.test.ts
+++ b/__tests__/api.test.ts
@@ -131,7 +131,8 @@ describe('oauthToken', () => {
             'Content-type': 'application/json',
             'Auth0-Client': btoa(JSON.stringify(auth0Client))
           },
-          method: 'POST'
+          method: 'POST',
+          signal: new AbortController().signal
         },
         auth: {
           audience: '__test_audience__',
diff --git a/__tests__/token.worker.test.ts b/__tests__/token.worker.test.ts
index 6eba15c..b25ea80 100644
--- a/__tests__/token.worker.test.ts
+++ b/__tests__/token.worker.test.ts
@@ -36,6 +36,33 @@ describe('token worker', () => {
         json: () => ({ foo: 'bar', refresh_token: 'baz' })
       })
     );
+
+    const response = await messageHandlerAsync({
+      fetchUrl: '/foo',
+      fetchOptions: {
+        method: 'POST',
+        body: JSON.stringify({})
+      }
+    });
+
+    expect(response.json).toEqual({
+      foo: 'bar'
+    });
+
+    expect(mockFetch.mock.calls[0][1].signal).toBeDefined();
+  });
+
+  it('calls fetch without AbortSignal if AbortController is not available', async () => {
+    const originalAbortController = window.AbortController;
+    delete window.AbortController;
+
+    mockFetch.mockReturnValue(
+      Promise.resolve({
+        ok: true,
+        json: () => ({ foo: 'bar', refresh_token: 'baz' })
+      })
+    );
+
     const response = await messageHandlerAsync({
       fetchUrl: '/foo',
       fetchOptions: {
@@ -47,6 +74,10 @@ describe('token worker', () => {
     expect(response.json).toEqual({
       foo: 'bar'
     });
+
+    expect(mockFetch.mock.calls[0][1].signal).toBeUndefined();
+
+    window.AbortController = originalAbortController;
   });
 
   it(`stores the refresh token and uses it for grant_type='refresh_token'`, async () => {
@@ -100,6 +131,7 @@ describe('token worker', () => {
 
   it(`errors when fetch rejects`, async () => {
     mockFetch.mockReturnValue(Promise.reject(new Error('fail')));
+
     const response = await messageHandlerAsync({
       fetchUrl: '/foo',
       fetchOptions: {
@@ -107,9 +139,62 @@ describe('token worker', () => {
         body: JSON.stringify({})
       }
     });
+
     expect(response.error).toEqual('fail');
   });
 
+  it(`aborts when timed out`, async () => {
+    const originalAbortController = window.AbortController;
+    const abortFn = jest.fn();
+
+    window.AbortController = jest.fn(() => ({
+      signal: {},
+      abort: abortFn
+    })) as any;
+
+    mockFetch.mockReturnValue(
+      new Promise(resolve => {
+        setTimeout(resolve, 1000);
+      })
+    );
+
+    const response = await messageHandlerAsync({
+      fetchUrl: '/foo',
+      fetchOptions: {
+        method: 'POST',
+        body: JSON.stringify({})
+      },
+      timeout: 1
+    });
+
+    expect(response.error).toEqual("Timeout when executing 'fetch'");
+    expect(abortFn).toHaveBeenCalled();
+    window.AbortController = originalAbortController;
+  });
+
+  it(`does not abort when timed out if no abort controller`, async () => {
+    const originalAbortController = window.AbortController;
+    delete window.AbortController;
+
+    mockFetch.mockReturnValue(
+      new Promise(resolve => {
+        setTimeout(resolve, 1000);
+      })
+    );
+
+    const response = await messageHandlerAsync({
+      fetchUrl: '/foo',
+      fetchOptions: {
+        method: 'POST',
+        body: JSON.stringify({})
+      },
+      timeout: 1
+    });
+
+    expect(response.error).toEqual("Timeout when executing 'fetch'");
+    window.AbortController = originalAbortController;
+  });
+
   it('removes the stored refresh token if none was returned from the server', async () => {
     mockFetch
       .mockReturnValueOnce(