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
|
|
@ -2,8 +2,8 @@ import assert from 'node:assert/strict'
|
|||
import { createSign, generateKeyPairSync } from 'node:crypto'
|
||||
import { test } from 'node:test'
|
||||
|
||||
import { OAuthClient } from './oauth.mjs'
|
||||
import { SESSION_COOKIE } from './sessions.mjs'
|
||||
import { OAuthClient, SessionRenewalPendingError } from './oauth.mjs'
|
||||
import { SESSION_COOKIE, SESSION_RENEWAL_PENDING } from './sessions.mjs'
|
||||
|
||||
const identifier = 'a'.repeat(43)
|
||||
const replacement = 'b'.repeat(43)
|
||||
|
|
@ -163,9 +163,88 @@ test('expired and malformed authenticated sessions are destroyed', async () => {
|
|||
assert.deepEqual(store.destroyed, [identifier, identifier])
|
||||
})
|
||||
|
||||
test('token renewal preserves the session identifier and CSRF value', async () => {
|
||||
const store = new MemoryStore()
|
||||
store.value = {
|
||||
...validSession(),
|
||||
refreshToken: 'refresh-token',
|
||||
accessExpiresAt: now + 20,
|
||||
}
|
||||
const oauth = new OAuthClient(config, store)
|
||||
oauth.refresh = async (session) => ({
|
||||
...session,
|
||||
accessToken: 'renewed-access-token',
|
||||
accessExpiresAt: now + 300,
|
||||
})
|
||||
|
||||
const loaded = await oauth.authenticated({
|
||||
headers: { cookie: `${SESSION_COOKIE}=${identifier}` },
|
||||
})
|
||||
|
||||
assert.equal(loaded.identifier, identifier)
|
||||
assert.equal(loaded.session.accessToken, 'renewed-access-token')
|
||||
assert.equal(loaded.session.csrfToken, 'csrf-value')
|
||||
assert.equal(store.replaced, 0)
|
||||
})
|
||||
|
||||
test('concurrent renewal uses only an unexpired access token', async () => {
|
||||
const store = new MemoryStore()
|
||||
store.pending = true
|
||||
store.value = {
|
||||
...validSession(),
|
||||
refreshToken: 'refresh-token',
|
||||
accessExpiresAt: now + 20,
|
||||
}
|
||||
const oauth = new OAuthClient(config, store)
|
||||
const request = {
|
||||
headers: { cookie: `${SESSION_COOKIE}=${identifier}` },
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
(await oauth.authenticated(request)).session.accessToken,
|
||||
'access-token',
|
||||
)
|
||||
store.value.accessExpiresAt = now - 1
|
||||
await assert.rejects(
|
||||
oauth.authenticated(request, false),
|
||||
SessionRenewalPendingError,
|
||||
)
|
||||
assert.deepEqual(store.destroyed, [])
|
||||
})
|
||||
|
||||
test('logout destroys an expired session during owned renewal', async () => {
|
||||
const store = new MemoryStore()
|
||||
store.pending = true
|
||||
store.value = {
|
||||
...validSession(),
|
||||
accessExpiresAt: now - 1,
|
||||
}
|
||||
const oauth = new OAuthClient(config, store)
|
||||
oauth.metadata = async () => ({})
|
||||
const response = new TestResponse()
|
||||
|
||||
await oauth.logout(
|
||||
{
|
||||
headers: {
|
||||
cookie: `${SESSION_COOKIE}=${identifier}`,
|
||||
origin: config.publicOrigin,
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'x-remora-csrf': 'csrf-value',
|
||||
},
|
||||
},
|
||||
response,
|
||||
)
|
||||
|
||||
assert.equal(response.status, 200)
|
||||
assert.deepEqual(store.destroyed, [identifier])
|
||||
assert.match(response.headers['Set-Cookie'], /Max-Age=0/)
|
||||
})
|
||||
|
||||
class MemoryStore {
|
||||
value = null
|
||||
destroyed = []
|
||||
replaced = 0
|
||||
pending = false
|
||||
|
||||
async create(value) {
|
||||
this.value = value
|
||||
|
|
@ -177,10 +256,23 @@ class MemoryStore {
|
|||
}
|
||||
|
||||
async replace(_identifier, value) {
|
||||
this.replaced += 1
|
||||
this.value = value
|
||||
return replacement
|
||||
}
|
||||
|
||||
async renew(identifier, shouldRenew, renewal) {
|
||||
if (!shouldRenew(this.value)) return this.value
|
||||
if (this.pending) return SESSION_RENEWAL_PENDING
|
||||
const renewed = await renewal(this.value)
|
||||
if (renewed === null) {
|
||||
await this.destroy(identifier)
|
||||
return null
|
||||
}
|
||||
this.value = renewed
|
||||
return this.value
|
||||
}
|
||||
|
||||
async destroy(identifier) {
|
||||
this.destroyed.push(identifier)
|
||||
this.value = null
|
||||
|
|
|
|||
Loading…
Reference in a new issue