Skip to content

Commit 324a8e1

Browse files
committed
fix: apply clock settings from dpopOptions to clientMetadata & test improvements
1 parent b742544 commit 324a8e1

2 files changed

Lines changed: 179 additions & 38 deletions

File tree

src/server/auth-client.test.ts

Lines changed: 169 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9327,18 +9327,18 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
93279327
// Clean up environment variables after each test
93289328
delete process.env[ENV_VARS.DPOP_PRIVATE_KEY];
93299329
delete process.env[ENV_VARS.DPOP_PUBLIC_KEY];
9330+
delete process.env.AUTH0_DPOP_CLOCK_SKEW;
9331+
delete process.env.AUTH0_DPOP_CLOCK_TOLERANCE;
93309332
});
93319333

9332-
it("should not validate dpopKeyPair during construction", async () => {
9334+
it("should include dpop_jkt in authorization URL when dpopKeyPair is provided", async () => {
93339335
const secret = await generateSecret(32);
93349336
const transactionStore = new TransactionStore({ secret });
93359337
const sessionStore = new StatelessSessionStore({ secret });
93369338

93379339
const { generateDpopKeyPair } = await import("../utils/dpopRetry.js");
93389340
const mockKeypair = await generateDpopKeyPair();
93399341

9340-
const warnSpy = vi.spyOn(console, "warn");
9341-
93429342
const authClient = new AuthClient({
93439343
transactionStore,
93449344
sessionStore,
@@ -9349,16 +9349,28 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
93499349
appBaseUrl: DEFAULT.appBaseUrl,
93509350
routes: getDefaultRoutes(),
93519351
useDPoP: true,
9352-
dpopKeyPair: mockKeypair
9352+
dpopKeyPair: mockKeypair,
9353+
fetch: getMockAuthorizationServer()
93539354
});
93549355

9355-
expect(authClient).toBeInstanceOf(AuthClient);
9356-
expect(warnSpy).not.toHaveBeenCalled();
9356+
const request = new NextRequest(
9357+
new URL("/auth/login", DEFAULT.appBaseUrl),
9358+
{ method: "GET" }
9359+
);
93579360

9358-
warnSpy.mockRestore();
9361+
const response = await authClient.handleLogin(request);
9362+
expect(response.status).toEqual(307);
9363+
9364+
const authorizationUrl = new URL(response.headers.get("Location")!);
9365+
9366+
// Verify DPoP is enabled (dpop_jkt parameter is present)
9367+
expect(authorizationUrl.searchParams.has("dpop_jkt")).toBe(true);
9368+
expect(authorizationUrl.searchParams.get("dpop_jkt")).toMatch(
9369+
/^[A-Za-z0-9_-]+$/
9370+
);
93599371
});
93609372

9361-
it("should load and validate DPoP keypair from environment variables when DPoP operations are triggered", async () => {
9373+
it("should load DPoP keypair from environment variables and include dpop_jkt", async () => {
93629374
process.env[ENV_VARS.DPOP_PRIVATE_KEY] = TEST_PRIVATE_KEY_PEM;
93639375
process.env[ENV_VARS.DPOP_PUBLIC_KEY] = TEST_PUBLIC_KEY_PEM;
93649376

@@ -9379,22 +9391,27 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
93799391
fetch: getMockAuthorizationServer()
93809392
});
93819393

9382-
expect(authClient).toBeInstanceOf(AuthClient);
9394+
const request = new NextRequest(
9395+
new URL("/auth/login", DEFAULT.appBaseUrl),
9396+
{ method: "GET" }
9397+
);
93839398

9384-
const warnSpy = vi.spyOn(console, "warn");
9399+
const response = await authClient.handleLogin(request);
9400+
expect(response.status).toEqual(307);
93859401

9386-
await expect(authClient.startInteractiveLogin()).resolves.toBeDefined();
9402+
const authorizationUrl = new URL(response.headers.get("Location")!);
93879403

9388-
expect(warnSpy).not.toHaveBeenCalledWith(
9389-
expect.stringContaining("Failed to load DPoP keypair")
9404+
// Verify DPoP is enabled (keypair loaded from env vars, dpop_jkt is present)
9405+
expect(authorizationUrl.searchParams.has("dpop_jkt")).toBe(true);
9406+
expect(authorizationUrl.searchParams.get("dpop_jkt")).toMatch(
9407+
/^[A-Za-z0-9_-]+$/
93909408
);
9391-
9392-
warnSpy.mockRestore();
93939409
});
93949410

93959411
it("should prioritize provided dpopKeyPair over environment variables", async () => {
9396-
process.env[ENV_VARS.DPOP_PRIVATE_KEY] = TEST_PRIVATE_KEY_PEM;
9397-
process.env[ENV_VARS.DPOP_PUBLIC_KEY] = TEST_PUBLIC_KEY_PEM;
9412+
// Set INVALID env vars to ensure they are NOT used
9413+
process.env[ENV_VARS.DPOP_PRIVATE_KEY] = "invalid-private-key";
9414+
process.env[ENV_VARS.DPOP_PUBLIC_KEY] = "invalid-public-key";
93989415

93999416
const secret = await generateSecret(32);
94009417
const transactionStore = new TransactionStore({ secret });
@@ -9417,18 +9434,33 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
94179434
fetch: getMockAuthorizationServer()
94189435
});
94199436

9420-
expect(authClient).toBeInstanceOf(AuthClient);
9421-
await expect(authClient.startInteractiveLogin()).resolves.toBeDefined();
9437+
const request = new NextRequest(
9438+
new URL("/auth/login", DEFAULT.appBaseUrl),
9439+
{ method: "GET" }
9440+
);
9441+
9442+
const response = await authClient.handleLogin(request);
9443+
expect(response.status).toEqual(307);
9444+
9445+
const authorizationUrl = new URL(response.headers.get("Location")!);
9446+
9447+
// Verify DPoP is enabled with provided keypair (not invalid env vars)
9448+
expect(authorizationUrl.searchParams.has("dpop_jkt")).toBe(true);
9449+
expect(authorizationUrl.searchParams.get("dpop_jkt")).toMatch(
9450+
/^[A-Za-z0-9_-]+$/
9451+
);
94229452
});
94239453

9424-
it("should handle invalid PEM format from environment variables during lazy validation", async () => {
9454+
it("should fall back to bearer auth when keypair not provided and environment variables contain invalid keys", async () => {
94259455
process.env[ENV_VARS.DPOP_PRIVATE_KEY] = "invalid-private-key";
94269456
process.env[ENV_VARS.DPOP_PUBLIC_KEY] = "invalid-public-key";
94279457

94289458
const secret = await generateSecret(32);
94299459
const transactionStore = new TransactionStore({ secret });
94309460
const sessionStore = new StatelessSessionStore({ secret });
94319461

9462+
const warnSpy = vi.spyOn(console, "warn");
9463+
94329464
const authClient = new AuthClient({
94339465
transactionStore,
94349466
sessionStore,
@@ -9442,11 +9474,19 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
94429474
fetch: getMockAuthorizationServer()
94439475
});
94449476

9445-
expect(authClient).toBeInstanceOf(AuthClient);
9477+
const loginRequest = new NextRequest(
9478+
new URL("/auth/login", DEFAULT.appBaseUrl),
9479+
{ method: "GET" }
9480+
);
9481+
const loginResponse = await authClient.handleLogin(loginRequest);
9482+
expect(loginResponse.status).toEqual(307);
94469483

9447-
const warnSpy = vi.spyOn(console, "warn");
9448-
await expect(authClient.startInteractiveLogin()).resolves.toBeDefined();
9484+
const authorizationUrl = new URL(loginResponse.headers.get("Location")!);
94499485

9486+
// Verify DPoP was NOT enabled (invalid keys = falls back to bearer auth)
9487+
expect(authorizationUrl.searchParams.has("dpop_jkt")).toBe(false);
9488+
9489+
// Verify warning about failed key loading
94509490
expect(warnSpy).toHaveBeenCalledWith(
94519491
expect.stringContaining(
94529492
"Failed to load DPoP keypair from environment variables"
@@ -9456,14 +9496,49 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
94569496
warnSpy.mockRestore();
94579497
});
94589498

9459-
it("should skip DPoP validation entirely when useDPoP is false", async () => {
9499+
it("should not include dpop_jkt when useDPoP is false", async () => {
94609500
process.env[ENV_VARS.DPOP_PRIVATE_KEY] = TEST_PRIVATE_KEY_PEM;
94619501
process.env[ENV_VARS.DPOP_PUBLIC_KEY] = TEST_PUBLIC_KEY_PEM;
94629502

94639503
const secret = await generateSecret(32);
94649504
const transactionStore = new TransactionStore({ secret });
94659505
const sessionStore = new StatelessSessionStore({ secret });
94669506

9507+
const authClient = new AuthClient({
9508+
transactionStore,
9509+
sessionStore,
9510+
domain: DEFAULT.domain,
9511+
clientId: DEFAULT.clientId,
9512+
clientSecret: DEFAULT.clientSecret,
9513+
secret,
9514+
appBaseUrl: DEFAULT.appBaseUrl,
9515+
routes: getDefaultRoutes(),
9516+
useDPoP: false,
9517+
fetch: getMockAuthorizationServer()
9518+
});
9519+
9520+
const request = new NextRequest(
9521+
new URL("/auth/login", DEFAULT.appBaseUrl),
9522+
{ method: "GET" }
9523+
);
9524+
9525+
const response = await authClient.handleLogin(request);
9526+
expect(response.status).toEqual(307);
9527+
9528+
const authorizationUrl = new URL(response.headers.get("Location")!);
9529+
9530+
// Verify DPoP is NOT enabled (useDPoP=false)
9531+
expect(authorizationUrl.searchParams.has("dpop_jkt")).toBe(false);
9532+
});
9533+
9534+
it("should fall back to bearer auth when only public key is in environment variables", async () => {
9535+
// Only public key set, private key missing
9536+
process.env[ENV_VARS.DPOP_PUBLIC_KEY] = TEST_PUBLIC_KEY_PEM;
9537+
9538+
const secret = await generateSecret(32);
9539+
const transactionStore = new TransactionStore({ secret });
9540+
const sessionStore = new StatelessSessionStore({ secret });
9541+
94679542
const warnSpy = vi.spyOn(console, "warn");
94689543

94699544
const authClient = new AuthClient({
@@ -9475,29 +9550,43 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
94759550
secret,
94769551
appBaseUrl: DEFAULT.appBaseUrl,
94779552
routes: getDefaultRoutes(),
9478-
useDPoP: false,
9553+
useDPoP: true,
94799554
fetch: getMockAuthorizationServer()
94809555
});
94819556

9482-
expect(authClient).toBeInstanceOf(AuthClient);
9557+
const request = new NextRequest(
9558+
new URL("/auth/login", DEFAULT.appBaseUrl),
9559+
{ method: "GET" }
9560+
);
9561+
9562+
const response = await authClient.handleLogin(request);
9563+
expect(response.status).toEqual(307);
94839564

9484-
// Trigger operations that would validate DPoP if it were enabled
9485-
await expect(authClient.startInteractiveLogin()).resolves.toBeDefined();
9565+
const authorizationUrl = new URL(response.headers.get("Location")!);
94869566

9487-
// Verify NO DPoP-related warnings were issued - validation was completely skipped
9488-
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining("DPoP"));
9489-
expect(warnSpy).not.toHaveBeenCalledWith(expect.stringContaining("dpop"));
9567+
// Verify DPoP was NOT enabled (missing private key)
9568+
expect(authorizationUrl.searchParams.has("dpop_jkt")).toBe(false);
9569+
9570+
// Verify warning about missing keypair
9571+
expect(warnSpy).toHaveBeenCalledWith(
9572+
expect.stringContaining(
9573+
"useDPoP is set to true but dpopKeyPair is not provided"
9574+
)
9575+
);
94909576

94919577
warnSpy.mockRestore();
94929578
});
94939579

9494-
it("should handle missing private key only from environment variables", async () => {
9495-
process.env[ENV_VARS.DPOP_PUBLIC_KEY] = TEST_PUBLIC_KEY_PEM;
9580+
it("should fall back to bearer auth when only private key is in environment variables", async () => {
9581+
// Only private key set, public key missing
9582+
process.env[ENV_VARS.DPOP_PRIVATE_KEY] = TEST_PRIVATE_KEY_PEM;
94969583

94979584
const secret = await generateSecret(32);
94989585
const transactionStore = new TransactionStore({ secret });
94999586
const sessionStore = new StatelessSessionStore({ secret });
95009587

9588+
const warnSpy = vi.spyOn(console, "warn");
9589+
95019590
const authClient = new AuthClient({
95029591
transactionStore,
95039592
sessionStore,
@@ -9511,12 +9600,34 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
95119600
fetch: getMockAuthorizationServer()
95129601
});
95139602

9514-
expect(authClient).toBeInstanceOf(AuthClient);
9515-
await expect(authClient.startInteractiveLogin()).resolves.toBeDefined();
9603+
const request = new NextRequest(
9604+
new URL("/auth/login", DEFAULT.appBaseUrl),
9605+
{ method: "GET" }
9606+
);
9607+
9608+
const response = await authClient.handleLogin(request);
9609+
expect(response.status).toEqual(307);
9610+
9611+
const authorizationUrl = new URL(response.headers.get("Location")!);
9612+
9613+
// Verify DPoP was NOT enabled (missing public key)
9614+
expect(authorizationUrl.searchParams.has("dpop_jkt")).toBe(false);
9615+
9616+
// Verify warning about missing keypair
9617+
expect(warnSpy).toHaveBeenCalledWith(
9618+
expect.stringContaining(
9619+
"useDPoP is set to true but dpopKeyPair is not provided"
9620+
)
9621+
);
9622+
9623+
warnSpy.mockRestore();
95169624
});
95179625

9518-
it("should handle missing public key only from environment variables", async () => {
9626+
it("should update clientMetadata with clockSkew and clockTolerance from environment variables", async () => {
95199627
process.env[ENV_VARS.DPOP_PRIVATE_KEY] = TEST_PRIVATE_KEY_PEM;
9628+
process.env[ENV_VARS.DPOP_PUBLIC_KEY] = TEST_PUBLIC_KEY_PEM;
9629+
process.env.AUTH0_DPOP_CLOCK_SKEW = "15";
9630+
process.env.AUTH0_DPOP_CLOCK_TOLERANCE = "30";
95209631

95219632
const secret = await generateSecret(32);
95229633
const transactionStore = new TransactionStore({ secret });
@@ -9535,8 +9646,28 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
95359646
fetch: getMockAuthorizationServer()
95369647
});
95379648

9538-
expect(authClient).toBeInstanceOf(AuthClient);
9539-
await expect(authClient.startInteractiveLogin()).resolves.toBeDefined();
9649+
// Before triggering DPoP operations, clientMetadata should not have the values
9650+
expect(authClient["clientMetadata"][oauth.clockSkew]).toBeUndefined();
9651+
expect(
9652+
authClient["clientMetadata"][oauth.clockTolerance]
9653+
).toBeUndefined();
9654+
9655+
const request = new NextRequest(
9656+
new URL("/auth/login", DEFAULT.appBaseUrl),
9657+
{ method: "GET" }
9658+
);
9659+
9660+
const response = await authClient.handleLogin(request);
9661+
expect(response.status).toEqual(307);
9662+
9663+
const authorizationUrl = new URL(response.headers.get("Location")!);
9664+
9665+
// Verify DPoP is enabled
9666+
expect(authorizationUrl.searchParams.has("dpop_jkt")).toBe(true);
9667+
9668+
// After lazy validation, clientMetadata should contain env var values
9669+
expect(authClient["clientMetadata"][oauth.clockSkew]).toBe(15);
9670+
expect(authClient["clientMetadata"][oauth.clockTolerance]).toBe(30);
95409671
});
95419672
});
95429673
});

src/server/auth-client.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,16 @@ export class AuthClient {
497497
}
498498
if (dpopConfig.dpopOptions) {
499499
this.dpopOptions = dpopConfig.dpopOptions;
500+
501+
// Update clientMetadata with resolved values from environment variables
502+
// This ensures clockSkew/clockTolerance from env vars are applied to OAuth operations
503+
if (typeof this.dpopOptions.clockSkew === "number") {
504+
this.clientMetadata[oauth.clockSkew] = this.dpopOptions.clockSkew;
505+
}
506+
if (typeof this.dpopOptions.clockTolerance === "number") {
507+
this.clientMetadata[oauth.clockTolerance] =
508+
this.dpopOptions.clockTolerance;
509+
}
500510
}
501511
this.dpopValidated = true;
502512
}

0 commit comments

Comments
 (0)