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

Issue:
Sending additional query params on a refresh token request
Hello,
I was testing out the refresh token implementation recently and ran into an issue. We have a rule that reaches out to an external service to grab the user's scopes based on what tenant they're logging into. We set this `tenant_id` on creation of the auth0 client. This works fine on a `authorization_code` flow, however the token refresh request doesn't include any custom query parameters added to the client and thus the rule breaks. 

Is this a planned feature? If not, I could always pick apart the `redirect_uri` to figure out the tenant, but that's more complexity in the rule that I'd like to avoid. 



Pull request description:
[SDK-1634] Pass custom options to the token endpoint
### Description

This fixes the ability to send custom options when using `getTokenSilently` in conjunction with using refresh tokens.

When using custom parameters with `getTokenSilently`, these custom options are now sent to both the **authorize** endpoint in the query string and the **token** endpoint in the request body (when not using refresh tokens), and to the **token** endpoint only, when using refresh tokens.

These custom parameters can then be picked up in a custom Rule.

This functionality appeared to work in when not using refresh tokens, as you could pass custom options and they would be sent, but only to the authorize endpoint.

### References

Fixes #448 

### Testing

Added test coverage, also tested manually using the playground.

- [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 c54958f..9d8da5f 100644
--- a/src/Auth0Client.ts
+++ b/src/Auth0Client.ts
@@ -668,8 +668,18 @@ export default class Auth0Client {
       throw new Error('Invalid state');
     }
 
+    const {
+      scope,
+      audience,
+      redirect_uri,
+      ignoreCache,
+      timeoutInSeconds,
+      ...customOptions
+    } = options;
+
     const tokenResult = await oauthToken(
       {
+        ...customOptions,
         baseUrl: this.domainUrl,
         client_id: this.options.client_id,
         code_verifier,
@@ -695,7 +705,7 @@ export default class Auth0Client {
   ): Promise<any> {
     options.scope = getUniqueScopes(
       this.defaultScope,
-      this.scope,
+      this.options.scope,
       options.scope
     );
 
@@ -718,9 +728,19 @@ export default class Auth0Client {
       window.location.origin;
 
     let tokenResult;
+
+    const {
+      scope,
+      audience,
+      ignoreCache,
+      timeoutInSeconds,
+      ...customOptions
+    } = options;
+
     try {
       tokenResult = await oauthToken(
         {
+          ...customOptions,
           baseUrl: this.domainUrl,
           client_id: this.options.client_id,
           grant_type: 'refresh_token',
@@ -737,6 +757,7 @@ export default class Auth0Client {
       }
       throw e;
     }
+
     const decodedToken = this._verifyIdToken(tokenResult.id_token);
 
     return {
diff --git a/src/global.ts b/src/global.ts
index 6efb93a..d48e8a7 100644
--- a/src/global.ts
+++ b/src/global.ts
@@ -313,6 +313,7 @@ export interface TokenEndpointOptions {
   client_id: string;
   grant_type: string;
   timeout?: number;
+  [key: string]: any;
 }
 
 /**
diff --git a/static/index.html b/static/index.html
index c4ad229..eb96a61 100644
--- a/static/index.html
+++ b/static/index.html
@@ -414,7 +414,10 @@
             var _self = this;
 
             _self.auth0
-              .getTokenSilently({ ignoreCache: !_self.useCache })
+              .getTokenSilently({
+                ignoreCache: !_self.useCache,
+                aCustomOption: 'hello world'
+              })
               .then(function (token) {
                 _self.access_tokens.push(token);
                 _self.error = null;


Test patch:
diff --git a/__tests__/Auth0Client.test.ts b/__tests__/Auth0Client.test.ts
index bec9bfd..1145ae2 100644
--- a/__tests__/Auth0Client.test.ts
+++ b/__tests__/Auth0Client.test.ts
@@ -542,4 +542,68 @@ describe('Auth0Client', () => {
     expect(access_token).toEqual('my_access_token');
     expect(utils.runIframe).not.toHaveBeenCalled();
   });
+
+  it('sends custom options through to the token endpoint when using an iframe', async () => {
+    const auth0 = setup();
+
+    await login(auth0, true);
+
+    jest.spyOn(<any>utils, 'runIframe').mockResolvedValue({
+      access_token: 'my_access_token',
+      state: 'MTIz'
+    });
+
+    await auth0.getTokenSilently({
+      ignoreCache: true,
+      customParam: 'hello world'
+    });
+
+    expect(
+      (<any>utils.runIframe).mock.calls[0][0].includes(
+        'customParam=hello%20world'
+      )
+    ).toBe(true);
+
+    expect(JSON.parse(mockFetch.mock.calls[1][1].body)).toEqual({
+      redirect_uri: 'my_callback_url',
+      client_id: 'auth0_client_id',
+      grant_type: 'authorization_code',
+      customParam: 'hello world',
+      code_verifier: '123'
+    });
+  });
+
+  it('sends custom options through to the token endpoint when using refresh tokens', async () => {
+    const auth0 = setup({
+      useRefreshTokens: true
+    });
+
+    await login(auth0, true, { refresh_token: 'a_refresh_token' });
+
+    mockFetch.mockResolvedValueOnce(
+      fetchResponse(true, {
+        id_token: 'my_id_token',
+        refresh_token: 'my_refresh_token',
+        access_token: 'my_access_token',
+        expires_in: 86400
+      })
+    );
+
+    expect(utils.runIframe).not.toHaveBeenCalled();
+
+    const access_token = await auth0.getTokenSilently({
+      ignoreCache: true,
+      customParam: 'hello world'
+    });
+
+    expect(JSON.parse(mockFetch.mock.calls[1][1].body)).toEqual({
+      redirect_uri: 'my_callback_url',
+      client_id: 'auth0_client_id',
+      grant_type: 'refresh_token',
+      refresh_token: 'a_refresh_token',
+      customParam: 'hello world'
+    });
+
+    expect(access_token).toEqual('my_access_token');
+  });
 });