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:
parent
ffa65018ed
commit
414b380516
1 changed files with 980 additions and 136 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue