Repository: auth0/auth0-spa-js
Pull request #252 (2019-10-25 11:42:05)

Fix for race condition when using `sha256` on IE11
### Description

This PR fixes an issue with resolving SHA256 operations in IE11.

msCrypto (IE11) uses the old spec, which [is not Promise based](https://msdn.microsoft.com/en-us/expression/dn904640(v=vs.71)). Instead of returning a promise, it returns a CryptoOperation
with a result property in it.

As a result, the various events need to be handled in the event that we're working in IE11 (hence the msCrypto check). These events just call resolve or reject depending on the handler.

The `sha256` function essentially now has two clear paths depending on IE11 vs. not-IE11 - either return the promise from the browser's crypto API, or return a new promise which is resolved or rejected based on the events raised by `msCrypto`.

### References

CryptoOperation reference: https://msdn.microsoft.com/en-us/expression/dn904640(v=vs.71)

### Testing

- [X] 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/src/utils.ts b/src/utils.ts
index 3518ca9..1bc56d2 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -116,17 +116,35 @@ export const createQueryParams = (params: any) => {
 };
 
 export const sha256 = async (s: string) => {
-  const response = await Promise.resolve(
-    getCryptoSubtle().digest({ name: 'SHA-256' }, new TextEncoder().encode(s))
+  const digestOp = getCryptoSubtle().digest(
+    { name: 'SHA-256' },
+    new TextEncoder().encode(s)
   );
+
   // msCrypto (IE11) uses the old spec, which is not Promise based
   // https://msdn.microsoft.com/en-us/expression/dn904640(v=vs.71)
   // Instead of returning a promise, it returns a CryptoOperation
-  // with a `result` property in it
-  if ((<any>response).result) {
-    return (<any>response).result;
+  // with a result property in it.
+  // As a result, the various events need to be handled in the event that we're
+  // working in IE11 (hence the msCrypto check). These events just call resolve
+  // or reject depending on their intention.
+  if ((<any>window).msCrypto) {
+    return new Promise((res, rej) => {
+      digestOp.oncomplete = e => {
+        res(e.target.result);
+      };
+
+      digestOp.onerror = (e: ErrorEvent) => {
+        rej(e.error);
+      };
+
+      digestOp.onabort = () => {
+        rej('The digest operation was aborted');
+      };
+    });
   }
-  return response;
+
+  return await digestOp;
 };
 
 const urlEncodeB64 = (input: string) => {
diff --git a/static/index.html b/static/index.html
index a8d28bb..095b6df 100644
--- a/static/index.html
+++ b/static/index.html
@@ -62,8 +62,8 @@
               Promise.all([
                 auth0.getTokenSilently({ ignoreCache: true }),
                 auth0.getTokenSilently({ ignoreCache: true })
-              ]).then(function([token1, token2]) {
-                console.log(token1, token2);
+              ]).then(function(tokens) {
+                console.log(tokens[0], tokens[1]);
               });
             });
             $('#getTokenPopup').click(function() {


Test patch:
diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts
index e7fdcea..584f2d3 100644
--- a/__tests__/utils.test.ts
+++ b/__tests__/utils.test.ts
@@ -162,18 +162,75 @@ describe('utils', () => {
       const result = await sha256('test');
       expect(result).toBe(true);
     });
-    it('handles ie11 digest.result scenario', async () => {
+    it('handles ie11 digest.result scenario', () => {
+      (<any>global).msCrypto = {};
+
+      const digestResult = {
+        oncomplete: null
+      };
+
       (<any>global).crypto = {
         subtle: {
-          digest: jest.fn((alg, encoded) => {
-            expect(alg).toMatchObject({ name: 'SHA-256' });
-            expect(Array.from(encoded)).toMatchObject([116, 101, 115, 116]);
-            return { result: true };
+          digest: jest.fn(() => {
+            return digestResult;
           })
         }
       };
-      const result = await sha256('test');
-      expect(result).toBe(true);
+
+      const sha = sha256('test').then(r => {
+        expect(r).toBe(true);
+      });
+
+      digestResult.oncomplete({ target: { result: true } });
+
+      return sha;
+    });
+    it('handles ie11 digest.result error scenario', () => {
+      (<any>global).msCrypto = {};
+
+      const digestResult = {
+        onerror: null
+      };
+
+      (<any>global).crypto = {
+        subtle: {
+          digest: jest.fn(() => {
+            return digestResult;
+          })
+        }
+      };
+
+      const sha = sha256('test').catch(e => {
+        expect(e).toBe('An error occurred');
+      });
+
+      digestResult.onerror({ error: 'An error occurred' });
+
+      return sha;
+    });
+
+    it('handles ie11 digest.result abort scenario', () => {
+      (<any>global).msCrypto = {};
+
+      const digestResult = {
+        onabort: null
+      };
+
+      (<any>global).crypto = {
+        subtle: {
+          digest: jest.fn(() => {
+            return digestResult;
+          })
+        }
+      };
+
+      const sha = sha256('test').catch(e => {
+        expect(e).toBe('The digest operation was aborted');
+      });
+
+      digestResult.onabort();
+
+      return sha;
     });
   });
   describe('bufferToBase64UrlEncoded ', () => {