Harden WebBFF and deployment release behavior

Model fail-closed cross-replica session renewal and implement owner-fenced refresh, transient observer handling, logout fencing, and graceful shutdown. Remove chart-owned Ingress while retaining qualification-owned external routing.
This commit is contained in:
ms2k 2026-07-20 17:09:26 +09:00
commit 414b380516

View file

@ -4,6 +4,7 @@ import {
equalSecret,
expiredSessionCookie,
randomValue,
SESSION_RENEWAL_PENDING,
sessionCookie,
sessionIdentifier,
} from './sessions.mjs'
@ -11,6 +12,8 @@ import {
const CLOCK_SKEW_SECONDS = 30
const OIDC_TIMEOUT_MILLISECONDS = 15_000
export class SessionRenewalPendingError extends Error {}
export class OAuthClient {
constructor(config, store, fetchImplementation = fetch) {
this.config = config
@ -127,23 +130,16 @@ export class OAuthClient {
)
return true
}
json(
response,
200,
{
kind: 'authenticated',
identity: loaded.session.identity,
csrfToken: loaded.session.csrfToken,
},
loaded.rotated
? { 'Set-Cookie': sessionCookie(loaded.identifier, this.config) }
: {},
)
json(response, 200, {
kind: 'authenticated',
identity: loaded.session.identity,
csrfToken: loaded.session.csrfToken,
})
return true
}
async logout(request, response) {
const loaded = await this.authenticated(request, false)
const loaded = await this.storedSession(request)
if (
loaded === null ||
!validUnsafeRequest(request, this.config, loaded.session.csrfToken)
@ -173,29 +169,58 @@ export class OAuthClient {
}
async authenticated(request, refresh = true) {
const loaded = await this.storedSession(request)
if (loaded === null) return null
const { identifier } = loaded
let { session } = loaded
if (session.accessExpiresAt <= now() + CLOCK_SKEW_SECONDS) {
if (!refresh) {
if (session.accessExpiresAt <= now()) {
session = await this.store.renew(
identifier,
(current) =>
validAuthenticatedSession(current) &&
current.accessExpiresAt <= now(),
async () => null,
)
if (session === SESSION_RENEWAL_PENDING)
throw new SessionRenewalPendingError()
if (!validAuthenticatedSession(session)) return null
}
} else {
const renewed = await this.store.renew(
identifier,
(current) =>
validAuthenticatedSession(current) &&
current.accessExpiresAt <= now() + CLOCK_SKEW_SECONDS,
(current) => this.refresh(current).catch(() => null),
)
if (renewed === SESSION_RENEWAL_PENDING) {
if (session.accessExpiresAt <= now())
throw new SessionRenewalPendingError()
} else {
session = renewed
}
if (!validAuthenticatedSession(session)) {
if (session?.kind === 'authenticated')
await this.store.destroy(identifier)
return null
}
}
}
return { identifier, session }
}
async storedSession(request) {
const identifier = sessionIdentifier(request)
if (identifier === null) return null
let session = await this.store.read(identifier)
const session = await this.store.read(identifier)
if (!validAuthenticatedSession(session)) {
if (session?.kind === 'authenticated')
await this.store.destroy(identifier)
return null
}
let rotated = false
if (session.accessExpiresAt <= now() + CLOCK_SKEW_SECONDS) {
if (!refresh) {
await this.store.destroy(identifier)
return null
}
session = await this.refresh(session).catch(() => null)
if (session === null) {
await this.store.destroy(identifier)
return null
}
identifier = await this.store.replace(identifier, session)
rotated = true
}
return { identifier, session, rotated }
return { identifier, session }
}
async refresh(session) {