E2E for token timeout + refresh (todo §4); full-stack auth-refresh.spec.ts (real Ory stack): a lapsed session JWT is silently re-minted from the live Kratos session (roles re-read from Keto), and cleared once the session is revoked; ory/kratos/e2e.yml shortens the tokenizer ttl to 8s + adds JWT_CLOCK_SKEW_SEC config so re-mint fires at expiry; scope visual suite to visual.spec.ts
This commit is contained in:
@@ -21,6 +21,7 @@ test("loads dev defaults when the environment is empty", () => {
|
||||
assert.equal(c.ketoWriteUrl, "http://keto:4467");
|
||||
assert.match(c.cookieSecret, /dev-insecure/);
|
||||
assert.match(c.csrfSecret, /dev-insecure/);
|
||||
assert.equal(c.jwtClockSkewSec, 60); // default exp/nbf leeway for Kratos↔web clock drift
|
||||
});
|
||||
|
||||
test("JWKS_URL defaults to the committed Kratos tokenizer signing key, not an http endpoint", () => {
|
||||
@@ -59,6 +60,12 @@ test("rejects an invalid PORT", () => {
|
||||
for (const PORT of ["0", "70000", "abc", "3000.5"]) assert.throws(() => loadConfig({ PORT }), /PORT/);
|
||||
});
|
||||
|
||||
test("JWT_CLOCK_SKEW_SEC: parses a non-negative integer, rejects junk (E2E shortens it to 0)", () => {
|
||||
assert.equal(loadConfig({ JWT_CLOCK_SKEW_SEC: "0" }).jwtClockSkewSec, 0);
|
||||
assert.equal(loadConfig({ JWT_CLOCK_SKEW_SEC: "120" }).jwtClockSkewSec, 120);
|
||||
for (const v of ["-1", "1.5", "abc"]) assert.throws(() => loadConfig({ JWT_CLOCK_SKEW_SEC: v }), /JWT_CLOCK_SKEW_SEC/);
|
||||
});
|
||||
|
||||
test("rejects a malformed Ory URL", () => {
|
||||
assert.throws(() => loadConfig({ KETO_READ_URL: "not a url" }), /KETO_READ_URL/);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface Config {
|
||||
csrfSecret: string;
|
||||
jwksUrl: string;
|
||||
jwtAudience: string | undefined;
|
||||
jwtClockSkewSec: number;
|
||||
jwtIssuer: string | undefined;
|
||||
ketoReadUrl: string;
|
||||
ketoWriteUrl: string;
|
||||
@@ -71,6 +72,15 @@ function readPort(env: Env): number {
|
||||
return port;
|
||||
}
|
||||
|
||||
// A non-negative integer count of seconds, with a default. Used for the JWT exp/nbf leeway.
|
||||
function readNonNegInt(env: Env, key: string, devDefault: number): number {
|
||||
const raw = env[key];
|
||||
if (raw === undefined) return devDefault;
|
||||
const n = Number(raw);
|
||||
if (!Number.isInteger(n) || n < 0) throw new Error(`config: ${key} must be a non-negative integer, got "${raw}"`);
|
||||
return n;
|
||||
}
|
||||
|
||||
export function loadConfig(env: Env = process.env): Config {
|
||||
const requireSecure = readBool(env, "REQUIRE_SECURE_SECRETS", false);
|
||||
return {
|
||||
@@ -83,6 +93,8 @@ export function loadConfig(env: Env = process.env): Config {
|
||||
jwksUrl: readUrl(env, "JWKS_URL", "file:///etc/config/kratos/tokenizer/jwks.json"),
|
||||
// Optional, off by default: pin the session-JWT issuer/audience for a hardened deploy.
|
||||
jwtAudience: readOptional(env, "JWT_AUDIENCE"),
|
||||
// exp/nbf leeway (s) for Kratos↔web clock drift; the auth E2E sets 0 to time tokens out fast.
|
||||
jwtClockSkewSec: readNonNegInt(env, "JWT_CLOCK_SKEW_SEC", 60),
|
||||
jwtIssuer: readOptional(env, "JWT_ISSUER"),
|
||||
ketoReadUrl: readUrl(env, "KETO_READ_URL", "http://keto:4466"),
|
||||
ketoWriteUrl: readUrl(env, "KETO_WRITE_URL", "http://keto:4467"),
|
||||
|
||||
@@ -23,7 +23,7 @@ console.log(`Discovered ${plugins.length} plugin(s)${plugins.length ? `: ${plugi
|
||||
await runBootHooks(plugins); // plugin onBoot — after discovery, before listen; a throw aborts boot
|
||||
|
||||
const server = createApp({
|
||||
auth: { audience: config.jwtAudience, issuer: config.jwtIssuer },
|
||||
auth: { audience: config.jwtAudience, clockSkewSec: config.jwtClockSkewSec, issuer: config.jwtIssuer },
|
||||
cache: config.cacheTemplates,
|
||||
csrfSecret: config.csrfSecret,
|
||||
jwks,
|
||||
|
||||
Reference in New Issue
Block a user