Repository: authts/oidc-client-ts (ts)

Issue:
Include redirect_uri
Hi! 👋 
      
Firstly, thanks for your work on this project! 🙂

Today I used [patch-package](https://github.com/ds300/patch-package) to patch `oidc-client-ts@2.2.5` for the project I'm working on.

The problem was that we're using Azure B2C as Auth-Provider and it does need a redirect_uri even on refresh-token-POST-requests. I know it's not in the oauth-specification and it doesn't really make sense. However, we do have to use it and we deal with it.

Here is the diff that solved my problem:

```diff
diff --git a/node_modules/oidc-client-ts/dist/esm/oidc-client-ts.js b/node_modules/oidc-client-ts/dist/esm/oidc-client-ts.js
index 2bc62f7..50bd84e 100644
--- a/node_modules/oidc-client-ts/dist/esm/oidc-client-ts.js
+++ b/node_modules/oidc-client-ts/dist/esm/oidc-client-ts.js
@@ -805,7 +805,8 @@ var OidcClientSettingsStore = class {
     // extra
     extraQueryParams = {},
     extraTokenParams = {},
-    extraHeaders = {}
+    extraHeaders = {},
+    includeRedirectUriInRefreshTokenRequest = false
   }) {
     this.authority = authority;
     if (metadataUrl) {
@@ -858,6 +859,7 @@ var OidcClientSettingsStore = class {
     this.extraQueryParams = extraQueryParams;
     this.extraTokenParams = extraTokenParams;
     this.extraHeaders = extraHeaders;
+    this.includeRedirectUriInRefreshTokenRequest = includeRedirectUriInRefreshTokenRequest;
   }
 };
 
@@ -1049,6 +1051,10 @@ var TokenClient = class {
         }
         break;
     }
+    // fix for Azure B2C "speciality"
+    if (this._settings.includeRedirectUriInRefreshTokenRequest) {
+      params.set('redirect_uri', this._settings.redirect_uri);
+    }
     const url = await this._metadataService.getTokenEndpoint(false);
     logger2.debug("got token endpoint");
     const response = await this._jsonService.postForm(url, { body: params, basicAuth, timeoutInSeconds, initCredentials: this._settings.fetchRequestCredentials });
diff --git a/node_modules/oidc-client-ts/dist/types/oidc-client-ts.d.ts b/node_modules/oidc-client-ts/dist/types/oidc-client-ts.d.ts
index 67480e1..ba9e70a 100644
--- a/node_modules/oidc-client-ts/dist/types/oidc-client-ts.d.ts
+++ b/node_modules/oidc-client-ts/dist/types/oidc-client-ts.d.ts
@@ -633,6 +633,13 @@ export declare interface OidcClientSettings {
      * Only scopes in this list will be passed in the token refresh request.
      */
     refreshTokenAllowedScope?: string | undefined;
+    /**
+     * Include redirect_uri parameter in refresh token request.
+     * Yes, this is outside of OIDC specification and makes not really sense, but some big providers (AKA Azure B2C) does require redirect_uri in refresh token request.
+     *
+     * see: https://learn.microsoft.com/en-us/azure/active-directory-b2c/authorization-code-flow#4-refresh-the-token
+     */
+    includeRedirectUriInRefreshTokenRequest?: boolean;
 }
 
 /**
@@ -675,7 +682,8 @@ export declare class OidcClientSettingsStore {
     readonly fetchRequestCredentials: RequestCredentials;
     readonly refreshTokenAllowedScope: string | undefined;
     readonly disablePKCE: boolean;
-    constructor({ authority, metadataUrl, metadata, signingKeys, metadataSeed, client_id, client_secret, response_type, scope, redirect_uri, post_logout_redirect_uri, client_authentication, prompt, display, max_age, ui_locales, acr_values, resource, response_mode, filterProtocolClaims, loadUserInfo, staleStateAgeInSeconds, clockSkewInSeconds, userInfoJwtIssuer, mergeClaims, disablePKCE, stateStore, refreshTokenCredentials, revokeTokenAdditionalContentTypes, fetchRequestCredentials, refreshTokenAllowedScope, extraQueryParams, extraTokenParams, extraHeaders, }: OidcClientSettings);
+    readonly includeRedirectUriInRefreshTokenRequest: boolean;
+    constructor({ authority, metadataUrl, metadata, signingKeys, metadataSeed, client_id, client_secret, response_type, scope, redirect_uri, post_logout_redirect_uri, client_authentication, prompt, display, max_age, ui_locales, acr_values, resource, response_mode, filterProtocolClaims, loadUserInfo, staleStateAgeInSeconds, clockSkewInSeconds, userInfoJwtIssuer, mergeClaims, disablePKCE, stateStore, refreshTokenCredentials, revokeTokenAdditionalContentTypes, fetchRequestCredentials, refreshTokenAllowedScope, extraQueryParams, extraTokenParams, extraHeaders, includeRedirectUriInRefreshTokenRequest,}: OidcClientSettings);
 }
 
 /**
diff --git a/node_modules/oidc-client-ts/dist/umd/oidc-client-ts.js b/node_modules/oidc-client-ts/dist/umd/oidc-client-ts.js
index 0c1838e..67c715a 100644
--- a/node_modules/oidc-client-ts/dist/umd/oidc-client-ts.js
+++ b/node_modules/oidc-client-ts/dist/umd/oidc-client-ts.js
@@ -1104,6 +1104,10 @@ var TokenClient = class {
         }
         break;
     }
+    // fix for Azure B2C "speciality"
+    if (this._settings.includeRedirectUriInRefreshTokenRequest) {
+      params.set('redirect_uri', this._settings.redirect_uri);
+    }
     const url = await this._metadataService.getTokenEndpoint(false);
     logger2.debug("got token endpoint");
     const response = await this._jsonService.postForm(url, { body: params, basicAuth, timeoutInSeconds, initCredentials: this._settings.fetchRequestCredentials });
```

<em>This issue body was [partially generated by patch-package](https://github.com/ds300/patch-package/issues/296).</em>


Pull request description:
allow to optionally pass redirect_uri into exchangeRefreshToken
<!-- Please link relevant issue numbers or provide context for this change -->
Closes/fixes #1175

Removes the `resource` prop from the state and adds it into the args to pass down to the `exchangeRefreshToken` function. Treat `resource`, `extraTokenParams` and `redirect_uri` and any future prop the same way.

### Checklist

- [x] This PR makes changes to the public API <!-- was the API report (docs/oidc-client-ts.api.md) updated by this PR? -->
- [x] I have included links for closing relevant issue numbers


Patch:
diff --git a/docs/oidc-client-ts.api.md b/docs/oidc-client-ts.api.md
index a396f129..1170b884 100644
--- a/docs/oidc-client-ts.api.md
+++ b/docs/oidc-client-ts.api.md
@@ -333,7 +333,7 @@ export class OidcClient {
     // (undocumented)
     protected readonly _tokenClient: TokenClient;
     // (undocumented)
-    useRefreshToken({ state, timeoutInSeconds, extraTokenParams, }: UseRefreshTokenArgs): Promise<SigninResponse>;
+    useRefreshToken({ state, redirect_uri, resource, timeoutInSeconds, extraTokenParams, }: UseRefreshTokenArgs): Promise<SigninResponse>;
     // Warning: (ae-forgotten-export) The symbol "ResponseValidator" needs to be exported by the entry point index.d.ts
     //
     // (undocumented)
@@ -570,7 +570,7 @@ export class RefreshState {
         scope?: string;
         profile: UserProfile;
         state?: unknown;
-    }, resource?: string | string[]);
+    });
     readonly data?: unknown;
     // (undocumented)
     readonly id_token?: string;
@@ -579,8 +579,6 @@ export class RefreshState {
     // (undocumented)
     readonly refresh_token: string;
     // (undocumented)
-    readonly resource?: string | string[];
-    // (undocumented)
     readonly scope?: string;
     // (undocumented)
     readonly session_state: string | null;
@@ -929,6 +927,10 @@ export interface UseRefreshTokenArgs {
     // (undocumented)
     extraTokenParams?: Record<string, unknown>;
     // (undocumented)
+    redirect_uri?: string;
+    // (undocumented)
+    resource?: string | string[];
+    // (undocumented)
     state: RefreshState;
     // (undocumented)
     timeoutInSeconds?: number;
diff --git a/src/OidcClient.ts b/src/OidcClient.ts
index 49e9447e..3ab42db5 100644
--- a/src/OidcClient.ts
+++ b/src/OidcClient.ts
@@ -33,9 +33,12 @@ export interface CreateSigninRequestArgs
  * @public
  */
 export interface UseRefreshTokenArgs {
-    state: RefreshState;
-    timeoutInSeconds?: number;
+    redirect_uri?: string;
+    resource?: string | string[];
     extraTokenParams?: Record<string, unknown>;
+    timeoutInSeconds?: number;
+
+    state: RefreshState;
 }
 
 /**
@@ -185,6 +188,8 @@ export class OidcClient {
 
     public async useRefreshToken({
         state,
+        redirect_uri,
+        resource,
         timeoutInSeconds,
         extraTokenParams,
     }: UseRefreshTokenArgs): Promise<SigninResponse> {
@@ -205,9 +210,10 @@ export class OidcClient {
 
         const result = await this._tokenClient.exchangeRefreshToken({
             refresh_token: state.refresh_token,
-            resource: state.resource,
             // provide the (possible filtered) scope list
             scope,
+            redirect_uri,
+            resource,
             timeoutInSeconds,
             ...extraTokenParams,
         });
@@ -216,7 +222,7 @@ export class OidcClient {
         logger.debug("validating response", response);
         await this._validator.validateRefreshResponse(response, {
             ...state,
-            // overide the scope in the state handed over to the validator
+            // override the scope in the state handed over to the validator
             // so it can set the granted scope to the requested scope in case none is included in the response
             scope,
         });
diff --git a/src/RefreshState.ts b/src/RefreshState.ts
index c2187aa7..54de5451 100644
--- a/src/RefreshState.ts
+++ b/src/RefreshState.ts
@@ -17,7 +17,6 @@ export class RefreshState {
     public readonly session_state: string | null;
     public readonly scope?: string;
     public readonly profile: UserProfile;
-    public readonly resource?: string | string[];
 
     constructor(args: {
         refresh_token: string;
@@ -27,13 +26,12 @@ export class RefreshState {
         profile: UserProfile;
 
         state?: unknown;
-    }, resource?: string | string[]) {
+    }) {
         this.refresh_token = args.refresh_token;
         this.id_token = args.id_token;
         this.session_state = args.session_state;
         this.scope = args.scope;
         this.profile = args.profile;
-        this.resource = resource;
 
         this.data = args.state;
 
diff --git a/src/TokenClient.ts b/src/TokenClient.ts
index 6d6f5ac4..a56558a2 100644
--- a/src/TokenClient.ts
+++ b/src/TokenClient.ts
@@ -39,6 +39,7 @@ export interface ExchangeCredentialsArgs {
 export interface ExchangeRefreshTokenArgs {
     client_id?: string;
     client_secret?: string;
+    redirect_uri?: string;
 
     grant_type?: string;
     refresh_token: string;
diff --git a/src/UserManager.ts b/src/UserManager.ts
index f55f0992..65e18825 100644
--- a/src/UserManager.ts
+++ b/src/UserManager.ts
@@ -282,15 +282,20 @@ export class UserManager {
         const logger = this._logger.create("signinSilent");
         const {
             silentRequestTimeoutInSeconds,
-            resource,
             ...requestArgs
         } = args;
         // first determine if we have a refresh token, or need to use iframe
         let user = await this._loadUser();
         if (user?.refresh_token) {
             logger.debug("using refresh token");
-            const state = new RefreshState(user as Required<User>, resource);
-            return await this._useRefreshToken({ state, extraTokenParams: requestArgs.extraTokenParams });
+            const state = new RefreshState(user as Required<User>);
+            return await this._useRefreshToken({
+                state,
+                redirect_uri: requestArgs.redirect_uri,
+                resource: requestArgs.resource,
+                extraTokenParams: requestArgs.extraTokenParams,
+                timeoutInSeconds: silentRequestTimeoutInSeconds,
+            });
         }
 
         const url = this.settings.silent_redirect_uri;


Test patch:
diff --git a/src/OidcClient.test.ts b/src/OidcClient.test.ts
index aa3d43dc..c6f76863 100644
--- a/src/OidcClient.test.ts
+++ b/src/OidcClient.test.ts
@@ -426,10 +426,10 @@ describe("OidcClient", () => {
                 session_state: "session_state",
                 scope: "openid",
                 profile: {} as UserProfile,
-            }, "resource");
+            });
 
             // act
-            const response = await subject.useRefreshToken({ state });
+            const response = await subject.useRefreshToken({ state, resource: "resource" });
 
             // assert
             expect(exchangeRefreshTokenMock).toHaveBeenCalledWith( {
diff --git a/src/UserManager.test.ts b/src/UserManager.test.ts
index afaf997f..a4eef928 100644
--- a/src/UserManager.test.ts
+++ b/src/UserManager.test.ts
@@ -552,11 +552,11 @@ describe("UserManager", () => {
             expect(useRefreshTokenSpy).toBeCalledWith(
                 expect.objectContaining({
                     state: {
-                        resource: "resource",
                         refresh_token: user.refresh_token,
                         session_state: null,
                         "profile": { "nickname": "Nick", "sub": "sub" },
                     },
+                    resource: "resource",
                 }),
             );
         });