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

Issue:
Authorization code flow sends incorrect redirect_url parameter
### Description

Following oAuth authorization code flow first user is redirected to `/authorize` endpoint and one of the parameters is `redirect_url` for callback url. That part works just fine, however there is issue with later step - code for token exchange. Callback url receives `code`, `state` etc. parameters to perform token exchange but `/oauth/token` endpoint receives incorrect `redirect_uri`. The culprit seems to be this line: https://github.com/auth0/auth0-spa-js/blob/2ace866d33cefc8140dceae7437e2a9e61469564/src/utils.ts#L195

The following statement strips out the path and query string parts of the url. This parameter should be populated either with `redirect_url` that's present in the client instance or retrieved (if it's saved) from corresponding state object. 

Even Auth0 documentation states that both `redirect_url` parameters sent to `/authorize` and `/oauth/token` must be the same: https://auth0.com/docs/api-auth/tutorials/authorization-code-grant-pkce#4-exchange-the-authorization-code-for-an-access-token

### Reproduction

1. Set `redirect_url` on the client config to `https://some-domain.com/callback` and proceed with authorization code flow. 
2. After signing in and redirect to provided `redirect_url` code for token exchange takes place
3. Parameter `redirect_url` sent to `/oauth/token` has value of: `https://some-domain.com`

### Environment

- Version of this library used: 1.3.2

Pull request description:
Send same redirect_uri as /authorize to /token
### Description

This PR ensures that the same `redirect_uri` value is sent to the `/token` endpoint as was sent to the `/authorize` endpoint.

In the case of `loginWithRedirect`, the redirect URI is stored in the transaction store and then used in `handleRedirectCallback`.

### References

Fixes #287 

### Testing

This has been tested manually in the [Vue quickstart](https://github.com/auth0-samples/auth0-vue-samples) that has been modified to accept a different callback URL from the base app URL.

- [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/Auth0Client.ts b/src/Auth0Client.ts
index 9ad33b3..286058e 100644
--- a/src/Auth0Client.ts
+++ b/src/Auth0Client.ts
@@ -134,7 +134,8 @@ export default class Auth0Client {
       code_verifier,
       appState,
       scope: params.scope,
-      audience: params.audience || 'default'
+      audience: params.audience || 'default',
+      redirect_uri: params.redirect_uri
     });
     return url + fragment;
   }
@@ -186,7 +187,8 @@ export default class Auth0Client {
       audience: options.audience || this.options.audience,
       client_id: this.options.client_id,
       code_verifier,
-      code: codeResult.code
+      code: codeResult.code,
+      redirect_uri: params.redirect_uri
     });
     const decodedToken = this._verifyIdToken(authResult.id_token, nonceIn);
     const cacheEntry = {
@@ -289,7 +291,8 @@ export default class Auth0Client {
       audience: this.options.audience,
       client_id: this.options.client_id,
       code_verifier: transaction.code_verifier,
-      code
+      code,
+      redirect_uri: transaction.redirect_uri
     });
 
     const decodedToken = this._verifyIdToken(
@@ -389,7 +392,8 @@ export default class Auth0Client {
         audience: options.audience || this.options.audience,
         client_id: this.options.client_id,
         code_verifier,
-        code: codeResult.code
+        code: codeResult.code,
+        redirect_uri: params.redirect_uri
       });
 
       const decodedToken = this._verifyIdToken(authResult.id_token, nonceIn);
diff --git a/src/global.ts b/src/global.ts
index 8399438..0f58c3d 100644
--- a/src/global.ts
+++ b/src/global.ts
@@ -234,6 +234,7 @@ interface OAuthTokenOptions {
   audience?: string;
   code_verifier: string;
   code: string;
+  redirect_uri: string;
 }
 
 /**
diff --git a/src/transaction-manager.ts b/src/transaction-manager.ts
index 570ab7c..f9ebf30 100644
--- a/src/transaction-manager.ts
+++ b/src/transaction-manager.ts
@@ -9,6 +9,7 @@ interface Transaction {
   audience: string;
   appState?: any;
   code_verifier: string;
+  redirect_uri: string;
 }
 interface Transactions {
   [key: string]: Transaction;


Test patch:
diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts
index 0fc1aa0..aa2cc28 100644
--- a/__tests__/index.test.ts
+++ b/__tests__/index.test.ts
@@ -260,10 +260,43 @@ describe('Auth0', () => {
         baseUrl: 'https://test.auth0.com',
         client_id: TEST_CLIENT_ID,
         code: TEST_CODE,
-        code_verifier: TEST_RANDOM_STRING
+        code_verifier: TEST_RANDOM_STRING,
+        redirect_uri: 'http://localhost'
       });
     });
-    it('calls oauth/token with correct params', async () => {
+
+    it('calls oauth/token with the same custom redirect_uri as /authorize', async () => {
+      const redirect_uri = 'http://another.uri';
+
+      const { auth0, utils } = await setup({
+        redirect_uri
+      });
+
+      await auth0.loginWithPopup();
+
+      expect(utils.createQueryParams).toHaveBeenCalledWith({
+        client_id: TEST_CLIENT_ID,
+        scope: TEST_SCOPES,
+        response_type: TEST_CODE,
+        response_mode: 'web_message',
+        state: TEST_ENCODED_STATE,
+        nonce: TEST_RANDOM_STRING,
+        redirect_uri,
+        code_challenge: TEST_BASE64_ENCODED_STRING,
+        code_challenge_method: 'S256'
+      });
+
+      expect(utils.oauthToken).toHaveBeenCalledWith({
+        audience: undefined,
+        baseUrl: 'https://test.auth0.com',
+        client_id: TEST_CLIENT_ID,
+        code: TEST_CODE,
+        code_verifier: TEST_RANDOM_STRING,
+        redirect_uri
+      });
+    });
+
+    it('calls oauth/token with correct params and a different audience', async () => {
       const { auth0, utils } = await setup();
 
       await auth0.loginWithPopup({ audience: 'test-audience' });
@@ -272,7 +305,8 @@ describe('Auth0', () => {
         baseUrl: 'https://test.auth0.com',
         client_id: TEST_CLIENT_ID,
         code: TEST_CODE,
-        code_verifier: TEST_RANDOM_STRING
+        code_verifier: TEST_RANDOM_STRING,
+        redirect_uri: 'http://localhost'
       });
     });
     it('calls `tokenVerifier.verify` with the `id_token` from in the oauth/token response', async () => {
@@ -514,7 +548,8 @@ describe('Auth0', () => {
           audience: 'default',
           code_verifier: TEST_RANDOM_STRING,
           nonce: TEST_RANDOM_STRING,
-          scope: TEST_SCOPES
+          scope: TEST_SCOPES,
+          redirect_uri: 'https://redirect.uri'
         }
       );
     });
@@ -1267,7 +1302,8 @@ describe('Auth0', () => {
           baseUrl: 'https://test.auth0.com',
           client_id: TEST_CLIENT_ID,
           code: TEST_CODE,
-          code_verifier: TEST_RANDOM_STRING
+          code_verifier: TEST_RANDOM_STRING,
+          redirect_uri: 'http://localhost'
         });
       });
       it('calls `tokenVerifier.verify` with the `id_token` from in the oauth/token response', async () => {
diff --git a/__tests__/transaction-manager.test.ts b/__tests__/transaction-manager.test.ts
index a40db18..b50e1b1 100644
--- a/__tests__/transaction-manager.test.ts
+++ b/__tests__/transaction-manager.test.ts
@@ -8,7 +8,8 @@ const transaction = {
   code_verifier: 'code_verifierIn',
   appState: 'appStateIn',
   scope: 'scopeIn',
-  audience: ' audienceIn'
+  audience: ' audienceIn',
+  redirect_uri: 'http://localhost'
 };
 
 jest.mock('../src/storage');