Repository: auth0/auth0-spa-js
Pull request #25 (2019-05-29 20:24:58)

Error handling in handleRedirectCallback
Throw errors provided by Auth0 authentication callback query params

Resolves https://github.com/auth0/auth0-spa-js/issues/24


Patch:
diff --git a/src/Auth0Client.ts b/src/Auth0Client.ts
index 8137e6c..04c5586 100644
--- a/src/Auth0Client.ts
+++ b/src/Auth0Client.ts
@@ -15,6 +15,7 @@ import {
 import Cache from './cache';
 import TransactionManager from './transaction-manager';
 import { verify as verifyIdToken } from './jwt';
+import { AuthenticationError } from './errors';
 import * as ClientStorage from './storage';
 import version from './version';
 
@@ -229,19 +230,26 @@ export default class Auth0Client {
         'There are no query params available at `window.location.search`.'
       );
     }
-    const codeResult = parseQueryResult(window.location.search.substr(1));
-    const transaction = this.transactionManager.get(codeResult.state);
+    const { state, code, error, error_description } = parseQueryResult(
+      window.location.search.substr(1)
+    );
+
+    if (error) {
+      throw new AuthenticationError(error, error_description, state);
+    }
+
+    const transaction = this.transactionManager.get(state);
     if (!transaction) {
       throw new Error('Invalid state');
     }
-    this.transactionManager.remove(codeResult.state);
+    this.transactionManager.remove(state);
 
     const authResult = await oauthToken({
       baseUrl: this.domainUrl,
       audience: this.options.audience,
       client_id: this.options.client_id,
       code_verifier: transaction.code_verifier,
-      code: codeResult.code
+      code
     });
 
     const decodedToken = this._verifyIdToken(
diff --git a/src/errors.ts b/src/errors.ts
new file mode 100644
index 0000000..e50302f
--- /dev/null
+++ b/src/errors.ts
@@ -0,0 +1,9 @@
+export class AuthenticationError extends Error {
+  constructor(
+    public error: string,
+    public error_description: string,
+    public state: string
+  ) {
+    super(error_description);
+  }
+}
diff --git a/src/global.ts b/src/global.ts
index 8c23d88..acdff11 100644
--- a/src/global.ts
+++ b/src/global.ts
@@ -14,7 +14,7 @@ interface BaseLoginOptions {
    * - `'login'`: prompt user for reauthentication
    * - `'consent'`: prompt user for consent before processing request
    * - `'select_account'`: prompt user to select an account
-   */ 
+   */
   prompt?: 'none' | 'login' | 'consent' | 'select_account';
   /**
    * Maximum allowable elasped time (in seconds) since authentication.
@@ -182,7 +182,9 @@ interface LogoutOptions {
  */
 interface AuthenticationResult {
   state: string;
-  code: string;
+  code?: string;
+  error?: string;
+  error_description?: string;
 }
 
 /**


Test patch:
diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts
index 0e6c8ce..a39b93a 100644
--- a/__tests__/index.test.ts
+++ b/__tests__/index.test.ts
@@ -6,6 +6,7 @@ jest.mock('../src/utils');
 
 import Auth0Client from '../src/Auth0Client';
 import createAuth0Client from '../src/index';
+import { AuthenticationError } from '../src/errors';
 import version from '../src/version';
 
 const TEST_DOMAIN = 'test.auth0.com';
@@ -30,6 +31,7 @@ const TEST_TELEMETRY_QUERY_STRING = `&auth0Client=${encodeURIComponent(
     })
   )
 )}`;
+
 const setup = async (options = {}) => {
   const auth0 = await createAuth0Client({
     domain: TEST_DOMAIN,
@@ -386,6 +388,49 @@ describe('Auth0', () => {
 
         expect(transactionManager.get).toHaveBeenCalledWith(queryState);
       });
+      it('throws error with AuthenticationError', async () => {
+        const { auth0, utils } = await localSetup();
+        const queryResult = { error: 'unauthorized' };
+        utils.parseQueryResult.mockReturnValue(queryResult);
+
+        await expect(auth0.handleRedirectCallback()).rejects.toBeInstanceOf(
+          AuthenticationError
+        );
+      });
+      it('throws AuthenticationError with message from error_description', async () => {
+        const { auth0, utils } = await localSetup();
+        const queryResult = {
+          error: 'unauthorized',
+          error_description: 'Unauthorized user'
+        };
+        utils.parseQueryResult.mockReturnValue(queryResult);
+
+        await expect(auth0.handleRedirectCallback()).rejects.toThrow(
+          queryResult.error_description
+        );
+      });
+      it('throws AuthenticationError with state, error, error_description', async () => {
+        const { auth0, utils } = await localSetup();
+        const queryResult = {
+          error: 'unauthorized',
+          error_description: 'Unauthorized user',
+          state: 'abcxyz'
+        };
+        utils.parseQueryResult.mockReturnValue(queryResult);
+
+        let errorThrown: AuthenticationError;
+        try {
+          await auth0.handleRedirectCallback();
+        } catch (error) {
+          errorThrown = error;
+        }
+
+        expect(errorThrown.state).toEqual(queryResult.state);
+        expect(errorThrown.error).toEqual(queryResult.error);
+        expect(errorThrown.error_description).toEqual(
+          queryResult.error_description
+        );
+      });
       it('throws error when there is no transaction', async () => {
         const { auth0, transactionManager } = await localSetup();
         transactionManager.get.mockReturnValue(undefined);