From 9a404eb616148b8579d500ef8b59abcdf0a65895 Mon Sep 17 00:00:00 2001 From: Tushar Pandey Date: Mon, 23 Jun 2025 15:48:19 +0530 Subject: [PATCH 1/8] getTokenSet now also returns updated id_token claims information (filtered) in the case of a token update using the refresh token --- src/server/auth-client.ts | 31 +++++++++++++++++++++---------- src/server/client.ts | 14 +++++--------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/server/auth-client.ts b/src/server/auth-client.ts index 89f825f7..a030aab9 100644 --- a/src/server/auth-client.ts +++ b/src/server/auth-client.ts @@ -25,7 +25,8 @@ import { LogoutToken, SessionData, StartInteractiveLoginOptions, - TokenSet + TokenSet, + User } from "../types/index.js"; import { ensureNoLeadingSlash, @@ -633,7 +634,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( @@ -648,17 +651,17 @@ export class AuthClient { } ); } + + const { tokenSet: updatedTokenSet, user } = getTokenSetResponse; + const res = NextResponse.json({ token: updatedTokenSet.accessToken, scope: updatedTokenSet.scope, expires_at: updatedTokenSet.expiresAt }); - if ( - updatedTokenSet.accessToken !== session.tokenSet.accessToken || - updatedTokenSet.refreshToken !== session.tokenSet.refreshToken || - updatedTokenSet.expiresAt !== session.tokenSet.expiresAt - ) { + if (user) { + session.user = user; await this.sessionStore.set(req.cookies, res.cookies, { ...session, tokenSet: updatedTokenSet @@ -716,7 +719,7 @@ export class AuthClient { 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 [ @@ -771,6 +774,9 @@ export class AuthClient { ]; } + const idTokenClaims = oauth.getValidatedIdTokenClaims(oauthRes)!; + const filteredClaims = filterDefaultIdTokenClaims(idTokenClaims); + const accessTokenExpiresAt = Math.floor(Date.now() / 1000) + Number(oauthRes.expires_in); @@ -789,11 +795,11 @@ export class AuthClient { updatedTokenSet.refreshToken = tokenSet.refreshToken; } - return [null, updatedTokenSet]; + return [null, { tokenSet: updatedTokenSet, user: filteredClaims }]; } } - return [null, tokenSet]; + return [null, { tokenSet }]; } private async discoverAuthorizationServerMetadata(): Promise< @@ -1175,3 +1181,8 @@ const encodeBase64 = (input: string) => { } return btoa(arr.join("")); }; + +export type GetTokenSetResponse = { + tokenSet: TokenSet; + user?: User; +}; diff --git a/src/server/client.ts b/src/server/client.ts index db449c6b..181f2770 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -7,9 +7,8 @@ import { AccessTokenError, AccessTokenErrorCode, AccessTokenForConnectionError, - AccessTokenForConnectionErrorCode, + AccessTokenForConnectionErrorCode } from "../errors/index.js"; - import { AccessTokenForConnectionOptions, AuthorizationParameters, @@ -420,20 +419,17 @@ 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, user } = 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 (user) { + session.user = user; await this.saveToSession( { ...session, From ef2f1a8f5b383d58e25de66d9844c53f3c98c5a1 Mon Sep 17 00:00:00 2001 From: Tushar Pandey Date: Mon, 23 Jun 2025 17:29:36 +0530 Subject: [PATCH 2/8] add beforeSessionSaved hook call to access token methods; return hasTokenSetsChanged from getTokenSet --- src/server/auth-client.ts | 35 +++++++++++++++++++++++++++++------ src/server/client.ts | 22 ++++++++++++++++++---- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/server/auth-client.ts b/src/server/auth-client.ts index a030aab9..ccc4e31b 100644 --- a/src/server/auth-client.ts +++ b/src/server/auth-client.ts @@ -652,7 +652,11 @@ export class AuthClient { ); } - const { tokenSet: updatedTokenSet, user } = getTokenSetResponse; + const { + tokenSet: updatedTokenSet, + hasTokenSetChanged, + user + } = getTokenSetResponse; const res = NextResponse.json({ token: updatedTokenSet.accessToken, @@ -660,10 +664,21 @@ export class AuthClient { expires_at: updatedTokenSet.expiresAt }); - if (user) { - session.user = user; + if (hasTokenSetChanged) { + let finalSession = session; + finalSession.user = user!; + if (this.beforeSessionSaved) { + const updatedSession = await this.beforeSessionSaved( + finalSession, + updatedTokenSet.idToken ?? null + ); + finalSession = { + ...updatedSession, + internal: finalSession.internal + }; + } await this.sessionStore.set(req.cookies, res.cookies, { - ...session, + ...finalSession, tokenSet: updatedTokenSet }); addCacheControlHeadersForSession(res); @@ -795,11 +810,18 @@ export class AuthClient { updatedTokenSet.refreshToken = tokenSet.refreshToken; } - return [null, { tokenSet: updatedTokenSet, user: filteredClaims }]; + return [ + null, + { + tokenSet: updatedTokenSet, + hasTokenSetChanged: true, + user: filteredClaims + } + ]; } } - return [null, { tokenSet }]; + return [null, { tokenSet, hasTokenSetChanged: false }]; } private async discoverAuthorizationServerMetadata(): Promise< @@ -1184,5 +1206,6 @@ const encodeBase64 = (input: string) => { export type GetTokenSetResponse = { tokenSet: TokenSet; + hasTokenSetChanged: boolean; user?: User; }; diff --git a/src/server/client.ts b/src/server/client.ts index 181f2770..12e9e670 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -187,6 +187,7 @@ export class Auth0Client { private transactionStore: TransactionStore; private sessionStore: AbstractSessionStore; private authClient: AuthClient; + private readonly beforeSessionSaved?: BeforeSessionSavedHook; constructor(options: Auth0ClientOptions = {}) { // Extract and validate required options @@ -254,6 +255,8 @@ export class Auth0Client { cookieOptions: sessionCookieOptions }); + this.beforeSessionSaved = options.beforeSessionSaved; + this.authClient = new AuthClient({ transactionStore: this.transactionStore, sessionStore: this.sessionStore, @@ -426,13 +429,24 @@ export class Auth0Client { if (error) { throw error; } - const { tokenSet, user } = getTokenSetResponse; + const { tokenSet, hasTokenSetChanged, user } = getTokenSetResponse; // update the session with the new token set, if necessary - if (user) { - session.user = user; + if (hasTokenSetChanged) { + let finalSession = session; + finalSession.user = user!; + if (this.beforeSessionSaved) { + const updatedSession = await this.beforeSessionSaved( + finalSession, + tokenSet.idToken ?? null + ); + finalSession = { + ...updatedSession, + internal: finalSession.internal + }; + } await this.saveToSession( { - ...session, + ...finalSession, tokenSet }, req, From 85d782f5819e8e4d3db585107858dcfeda7c156c Mon Sep 17 00:00:00 2001 From: Tushar Pandey Date: Mon, 23 Jun 2025 17:36:10 +0530 Subject: [PATCH 3/8] make claims filtering conditional based on beforeSessionSaved hook --- src/server/auth-client.ts | 7 ++++--- src/server/client.ts | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/server/auth-client.ts b/src/server/auth-client.ts index ccc4e31b..1d0f3453 100644 --- a/src/server/auth-client.ts +++ b/src/server/auth-client.ts @@ -676,7 +676,10 @@ export class AuthClient { ...updatedSession, internal: finalSession.internal }; + } else { + finalSession.user = filterDefaultIdTokenClaims(finalSession.user); } + await this.sessionStore.set(req.cookies, res.cookies, { ...finalSession, tokenSet: updatedTokenSet @@ -790,8 +793,6 @@ export class AuthClient { } const idTokenClaims = oauth.getValidatedIdTokenClaims(oauthRes)!; - const filteredClaims = filterDefaultIdTokenClaims(idTokenClaims); - const accessTokenExpiresAt = Math.floor(Date.now() / 1000) + Number(oauthRes.expires_in); @@ -815,7 +816,7 @@ export class AuthClient { { tokenSet: updatedTokenSet, hasTokenSetChanged: true, - user: filteredClaims + user: idTokenClaims } ]; } diff --git a/src/server/client.ts b/src/server/client.ts index 12e9e670..9df170a7 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -34,6 +34,7 @@ import { TransactionCookieOptions, TransactionStore } from "./transaction-store.js"; +import { filterDefaultIdTokenClaims } from "./user.js"; export interface Auth0ClientOptions { // authorization server configuration @@ -443,6 +444,8 @@ export class Auth0Client { ...updatedSession, internal: finalSession.internal }; + } else { + finalSession.user = filterDefaultIdTokenClaims(finalSession.user); } await this.saveToSession( { From 150aee2049d9668d790142061aef0d86f949e9a2 Mon Sep 17 00:00:00 2001 From: Tushar Pandey Date: Mon, 23 Jun 2025 18:23:35 +0530 Subject: [PATCH 4/8] revert to old style of checking if token has updated --- src/server/auth-client.ts | 20 ++++++++++---------- src/server/client.ts | 12 +++++++++--- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/server/auth-client.ts b/src/server/auth-client.ts index 1d0f3453..466f5e03 100644 --- a/src/server/auth-client.ts +++ b/src/server/auth-client.ts @@ -652,11 +652,7 @@ export class AuthClient { ); } - const { - tokenSet: updatedTokenSet, - hasTokenSetChanged, - user - } = getTokenSetResponse; + const { tokenSet: updatedTokenSet, user } = getTokenSetResponse; const res = NextResponse.json({ token: updatedTokenSet.accessToken, @@ -664,9 +660,15 @@ export class AuthClient { expires_at: updatedTokenSet.expiresAt }); - if (hasTokenSetChanged) { + if ( + updatedTokenSet.accessToken !== session.tokenSet.accessToken || + updatedTokenSet.expiresAt !== session.tokenSet.expiresAt || + updatedTokenSet.refreshToken !== session.tokenSet.refreshToken + ) { let finalSession = session; - finalSession.user = user!; + if (user) { + finalSession.user = user!; + } if (this.beforeSessionSaved) { const updatedSession = await this.beforeSessionSaved( finalSession, @@ -815,14 +817,13 @@ export class AuthClient { null, { tokenSet: updatedTokenSet, - hasTokenSetChanged: true, user: idTokenClaims } ]; } } - return [null, { tokenSet, hasTokenSetChanged: false }]; + return [null, { tokenSet }]; } private async discoverAuthorizationServerMetadata(): Promise< @@ -1207,6 +1208,5 @@ const encodeBase64 = (input: string) => { export type GetTokenSetResponse = { tokenSet: TokenSet; - hasTokenSetChanged: boolean; user?: User; }; diff --git a/src/server/client.ts b/src/server/client.ts index 9df170a7..b063a464 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -430,11 +430,17 @@ export class Auth0Client { if (error) { throw error; } - const { tokenSet, hasTokenSetChanged, user } = getTokenSetResponse; + const { tokenSet, user } = getTokenSetResponse; // update the session with the new token set, if necessary - if (hasTokenSetChanged) { + if ( + tokenSet.accessToken !== session.tokenSet.accessToken || + tokenSet.expiresAt !== session.tokenSet.expiresAt || + tokenSet.refreshToken !== session.tokenSet.refreshToken + ) { let finalSession = session; - finalSession.user = user!; + if (user) { + finalSession.user = user!; + } if (this.beforeSessionSaved) { const updatedSession = await this.beforeSessionSaved( finalSession, From ca45b9349be191c7aa9e010054b1f91de25de01c Mon Sep 17 00:00:00 2001 From: Tushar Pandey Date: Mon, 23 Jun 2025 22:39:50 +0530 Subject: [PATCH 5/8] update tests to include the updated id_token and session.user scenario --- src/server/auth-client.test.ts | 6 +- src/server/get-access-token.test.ts | 149 ++++++++++++++-------------- 2 files changed, 76 insertions(+), 79 deletions(-) diff --git a/src/server/auth-client.test.ts b/src/server/auth-client.test.ts index 00fe8bad..7ca5541d 100644 --- a/src/server/auth-client.test.ts +++ b/src/server/auth-client.test.ts @@ -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 () => { @@ -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) @@ -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) diff --git a/src/server/get-access-token.test.ts b/src/server/get-access-token.test.ts index 1e7450e6..6b6932fc 100644 --- a/src/server/get-access-token.test.ts +++ b/src/server/get-access-token.test.ts @@ -13,62 +13,67 @@ import { vi } from "vitest"; -import { SessionData, TokenSet } from "../types/index.js"; +import { SessionData } from "../types/index.js"; import { Auth0Client } from "./client.js"; // Basic constants for testing -const DEFAULT = { - domain: "https://op.example.com", +const domain = "https://auth0.local"; +const alg = "RS256"; +const sub = "test-sub"; +const sid = "test-sid"; +const scope = "openid profile email offline_access"; + +const testAuth0ClientConfig = { + domain, clientId: "test-client-id", clientSecret: "test-client-secret", appBaseUrl: "https://example.org", - secret: "test-secret-long-enough-for-hs256-test-secret-long-enough-for-hs256", - alg: "RS256", - sub: "test-sub", - sid: "test-sid", - scope: "openid profile email offline_access" + secret: "test-secret-long-enough-for-hs256-test-secret-long-enough-for-hs256" }; -const initialTokenSetBase = { - accessToken: "test-access-token", - refreshToken: "test-refresh-token", - idToken: "test-id-token", - scope: DEFAULT.scope -}; - -const authClientConfig = { - domain: DEFAULT.domain, - clientId: DEFAULT.clientId, - clientSecret: DEFAULT.clientSecret, - appBaseUrl: DEFAULT.appBaseUrl, - secret: DEFAULT.secret -}; - -// msw server setup let keyPair: jose.GenerateKeyPairResult; + const refreshedAccessToken = "msw-refreshed-access-token"; const refreshedRefreshToken = "msw-refreshed-refresh-token"; const refreshedExpiresIn = 3600; -const issuer = DEFAULT.domain; -const audience = DEFAULT.clientId; +const issuer = domain; +const audience = testAuth0ClientConfig.clientId; +const initialName = "initialName"; +const updatedName = "updatedName"; + +const generateToken = async (claims?: any) => + await new jose.SignJWT({ + sid, + sub, + auth_time: Math.floor(Date.now() / 1000), + nonce: "nonce-value", + jti: Date.now().toString(), + ...(claims && { ...claims }) + }) + .setProtectedHeader({ alg }) + .setIssuer(issuer) + .setAudience(audience) + .setIssuedAt() + .setExpirationTime("1h") + .sign(keyPair.privateKey); const handlers = [ // OIDC Discovery Endpoint - http.get(`${DEFAULT.domain}/.well-known/openid-configuration`, () => { + http.get(`${domain}/.well-known/openid-configuration`, () => { return HttpResponse.json({ issuer: issuer, - token_endpoint: `${DEFAULT.domain}/oauth/token`, - jwks_uri: `${DEFAULT.domain}/.well-known/jwks.json` + token_endpoint: `${domain}/oauth/token`, + jwks_uri: `${domain}/.well-known/jwks.json` }); }), // JWKS Endpoint - http.get(`${DEFAULT.domain}/.well-known/jwks.json`, async () => { + http.get(`${domain}/.well-known/jwks.json`, async () => { const jwk = await jose.exportJWK(keyPair.publicKey); return HttpResponse.json({ keys: [jwk] }); }), // Token Endpoint (for refresh token grant) http.post( - `${DEFAULT.domain}/oauth/token`, + `${domain}/oauth/token`, async ({ request }: { request: Request }) => { const body = await request.formData(); @@ -76,27 +81,15 @@ const handlers = [ body.get("grant_type") === "refresh_token" && body.get("refresh_token") ) { - // Generate a new ID token for the refreshed set - const newIdToken = await new jose.SignJWT({ - sid: DEFAULT.sid, - sub: DEFAULT.sub, - auth_time: Math.floor(Date.now() / 1000), - nonce: "nonce-value" // Example nonce - }) - .setProtectedHeader({ alg: DEFAULT.alg }) - .setIssuer(issuer) - .setAudience(audience) - .setIssuedAt() - .setExpirationTime("1h") - .sign(keyPair.privateKey); - return HttpResponse.json({ access_token: refreshedAccessToken, refresh_token: refreshedRefreshToken, - id_token: newIdToken, + id_token: await generateToken({ + name: updatedName + }), token_type: "Bearer", expires_in: refreshedExpiresIn, - scope: DEFAULT.scope // Assuming scope doesn't change on refresh + scope }); } @@ -112,7 +105,7 @@ const handlers = [ const server = setupServer(...handlers); beforeAll(async () => { - keyPair = await jose.generateKeyPair(DEFAULT.alg); + keyPair = await jose.generateKeyPair(alg); server.listen({ onUnhandledRequest: "error" }); }); afterEach(() => server.resetHandlers()); @@ -121,19 +114,20 @@ afterAll(() => server.close()); /** * Creates initial session data for tests. */ -function createInitialSession(): SessionData { - // Use a VALID (non-expired) initial token - const initialExpiresAt = Math.floor(Date.now() / 1000) + 3600; // Expires in 1 hour - const initialTokenSet: TokenSet = { - ...initialTokenSetBase, // Spread the base token set from the new constant - expiresAt: initialExpiresAt // Add the dynamic expiration time - }; - const initialSession: SessionData = { - user: { sub: DEFAULT.sub }, - tokenSet: initialTokenSet, - internal: { sid: DEFAULT.sid, createdAt: Date.now() / 1000 } +async function createInitialSession(): Promise { + return { + user: { sub, name: initialName }, + tokenSet: { + accessToken: "test-access-token", + refreshToken: "test-refresh-token", + idToken: await generateToken({ + name: initialName + }), + scope, + expiresAt: Math.floor(Date.now() / 1000) + 3600 // Expires in 1 hour + }, + internal: { sid, createdAt: Date.now() / 1000 } }; - return initialSession; } describe("Auth0Client - getAccessToken", () => { @@ -142,12 +136,19 @@ describe("Auth0Client - getAccessToken", () => { beforeEach(async () => { // Instantiate Auth0Client normally, it will use intercepted fetch - auth0Client = new Auth0Client(authClientConfig); + auth0Client = new Auth0Client(testAuth0ClientConfig); // Mock saveToSession to avoid cookie/request context issues mockSaveToSession = vi .spyOn(Auth0Client.prototype as any, "saveToSession") .mockResolvedValue(undefined); // Mock successful save + + const initialSession = await createInitialSession(); + + // Mock getSession specifically for this test + vi.spyOn(Auth0Client.prototype as any, "getSession").mockResolvedValue( + initialSession + ); }); afterEach(() => { @@ -160,15 +161,10 @@ describe("Auth0Client - getAccessToken", () => { * it refreshes the token. */ it("should refresh token and save session for pages-router overload when refresh is true (with valid token)", async () => { - const initialSession = createInitialSession(); - - // Mock getSession specifically for this test - vi.spyOn(Auth0Client.prototype as any, "getSession").mockResolvedValue( - initialSession - ); - // Pages router overload requires req/res objects - const mockReq = new NextRequest(`https://${DEFAULT.appBaseUrl}/api/test`); + const mockReq = new NextRequest( + `https://${testAuth0ClientConfig.appBaseUrl}/api/test` + ); const mockRes = new NextResponse(); // --- Execution --- @@ -188,6 +184,10 @@ describe("Auth0Client - getAccessToken", () => { // The '0' precision checks for equality at the integer second level. expect(result?.expiresAt).toBeCloseTo(expectedExpiresAtRough, 0); expect(mockSaveToSession).toHaveBeenCalledOnce(); + + // Verify user profile data is updated in saved session + const savedSessionData = mockSaveToSession.mock.calls[0][0] as SessionData; + expect(savedSessionData.user.name).toBe(updatedName); }); /** @@ -196,13 +196,6 @@ describe("Auth0Client - getAccessToken", () => { * it refreshes the token. */ it("should refresh token for app-router overload when refresh is true (with valid token)", async () => { - const initialSession = createInitialSession(); - - // Mock getSession specifically for this test - vi.spyOn(Auth0Client.prototype as any, "getSession").mockResolvedValue( - initialSession - ); - // --- Execution --- const result = await auth0Client.getAccessToken({ refresh: true @@ -217,5 +210,9 @@ describe("Auth0Client - getAccessToken", () => { expect(result?.expiresAt).toBeCloseTo(expectedExpiresAtRough, 0); expect(mockSaveToSession).toHaveBeenCalledOnce(); + + // Verify user profile data is updated in saved session + const savedSessionData = mockSaveToSession.mock.calls[0][0] as SessionData; + expect(savedSessionData.user.name).toBe(updatedName); }); }); From add285e439ec96b2729dedeb6fe9de5cc5e55f0f Mon Sep 17 00:00:00 2001 From: Tushar Pandey Date: Tue, 24 Jun 2025 16:54:47 +0530 Subject: [PATCH 6/8] move beforeSessionSave logic to auth-client, add doc --- src/server/auth-client.ts | 102 +++++++++++++++++++++++++------------- src/server/client.ts | 27 +++------- 2 files changed, 76 insertions(+), 53 deletions(-) diff --git a/src/server/auth-client.ts b/src/server/auth-client.ts index 466f5e03..e6238337 100644 --- a/src/server/auth-client.ts +++ b/src/server/auth-client.ts @@ -578,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); @@ -652,7 +643,8 @@ export class AuthClient { ); } - const { tokenSet: updatedTokenSet, user } = getTokenSetResponse; + const { tokenSet: updatedTokenSet, idTokenClaims: user } = + getTokenSetResponse; const res = NextResponse.json({ token: updatedTokenSet.accessToken, @@ -665,23 +657,15 @@ export class AuthClient { updatedTokenSet.expiresAt !== session.tokenSet.expiresAt || updatedTokenSet.refreshToken !== session.tokenSet.refreshToken ) { - let finalSession = session; if (user) { - finalSession.user = user!; + session.user = user!; } - if (this.beforeSessionSaved) { - const updatedSession = await this.beforeSessionSaved( - finalSession, - updatedTokenSet.idToken ?? null - ); - finalSession = { - ...updatedSession, - internal: finalSession.internal - }; - } else { - finalSession.user = filterDefaultIdTokenClaims(finalSession.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, { ...finalSession, tokenSet: updatedTokenSet @@ -733,8 +717,23 @@ 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 TokenSet and ID token claims, if available. + * + * @param tokenSet - The current token set containing access token, refresh token, and expiration info + * @param forceRefresh - Optional flag to force token refresh even if the access token hasn't expired + * + * @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 + * + * @remarks + * - If the access token has expired and no refresh token is available, returns a MISSING_REFRESH_TOKEN error + * - If a refresh token is present and the token is expired (or forceRefresh is true), attempts to refresh + * - Retrieves and returns id_token claims in the case of RT flow. + * - Handles refresh token rotation by updating the refresh token if a new one is provided + * - Maintains session lifetime by preserving the original `iat` claim from the existing token set */ async getTokenSet( tokenSet: TokenSet, @@ -817,13 +816,13 @@ export class AuthClient { null, { tokenSet: updatedTokenSet, - user: idTokenClaims + idTokenClaims: idTokenClaims } ]; } } - return [null, { tokenSet }]; + return [null, { tokenSet, idTokenClaims: undefined }]; } private async discoverAuthorizationServerMetadata(): Promise< @@ -1191,6 +1190,41 @@ 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. + * + * @param session - The session data object containing user information and internal metadata + * @param idToken - The raw ID token string from the authentication provider + * @returns Promise that resolves when the session has been processed and filtered + * + * @remarks + * This method modifies the session object in place. When using a custom + * `beforeSessionSaved` callback, the internal session metadata is preserved + * and merged with the updated session data. + */ + async finalizeSession( + session: SessionData, + idToken?: string + ): Promise { + if (this.beforeSessionSaved) { + const updatedSession = await this.beforeSessionSaved( + session, + idToken ?? null + ); + session = { + ...updatedSession, + internal: session.internal + }; + } else { + session.user = filterDefaultIdTokenClaims(session.user); + } + return session; + } } const encodeBase64 = (input: string) => { @@ -1206,7 +1240,7 @@ const encodeBase64 = (input: string) => { return btoa(arr.join("")); }; -export type GetTokenSetResponse = { +type GetTokenSetResponse = { tokenSet: TokenSet; - user?: User; + idTokenClaims?: User; }; diff --git a/src/server/client.ts b/src/server/client.ts index b063a464..dc15fc92 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -34,7 +34,6 @@ import { TransactionCookieOptions, TransactionStore } from "./transaction-store.js"; -import { filterDefaultIdTokenClaims } from "./user.js"; export interface Auth0ClientOptions { // authorization server configuration @@ -188,7 +187,6 @@ export class Auth0Client { private transactionStore: TransactionStore; private sessionStore: AbstractSessionStore; private authClient: AuthClient; - private readonly beforeSessionSaved?: BeforeSessionSavedHook; constructor(options: Auth0ClientOptions = {}) { // Extract and validate required options @@ -256,8 +254,6 @@ export class Auth0Client { cookieOptions: sessionCookieOptions }); - this.beforeSessionSaved = options.beforeSessionSaved; - this.authClient = new AuthClient({ transactionStore: this.transactionStore, sessionStore: this.sessionStore, @@ -430,29 +426,22 @@ export class Auth0Client { if (error) { throw error; } - const { tokenSet, user } = getTokenSetResponse; + const { tokenSet, idTokenClaims: user } = 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 ) { - let finalSession = session; if (user) { - finalSession.user = user!; - } - if (this.beforeSessionSaved) { - const updatedSession = await this.beforeSessionSaved( - finalSession, - tokenSet.idToken ?? null - ); - finalSession = { - ...updatedSession, - internal: finalSession.internal - }; - } else { - finalSession.user = filterDefaultIdTokenClaims(finalSession.user); + session.user = 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( { ...finalSession, From 88285965aa89afae712eae6435fabad46b09776b Mon Sep 17 00:00:00 2001 From: Tushar Pandey Date: Wed, 25 Jun 2025 10:40:34 +0530 Subject: [PATCH 7/8] trim redundant info in docstrings; remove un-necessary non-null assertion operator --- src/server/auth-client.ts | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/server/auth-client.ts b/src/server/auth-client.ts index e6238337..b9188574 100644 --- a/src/server/auth-client.ts +++ b/src/server/auth-client.ts @@ -658,7 +658,7 @@ export class AuthClient { updatedTokenSet.refreshToken !== session.tokenSet.refreshToken ) { if (user) { - session.user = user!; + session.user = user; } // call beforeSessionSaved callback if present // if not then filter id_token claims with default rules @@ -718,22 +718,11 @@ export class AuthClient { /** * Retrieves OAuth token sets, handling token refresh when necessary or if forced. - * Returns TokenSet and ID token claims, if available. - * - * @param tokenSet - The current token set containing access token, refresh token, and expiration info - * @param forceRefresh - Optional flag to force token refresh even if the access token hasn't expired * * @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 - * - * @remarks - * - If the access token has expired and no refresh token is available, returns a MISSING_REFRESH_TOKEN error - * - If a refresh token is present and the token is expired (or forceRefresh is true), attempts to refresh - * - Retrieves and returns id_token claims in the case of RT flow. - * - Handles refresh token rotation by updating the refresh token if a new one is provided - * - Maintains session lifetime by preserving the original `iat` claim from the existing token set */ async getTokenSet( tokenSet: TokenSet, @@ -1197,15 +1186,6 @@ export class AuthClient { * 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. - * - * @param session - The session data object containing user information and internal metadata - * @param idToken - The raw ID token string from the authentication provider - * @returns Promise that resolves when the session has been processed and filtered - * - * @remarks - * This method modifies the session object in place. When using a custom - * `beforeSessionSaved` callback, the internal session metadata is preserved - * and merged with the updated session data. */ async finalizeSession( session: SessionData, From e8ac4a89301d449563aa842d5da99d9030554904 Mon Sep 17 00:00:00 2001 From: Tushar Pandey Date: Wed, 25 Jun 2025 10:48:17 +0530 Subject: [PATCH 8/8] change type of idTokenClaims in GetTokenSetResponse to { [key: string]: any } --- src/server/auth-client.ts | 9 ++++----- src/server/client.ts | 9 +++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/server/auth-client.ts b/src/server/auth-client.ts index b9188574..378fa49d 100644 --- a/src/server/auth-client.ts +++ b/src/server/auth-client.ts @@ -643,8 +643,7 @@ export class AuthClient { ); } - const { tokenSet: updatedTokenSet, idTokenClaims: user } = - getTokenSetResponse; + const { tokenSet: updatedTokenSet, idTokenClaims } = getTokenSetResponse; const res = NextResponse.json({ token: updatedTokenSet.accessToken, @@ -657,8 +656,8 @@ export class AuthClient { updatedTokenSet.expiresAt !== session.tokenSet.expiresAt || updatedTokenSet.refreshToken !== session.tokenSet.refreshToken ) { - if (user) { - session.user = user; + if (idTokenClaims) { + session.user = idTokenClaims as User; } // call beforeSessionSaved callback if present // if not then filter id_token claims with default rules @@ -1222,5 +1221,5 @@ const encodeBase64 = (input: string) => { type GetTokenSetResponse = { tokenSet: TokenSet; - idTokenClaims?: User; + idTokenClaims?: { [key: string]: any }; }; diff --git a/src/server/client.ts b/src/server/client.ts index dc15fc92..ad99301b 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -14,7 +14,8 @@ import { AuthorizationParameters, SessionData, SessionDataStore, - StartInteractiveLoginOptions + StartInteractiveLoginOptions, + User } from "../types/index.js"; import { AuthClient, @@ -426,15 +427,15 @@ export class Auth0Client { if (error) { throw error; } - const { tokenSet, idTokenClaims: user } = getTokenSetResponse; + 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 (user) { - session.user = user!; + if (idTokenClaims) { + session.user = idTokenClaims as User; } // call beforeSessionSaved callback if present // if not then filter id_token claims with default rules