Skip to content

Commit 4d58651

Browse files
fix: skip MCD session backfill in static mode (#2618)
1 parent c4b3920 commit 4d58651

4 files changed

Lines changed: 462 additions & 26 deletions

File tree

src/server/auth-client.test.ts

Lines changed: 125 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -811,11 +811,9 @@ ca/T0LLtgmbMmxSv/MmzIg==
811811
},
812812
internal: {
813813
sid: DEFAULT.sid,
814-
createdAt: expect.any(Number),
815-
mcd: {
816-
domain: DEFAULT.domain,
817-
issuer: `https://${DEFAULT.domain}/`
818-
}
814+
createdAt: expect.any(Number)
815+
// No mcd field in static mode — backfill is skipped to avoid
816+
// unnecessary session growth and cookie chunking (#2595)
819817
}
820818
})
821819
);
@@ -5120,11 +5118,9 @@ ca/T0LLtgmbMmxSv/MmzIg==
51205118
},
51215119
internal: {
51225120
sid: expect.any(String),
5123-
createdAt: expect.any(Number),
5124-
mcd: {
5125-
domain: DEFAULT.domain,
5126-
issuer: `https://${DEFAULT.domain}/`
5127-
}
5121+
createdAt: expect.any(Number)
5122+
// No mcd field in static mode (no provider) — callback only adds
5123+
// mcd in resolver mode per DD-2 (zero-overhead static mode)
51285124
}
51295125
});
51305126
const expectedContext = expect.objectContaining({
@@ -9347,6 +9343,12 @@ ca/T0LLtgmbMmxSv/MmzIg==
93479343
getAll: vi.fn().mockReturnValue([])
93489344
});
93499345

9346+
// Backfill only runs in resolver mode. Provide a minimal mock provider
9347+
// so that `this.provider?.isResolverMode` evaluates to true.
9348+
const mockResolverProvider = {
9349+
isResolverMode: true
9350+
} as any;
9351+
93509352
it("should infer domain from idToken iss claim when no mcd field exists", async () => {
93519353
const secret = await generateSecret(32);
93529354
const transactionStore = new TransactionStore({
@@ -9391,7 +9393,8 @@ ca/T0LLtgmbMmxSv/MmzIg==
93919393
secret,
93929394
appBaseUrl: DEFAULT.appBaseUrl,
93939395
routes: getDefaultRoutes(),
9394-
fetch: getMockAuthorizationServer()
9396+
fetch: getMockAuthorizationServer(),
9397+
provider: mockResolverProvider
93959398
});
93969399

93979400
const result = await authClient.getSessionWithDomainCheck(
@@ -9443,7 +9446,8 @@ ca/T0LLtgmbMmxSv/MmzIg==
94439446
secret,
94449447
appBaseUrl: DEFAULT.appBaseUrl,
94459448
routes: getDefaultRoutes(),
9446-
fetch: getMockAuthorizationServer()
9449+
fetch: getMockAuthorizationServer(),
9450+
provider: mockResolverProvider
94479451
});
94489452

94499453
const result = await authClient.getSessionWithDomainCheck(
@@ -9491,7 +9495,8 @@ ca/T0LLtgmbMmxSv/MmzIg==
94919495
secret,
94929496
appBaseUrl: DEFAULT.appBaseUrl,
94939497
routes: getDefaultRoutes(),
9494-
fetch: getMockAuthorizationServer()
9498+
fetch: getMockAuthorizationServer(),
9499+
provider: mockResolverProvider
94959500
});
94969501

94979502
const result = await authClient.getSessionWithDomainCheck(
@@ -9552,7 +9557,8 @@ ca/T0LLtgmbMmxSv/MmzIg==
95529557
secret,
95539558
appBaseUrl: DEFAULT.appBaseUrl,
95549559
routes: getDefaultRoutes(),
9555-
fetch: getMockAuthorizationServer()
9560+
fetch: getMockAuthorizationServer(),
9561+
provider: mockResolverProvider
95569562
});
95579563

95589564
const result = await authClient.getSessionWithDomainCheck(
@@ -9657,7 +9663,8 @@ ca/T0LLtgmbMmxSv/MmzIg==
96579663
secret,
96589664
appBaseUrl: DEFAULT.appBaseUrl,
96599665
routes: getDefaultRoutes(),
9660-
fetch: getMockAuthorizationServer()
9666+
fetch: getMockAuthorizationServer(),
9667+
provider: mockResolverProvider
96619668
});
96629669

96639670
const result = await authClient.getSessionWithDomainCheck(
@@ -9673,6 +9680,109 @@ ca/T0LLtgmbMmxSv/MmzIg==
96739680
"https://custom-domain.auth0.com/"
96749681
);
96759682
});
9683+
9684+
it("should skip backfill in static mode (no provider or static provider)", async () => {
9685+
const secret = await generateSecret(32);
9686+
const transactionStore = new TransactionStore({
9687+
secret
9688+
});
9689+
9690+
const idToken = await new jose.SignJWT({
9691+
sub: DEFAULT.sub,
9692+
sid: DEFAULT.sid
9693+
})
9694+
.setProtectedHeader({ alg: DEFAULT.alg })
9695+
.setIssuer("https://example.auth0.com/")
9696+
.setAudience(DEFAULT.clientId)
9697+
.setExpirationTime("2h")
9698+
.setIssuedAt()
9699+
.sign(DEFAULT.keyPair.privateKey);
9700+
9701+
// Pre-MCD session without mcd field
9702+
const preMCDSession = createSessionData({
9703+
tokenSet: {
9704+
accessToken: "at_123",
9705+
refreshToken: "rt_123",
9706+
expiresAt: Date.now() + 3600000,
9707+
idToken
9708+
},
9709+
internal: {
9710+
sid: DEFAULT.sid,
9711+
createdAt: Date.now()
9712+
}
9713+
});
9714+
9715+
const mockSessionStore = createMockSessionStore(preMCDSession);
9716+
9717+
// Static mode: no provider (or provider with isResolverMode=false)
9718+
const authClient = new AuthClient({
9719+
transactionStore,
9720+
sessionStore: mockSessionStore as any,
9721+
domain: "example.auth0.com",
9722+
clientId: DEFAULT.clientId,
9723+
clientSecret: DEFAULT.clientSecret,
9724+
secret,
9725+
appBaseUrl: DEFAULT.appBaseUrl,
9726+
routes: getDefaultRoutes(),
9727+
fetch: getMockAuthorizationServer()
9728+
// no provider — static mode
9729+
});
9730+
9731+
const result = await authClient.getSessionWithDomainCheck(
9732+
createMockCookies() as any
9733+
);
9734+
9735+
// Session should be returned without backfill — no mcd field added
9736+
expect(result.error).toBeNull();
9737+
expect(result.session).toBeDefined();
9738+
expect(result.exists).toBe(true);
9739+
expect(result.session?.internal.mcd).toBeUndefined();
9740+
});
9741+
9742+
it("should skip backfill when provider is in static mode", async () => {
9743+
const secret = await generateSecret(32);
9744+
const transactionStore = new TransactionStore({
9745+
secret
9746+
});
9747+
9748+
const preMCDSession = createSessionData({
9749+
tokenSet: {
9750+
accessToken: "at_123",
9751+
refreshToken: "rt_123",
9752+
expiresAt: Date.now() + 3600000
9753+
},
9754+
internal: {
9755+
sid: DEFAULT.sid,
9756+
createdAt: Date.now()
9757+
}
9758+
});
9759+
9760+
const mockSessionStore = createMockSessionStore(preMCDSession);
9761+
9762+
const staticProvider = { isResolverMode: false } as any;
9763+
const authClient = new AuthClient({
9764+
transactionStore,
9765+
sessionStore: mockSessionStore as any,
9766+
domain: "example.auth0.com",
9767+
clientId: DEFAULT.clientId,
9768+
clientSecret: DEFAULT.clientSecret,
9769+
secret,
9770+
appBaseUrl: DEFAULT.appBaseUrl,
9771+
routes: getDefaultRoutes(),
9772+
fetch: getMockAuthorizationServer(),
9773+
provider: staticProvider
9774+
});
9775+
9776+
const result = await authClient.getSessionWithDomainCheck(
9777+
createMockCookies() as any
9778+
);
9779+
9780+
// Static mode: no backfill, session returned as-is
9781+
expect(result.error).toBeNull();
9782+
expect(result.session).toBeDefined();
9783+
expect(result.exists).toBe(true);
9784+
expect(result.session?.internal.mcd).toBeUndefined();
9785+
});
96769786
});
96779787

96789788
describe("DPoP lazy validation", () => {

src/server/auth-client.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2200,9 +2200,23 @@ export class AuthClient {
22002200
};
22012201
}
22022202

2203-
// Pre-MCD session (no mcd field): infer domain from ID token's iss claim.
2204-
// This is deterministic — the same session always yields the same domain,
2205-
// eliminating the race condition where the first resolver output claims it.
2203+
// Static mode: skip backfill entirely. There is only one domain, so
2204+
// internal.mcd provides no value and adding it grows the session cookie
2205+
// (~140 bytes), which can push sessions over the MAX_CHUNK_SIZE boundary
2206+
// and trigger unnecessary chunking. This preserves DD-2 (zero-overhead
2207+
// static mode). See: https://github.com/auth0/nextjs-auth0/issues/2595
2208+
if (!this.provider?.isResolverMode) {
2209+
return {
2210+
error: null,
2211+
session,
2212+
exists: true
2213+
};
2214+
}
2215+
2216+
// Resolver mode: pre-MCD session (no mcd field) — infer domain from ID
2217+
// token's iss claim. This is deterministic — the same session always yields
2218+
// the same domain, eliminating the race condition where the first resolver
2219+
// output claims it.
22062220
let inferredDomain: string | undefined;
22072221

22082222
if (session.tokenSet.idToken) {
@@ -2216,9 +2230,7 @@ export class AuthClient {
22162230
}
22172231
}
22182232

2219-
// Fallback: use current AuthClient's domain.
2220-
// Static mode: always correct (only one domain).
2221-
// Resolver mode: last resort if idToken absent.
2233+
// Fallback: use current AuthClient's domain (last resort if idToken absent)
22222234
const domain = inferredDomain ?? this.domain;
22232235

22242236
session.internal = session.internal || {};

0 commit comments

Comments
 (0)