Skip to content

update id_token when a new Access Token is fetched #2189

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/server/auth-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4584,7 +4584,7 @@ ca/T0LLtgmbMmxSv/MmzIg==

const [error, updatedTokenSet] = await authClient.getTokenSet(tokenSet);
expect(error).toBeNull();
expect(updatedTokenSet).toEqual(tokenSet);
expect(updatedTokenSet?.tokenSet).toEqual(tokenSet);
});

it("should return an error if the token set does not contain a refresh token and the access token has expired", async () => {
Expand Down Expand Up @@ -4657,7 +4657,7 @@ ca/T0LLtgmbMmxSv/MmzIg==

const [error, updatedTokenSet] = await authClient.getTokenSet(tokenSet);
expect(error).toBeNull();
expect(updatedTokenSet).toEqual({
expect(updatedTokenSet?.tokenSet).toEqual({
accessToken: DEFAULT.accessToken,
refreshToken: DEFAULT.refreshToken,
expiresAt: expect.any(Number)
Expand Down Expand Up @@ -4778,7 +4778,7 @@ ca/T0LLtgmbMmxSv/MmzIg==

const [error, updatedTokenSet] = await authClient.getTokenSet(tokenSet);
expect(error).toBeNull();
expect(updatedTokenSet).toEqual({
expect(updatedTokenSet?.tokenSet).toEqual({
accessToken: DEFAULT.accessToken,
refreshToken: "rt_456",
expiresAt: expect.any(Number)
Expand Down
92 changes: 70 additions & 22 deletions src/server/auth-client.ts
Copy link
Contributor Author

@tusharpandey13 tusharpandey13 Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • getTokenSet now returns tokenSet and parsed user object from id_token claims on success.
  • handleAccessToken updated to set session if user is avaialble from getTokenSet
  • A new method, finalizeSession was added that calls beforeSessionSaved hook if supplied else fitlers the ID token claims in session.user using the default filtering rules.

Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
LogoutToken,
SessionData,
StartInteractiveLoginOptions,
TokenSet
TokenSet,
User
} from "../types/index.js";
import {
ensureNoLeadingSlash,
Expand Down Expand Up @@ -577,18 +578,9 @@ export class AuthClient {

const res = await this.onCallback(null, onCallbackCtx, session);

if (this.beforeSessionSaved) {
const updatedSession = await this.beforeSessionSaved(
session,
oidcRes.id_token ?? null
);
session = {
...updatedSession,
internal: session.internal
};
} else {
session.user = filterDefaultIdTokenClaims(idTokenClaims);
}
// call beforeSessionSaved callback if present
// if not then filter id_token claims with default rules
session = await this.finalizeSession(session, oidcRes.id_token);

await this.sessionStore.set(req.cookies, res.cookies, session, true);
addCacheControlHeadersForSession(res);
Expand Down Expand Up @@ -633,7 +625,9 @@ export class AuthClient {
);
}

const [error, updatedTokenSet] = await this.getTokenSet(session.tokenSet);
const [error, getTokenSetResponse] = await this.getTokenSet(
session.tokenSet
);

if (error) {
return NextResponse.json(
Expand All @@ -648,6 +642,9 @@ export class AuthClient {
}
);
}

const { tokenSet: updatedTokenSet, idTokenClaims } = getTokenSetResponse;

const res = NextResponse.json({
token: updatedTokenSet.accessToken,
scope: updatedTokenSet.scope,
Expand All @@ -656,11 +653,20 @@ export class AuthClient {

if (
updatedTokenSet.accessToken !== session.tokenSet.accessToken ||
updatedTokenSet.refreshToken !== session.tokenSet.refreshToken ||
updatedTokenSet.expiresAt !== session.tokenSet.expiresAt
updatedTokenSet.expiresAt !== session.tokenSet.expiresAt ||
updatedTokenSet.refreshToken !== session.tokenSet.refreshToken
) {
if (idTokenClaims) {
session.user = idTokenClaims as User;
}
// call beforeSessionSaved callback if present
// if not then filter id_token claims with default rules
const finalSession = await this.finalizeSession(
session,
updatedTokenSet.idToken
);
await this.sessionStore.set(req.cookies, res.cookies, {
...session,
...finalSession,
tokenSet: updatedTokenSet
});
addCacheControlHeadersForSession(res);
Expand Down Expand Up @@ -710,13 +716,17 @@ export class AuthClient {
}

/**
* getTokenSet returns a valid token set. If the access token has expired, it will attempt to
* refresh it using the refresh token, if available.
* Retrieves OAuth token sets, handling token refresh when necessary or if forced.
*
* @returns A tuple containing either:
* - `[SdkError, null]` if an error occurred (missing refresh token, discovery failure, or refresh failure)
* - `[null, {tokenSet, idTokenClaims}]` if a new token was retrieved, containing the new token set ID token claims
* - `[null, {tokenSet, }]` if token refresh was not done and existing token was returned
*/
async getTokenSet(
tokenSet: TokenSet,
forceRefresh?: boolean | undefined
): Promise<[null, TokenSet] | [SdkError, null]> {
): Promise<[null, GetTokenSetResponse] | [SdkError, null]> {
// the access token has expired but we do not have a refresh token
if (!tokenSet.refreshToken && tokenSet.expiresAt <= Date.now() / 1000) {
return [
Expand Down Expand Up @@ -771,6 +781,7 @@ export class AuthClient {
];
}

const idTokenClaims = oauth.getValidatedIdTokenClaims(oauthRes)!;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since oauthRes: oauth.TokenEndpointResponse is available here, parse the id_token claims here.

const accessTokenExpiresAt =
Math.floor(Date.now() / 1000) + Number(oauthRes.expires_in);

Expand All @@ -789,11 +800,17 @@ export class AuthClient {
updatedTokenSet.refreshToken = tokenSet.refreshToken;
}

return [null, updatedTokenSet];
return [
null,
{
tokenSet: updatedTokenSet,
idTokenClaims: idTokenClaims
}
];
}
}

return [null, tokenSet];
return [null, { tokenSet, idTokenClaims: undefined }];
}

private async discoverAuthorizationServerMetadata(): Promise<
Expand Down Expand Up @@ -1161,6 +1178,32 @@ export class AuthClient {

return [null, connectionTokenSet] as [null, ConnectionTokenSet];
}

/**
* Filters and processes ID token claims for a session.
*
* If a `beforeSessionSaved` callback is configured, it will be invoked to allow
* custom processing of the session and ID token. Otherwise, default filtering
* will be applied to remove standard ID token claims from the user object.
*/
async finalizeSession(
session: SessionData,
idToken?: string
): Promise<SessionData> {
if (this.beforeSessionSaved) {
const updatedSession = await this.beforeSessionSaved(
session,
idToken ?? null
);
session = {
...updatedSession,
internal: session.internal
};
} else {
session.user = filterDefaultIdTokenClaims(session.user);
}
return session;
}
Comment on lines +1189 to +1206
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finalizeSession was added that calls beforeSessionSaved hook if supplied else fitlers the ID token claims in session.user using the default filtering rules.

This logic was duplicated at 3 places so moved this to a seperate method. Also, moving to a method was necessary to remove this logic from client.ts

}

const encodeBase64 = (input: string) => {
Expand All @@ -1175,3 +1218,8 @@ const encodeBase64 = (input: string) => {
}
return btoa(arr.join(""));
};

type GetTokenSetResponse = {
tokenSet: TokenSet;
idTokenClaims?: { [key: string]: any };
};
21 changes: 15 additions & 6 deletions src/server/client.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAccessToken changed similar to handleAccessToken above.

Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import {
AccessTokenError,
AccessTokenErrorCode,
AccessTokenForConnectionError,
AccessTokenForConnectionErrorCode,
AccessTokenForConnectionErrorCode
} from "../errors/index.js";

import {
AccessTokenForConnectionOptions,
AuthorizationParameters,
SessionData,
SessionDataStore,
StartInteractiveLoginOptions
StartInteractiveLoginOptions,
User
} from "../types/index.js";
import {
AuthClient,
Expand Down Expand Up @@ -420,23 +420,32 @@ export class Auth0Client {
);
}

const [error, tokenSet] = await this.authClient.getTokenSet(
const [error, getTokenSetResponse] = await this.authClient.getTokenSet(
session.tokenSet,
options.refresh
);
if (error) {
throw error;
}

const { tokenSet, idTokenClaims } = getTokenSetResponse;
// update the session with the new token set, if necessary
if (
tokenSet.accessToken !== session.tokenSet.accessToken ||
tokenSet.expiresAt !== session.tokenSet.expiresAt ||
tokenSet.refreshToken !== session.tokenSet.refreshToken
) {
if (idTokenClaims) {
session.user = idTokenClaims as User;
}
// call beforeSessionSaved callback if present
// if not then filter id_token claims with default rules
const finalSession = await this.authClient.finalizeSession(
session,
tokenSet.idToken
);
await this.saveToSession(
{
...session,
...finalSession,
tokenSet
},
req,
Expand Down
Loading