feat(portal-bff): session middleware with AES-256-GCM at rest per ADR-0010
mount express-session + connect-redis at bootstrap on top of the
shared ioredis client. the full json payload is encrypted with
aes-256-gcm before it reaches redis; envelope is versioned
(v1.<iv>.<tag>.<ciphertext>, base64url) so the algorithm or key
derivation can rotate without a flag-day re-encryption.
scope is intentionally infrastructure-only — middleware mounted,
req.session available downstream, cookie set on first write,
encryption-at-rest active. populating req.session.user from
/auth/callback, /me, /auth/logout, and the absolute-timeout
interceptor land in follow-ups.
notable shape choices captured in ADR-0010 (amended here):
- encryption at rest applies to the whole payload, not just a tokens
sub-field. the session also carries pii claims (oid, tid,
preferred_username) — encrypting the envelope removes the need to
classify fields one by one and costs essentially the same.
- connect-redis v9 was rewritten for node-redis v4 and dropped
ioredis. rather than swap the whole bff to node-redis, a small
adapter shapes the six commands connect-redis actually calls
(get / set with {expiration:{type:'EX',value}} / expire / del /
mGet / scanIterator) to look like node-redis. the rest of the
bff stays on a single shared ioredis client.
session id: crypto.randomBytes(32).toString('base64url')
cookie name: __Host-portal_session in production, portal_session in
dev (the __Host- prefix mandates Secure which dev http can't
provide). httpOnly + sameSite=lax + path=/. resave:false,
saveUninitialized:false, rolling:true; cookie maxAge follows
SESSION_IDLE_TIMEOUT_SECONDS (default 1800).
env: SESSION_ENCRYPTION_KEY is mandatory (32 bytes after base64url
decode); rejected at boot via a new assertSessionEncryptionKey()
mirroring the other pre-flight validators. SESSION_IDLE_TIMEOUT_SECONDS
and SESSION_ABSOLUTE_TIMEOUT_SECONDS are optional with adr-aligned
defaults (1800 / 43200).
This commit is contained in:
@@ -84,6 +84,27 @@ SESSION_SECRET=replace_with_32_random_bytes_base64url
|
||||
# variable supports the dev single-instance shape only.
|
||||
REDIS_URL=redis://default:redis_dev_change_me@localhost:6379/0
|
||||
|
||||
# Session payload encryption (per ADR-0010 §"At-rest encryption").
|
||||
# AES-256-GCM key for encrypting the session JSON that connect-redis
|
||||
# writes to Redis, so a Redis dump never carries raw user identities
|
||||
# / future tokens / claims in plaintext. **Distinct** from
|
||||
# SESSION_SECRET, which only signs the cookie's session-id — never
|
||||
# reuse one for the other. Mandatory at boot.
|
||||
#
|
||||
# node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
|
||||
SESSION_ENCRYPTION_KEY=replace_with_32_random_bytes_base64url
|
||||
|
||||
# Session timeouts (per ADR-0010). Both optional with sensible
|
||||
# defaults; override only when staging / prod policy diverges.
|
||||
# SESSION_IDLE_TIMEOUT_SECONDS — sliding window. Each request
|
||||
# extends the cookie's `expires` by this many seconds.
|
||||
# Default 1800 (30 min).
|
||||
# SESSION_ABSOLUTE_TIMEOUT_SECONDS — hard ceiling. Session is
|
||||
# destroyed regardless of activity at this age.
|
||||
# Default 43200 (12 h).
|
||||
# SESSION_IDLE_TIMEOUT_SECONDS=1800
|
||||
# SESSION_ABSOLUTE_TIMEOUT_SECONDS=43200
|
||||
|
||||
# Future env vars introduced by upcoming phases / ADRs:
|
||||
#
|
||||
# Auth flow (ADR-0009) — additional keys wired as the routes land:
|
||||
@@ -96,9 +117,6 @@ REDIS_URL=redis://default:redis_dev_change_me@localhost:6379/0
|
||||
# REDIS_SENTINEL_HOSTS (CSV `host:port,host:port,…`; prod HA)
|
||||
# REDIS_SENTINEL_NAME (master name in Sentinel; prod HA)
|
||||
# REDIS_TLS ('true' in prod)
|
||||
# SESSION_ENCRYPTION_KEY (32-byte base64)
|
||||
# SESSION_IDLE_TIMEOUT_SECONDS (default 1800)
|
||||
# SESSION_ABSOLUTE_TIMEOUT_SECONDS (default 43200)
|
||||
#
|
||||
# MFA (ADR-0011):
|
||||
# MFA_FRESHNESS_SECONDS (default 600)
|
||||
|
||||
Reference in New Issue
Block a user