Repository: auth0/auth0-spa-js
Pull request #789 (2021-09-06 13:22:04)

[SDK-2750] Expose mfa_token from the mfa_required error when getting new tokens
When mfa is enabled, the `/token` endpoint can return a `mfa_required` error. In the case that it does, it also included a `mfa_token` property, which we are currently ignoring.

This PR introduces a `MfaRequiredError` class that is used to unwrap the above information in case of a `mfa_required` error, exposing the `mfa_token` to the users.


Patch:
diff --git a/src/errors.ts b/src/errors.ts
index 1a2446d..86cd197 100644
--- a/src/errors.ts
+++ b/src/errors.ts
@@ -3,7 +3,7 @@
  */
 export class GenericError extends Error {
   constructor(public error: string, public error_description: string) {
-    super(error_description);
+    super(error_description) /* istanbul ignore next */;
 
     Object.setPrototypeOf(this, GenericError.prototype);
   }
@@ -30,7 +30,7 @@ export class AuthenticationError extends GenericError {
     public state: string,
     public appState: any = null
   ) {
-    super(error, error_description);
+    super(error, error_description) /* istanbul ignore next */;
     //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
     Object.setPrototypeOf(this, AuthenticationError.prototype);
   }
@@ -42,7 +42,7 @@ export class AuthenticationError extends GenericError {
  */
 export class TimeoutError extends GenericError {
   constructor() {
-    super('timeout', 'Timeout');
+    super('timeout', 'Timeout') /* istanbul ignore next */;
     //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
     Object.setPrototypeOf(this, TimeoutError.prototype);
   }
@@ -53,7 +53,7 @@ export class TimeoutError extends GenericError {
  */
 export class PopupTimeoutError extends TimeoutError {
   constructor(public popup: Window) {
-    super();
+    super() /* istanbul ignore next */;
     //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
     Object.setPrototypeOf(this, PopupTimeoutError.prototype);
   }
@@ -61,8 +61,23 @@ export class PopupTimeoutError extends TimeoutError {
 
 export class PopupCancelledError extends GenericError {
   constructor(public popup: Window) {
-    super('cancelled', 'Popup closed');
+    super('cancelled', 'Popup closed') /* istanbul ignore next */;
     //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
     Object.setPrototypeOf(this, PopupCancelledError.prototype);
   }
 }
+
+/**
+ * Error thrown when the token exchange results in a `mfa_required` error
+ */
+export class MfaRequiredError extends GenericError {
+  constructor(
+    error: string,
+    error_description: string,
+    public mfa_token: string
+  ) {
+    super(error, error_description) /* istanbul ignore next */;
+    //https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
+    Object.setPrototypeOf(this, MfaRequiredError.prototype);
+  }
+}
diff --git a/src/http.ts b/src/http.ts
index 63c8156..b2ab4ad 100644
--- a/src/http.ts
+++ b/src/http.ts
@@ -7,7 +7,7 @@ import {
 
 import { sendMessage } from './worker/worker.utils';
 import { FetchOptions } from './global';
-import { GenericError } from './errors';
+import { GenericError, MfaRequiredError } from './errors';
 
 export const createAbortController = () => new AbortController();
 
@@ -133,7 +133,7 @@ export async function getJSON<T>(
   }
 
   const {
-    json: { error, error_description, ...success },
+    json: { error, error_description, ...data },
     ok
   } = response;
 
@@ -141,8 +141,12 @@ export async function getJSON<T>(
     const errorMessage =
       error_description || `HTTP error. Unable to fetch ${url}`;
 
+    if (error === 'mfa_required') {
+      throw new MfaRequiredError(error, errorMessage, data.mfa_token);
+    }
+
     throw new GenericError(error || 'request_error', errorMessage);
   }
 
-  return success;
+  return data;
 }
diff --git a/src/index.cjs.ts b/src/index.cjs.ts
index 921737d..ab2f5c9 100644
--- a/src/index.cjs.ts
+++ b/src/index.cjs.ts
@@ -3,7 +3,8 @@ import createAuth0Client, {
   GenericError,
   AuthenticationError,
   TimeoutError,
-  PopupTimeoutError
+  PopupTimeoutError,
+  MfaRequiredError
 } from './index';
 
 /**
@@ -17,5 +18,6 @@ wrapper.GenericError = GenericError;
 wrapper.AuthenticationError = AuthenticationError;
 wrapper.TimeoutError = TimeoutError;
 wrapper.PopupTimeoutError = PopupTimeoutError;
+wrapper.MfaRequiredError = MfaRequiredError;
 
 export default wrapper;
diff --git a/src/index.ts b/src/index.ts
index 8c8928c..d324173 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -29,7 +29,8 @@ export {
   AuthenticationError,
   TimeoutError,
   PopupTimeoutError,
-  PopupCancelledError
+  PopupCancelledError,
+  MfaRequiredError
 } from './errors';
 
 export { ICache, LocalStorageCache, InMemoryCache, Cacheable } from './cache';


Test patch:
diff --git a/__tests__/http.test.ts b/__tests__/http.test.ts
index 5d3c913..5079e17 100644
--- a/__tests__/http.test.ts
+++ b/__tests__/http.test.ts
@@ -1,5 +1,6 @@
 import fetch from 'unfetch';
-import { switchFetch } from '../src/http';
+import { MfaRequiredError } from '../src/errors';
+import { switchFetch, getJSON } from '../src/http';
 
 jest.mock('../src/worker/token.worker');
 jest.mock('unfetch');
@@ -19,3 +20,32 @@ describe('switchFetch', () => {
     expect(clearTimeout).toBeCalledTimes(1);
   });
 });
+
+describe('getJson', () => {
+  it('throws MfaRequiredError when mfa_required is returned', async () => {
+    mockUnfetch.mockImplementation(() =>
+      Promise.resolve({
+        ok: false,
+        json: () => Promise.resolve({ error: 'mfa_required' })
+      })
+    );
+
+    await expect(
+      getJSON('https://test.com/', null, null, null, {}, undefined)
+    ).rejects.toBeInstanceOf(MfaRequiredError);
+  });
+
+  it('reads the mfa_token when mfa_required is returned', async () => {
+    mockUnfetch.mockImplementation(() =>
+      Promise.resolve({
+        ok: false,
+        json: () =>
+          Promise.resolve({ error: 'mfa_required', mfa_token: '1234' })
+      })
+    );
+
+    await expect(
+      getJSON('https://test.com/', null, null, null, {}, undefined)
+    ).rejects.toHaveProperty('mfa_token', '1234');
+  });
+});