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

Issue:
User profile information is lost when refresh token is used.
Hi!

I've run into an issue where the profile object on my User is empty after the refresh token is used to get a new access token. It looks like useRefreshToken in the client generates an empty SigninResponse and merges in the new access and refresh tokens to that object. That causes an empty profile to get returned when the new User object is created. Should we maybe include the profile in the RefreshState object so it can be merged into the new User? Or am I just doing something incorrectly. Thanks for the help!

Pull request description:
fix id_token is lost on silent refresh token renewal
<!-- Please link relevant issue numbers or provide context for this change -->
Closes/fixes #688

### Checklist

- [ ] 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/src/RefreshState.ts b/src/RefreshState.ts
index 944c0ff4..f02f2cbe 100644
--- a/src/RefreshState.ts
+++ b/src/RefreshState.ts
@@ -1,6 +1,8 @@
 // Copyright (C) AuthTS Contributors
 // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
 
+import type { UserProfile } from "./User";
+
 /**
  * Fake state store implementation necessary for validating refresh token requests.
  *
@@ -14,18 +16,23 @@ export class RefreshState {
     public readonly id_token?: string;
     public readonly session_state: string | null;
     public readonly scope?: string;
+    public readonly profile: UserProfile;
 
     constructor(args: {
         refresh_token: string;
         id_token?: string;
         session_state: string | null;
         scope?: string;
+        profile: UserProfile;
+
         state?: unknown;
     }) {
         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.data = args.state;
     }
 }
diff --git a/src/ResponseValidator.ts b/src/ResponseValidator.ts
index c40c7975..4bdd244e 100644
--- a/src/ResponseValidator.ts
+++ b/src/ResponseValidator.ts
@@ -82,12 +82,20 @@ export class ResponseValidator {
 
         // OpenID Connect Core 1.0 says that id_token is optional in refresh response:
         // https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
-        const hasIdToken = response.isOpenId && !!response.id_token;
-        if (hasIdToken) {
-            this._validateIdTokenAttributes(response, state.id_token);
-            logger.debug("ID Token validated");
+        if (response.isOpenId) {
+            if (response.id_token) {
+                this._validateIdTokenAttributes(response, state.id_token);
+                logger.debug("ID Token validated");
+            }
+            else {
+                // if there's no id_token on the response, copy over id_token from original request
+                response.id_token = state.id_token;
+                // and decoded part too
+                response.profile = state.profile;
+            }
         }
 
+        const hasIdToken = response.isOpenId && !!response.id_token;
         await this._processClaims(response, false, hasIdToken);
         logger.debug("claims processed");
     }


Test patch:
diff --git a/src/OidcClient.test.ts b/src/OidcClient.test.ts
index af5ab08b..e5aac357 100644
--- a/src/OidcClient.test.ts
+++ b/src/OidcClient.test.ts
@@ -13,6 +13,7 @@ import { SignoutRequest } from "./SignoutRequest";
 import { SignoutResponse } from "./SignoutResponse";
 import { RefreshState } from "./RefreshState";
 import { SigninResponse } from "./SigninResponse";
+import type { UserProfile } from "./User";
 
 describe("OidcClient", () => {
     let subject: OidcClient;
@@ -347,6 +348,7 @@ describe("OidcClient", () => {
                 id_token: "id_token",
                 session_state: "session_state",
                 scope: "openid",
+                profile: {} as UserProfile,
             });
 
             // act
@@ -371,6 +373,7 @@ describe("OidcClient", () => {
                 id_token: "id_token",
                 session_state: "session_state",
                 scope: "openid",
+                profile: {} as UserProfile,
             });
 
             // act
@@ -410,6 +413,7 @@ describe("OidcClient", () => {
                 id_token: "id_token",
                 session_state: "session_state",
                 scope: "openid",
+                profile: {} as UserProfile,
             });
 
             // act
diff --git a/src/UserManager.test.ts b/src/UserManager.test.ts
index 532cc0bf..1ef0dc84 100644
--- a/src/UserManager.test.ts
+++ b/src/UserManager.test.ts
@@ -454,7 +454,11 @@ describe("UserManager", () => {
             expect(refreshedUser!.profile).toHaveProperty("nickname", "Nicholas");
             expect(useRefreshTokenSpy).toBeCalledWith(
                 expect.objectContaining({
-                    state: { refresh_token: user.refresh_token, session_state: null },
+                    state: {
+                        refresh_token: user.refresh_token,
+                        session_state: null,
+                        "profile": { "nickname": "Nick", "sub": "sub" },
+                    },
                 }),
             );
         });