Move browser authentication behind a generated-contract BFF
This commit is contained in:
parent
6f0f20f5cc
commit
fa7220ee33
1 changed files with 7259 additions and 2825 deletions
89
web/server/sessions.mjs
Normal file
89
web/server/sessions.mjs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { randomBytes, timingSafeEqual } from 'node:crypto'
|
||||
|
||||
import { createClient } from 'redis'
|
||||
|
||||
export const SESSION_COOKIE = '__Host-remora-session'
|
||||
|
||||
export class SessionStore {
|
||||
constructor(config, client = createClient({ url: config.sessionUrl })) {
|
||||
this.config = config
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async connect() {
|
||||
this.client.on('error', () => undefined)
|
||||
await this.client.connect()
|
||||
}
|
||||
|
||||
get ready() {
|
||||
return this.client.isReady
|
||||
}
|
||||
|
||||
async create(value) {
|
||||
const identifier = randomValue(32)
|
||||
await this.client.set(this.key(identifier), JSON.stringify(value), {
|
||||
EX: this.config.sessionTtlSeconds,
|
||||
})
|
||||
return identifier
|
||||
}
|
||||
|
||||
async read(identifier) {
|
||||
if (!validIdentifier(identifier)) return null
|
||||
const encoded = await this.client.get(this.key(identifier))
|
||||
if (encoded === null) return null
|
||||
try {
|
||||
return JSON.parse(encoded)
|
||||
} catch {
|
||||
await this.destroy(identifier)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async replace(identifier, value) {
|
||||
const replacement = await this.create(value)
|
||||
await this.destroy(identifier)
|
||||
return replacement
|
||||
}
|
||||
|
||||
async destroy(identifier) {
|
||||
if (validIdentifier(identifier)) await this.client.del(this.key(identifier))
|
||||
}
|
||||
|
||||
key(identifier) {
|
||||
return `${this.config.sessionNamespace}${identifier}`
|
||||
}
|
||||
}
|
||||
|
||||
export function sessionIdentifier(request) {
|
||||
const cookies = String(request.headers.cookie || '').split(';')
|
||||
for (const cookie of cookies) {
|
||||
const separator = cookie.indexOf('=')
|
||||
if (separator === -1) continue
|
||||
if (cookie.slice(0, separator).trim() === SESSION_COOKIE)
|
||||
return cookie.slice(separator + 1).trim()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function sessionCookie(identifier, config) {
|
||||
const secure = config.secureCookie ? '; Secure' : ''
|
||||
return `${SESSION_COOKIE}=${identifier}; Path=/; HttpOnly${secure}; SameSite=Strict`
|
||||
}
|
||||
|
||||
export function expiredSessionCookie(config) {
|
||||
return `${sessionCookie('', config)}; Max-Age=0`
|
||||
}
|
||||
|
||||
export function randomValue(bytes = 24) {
|
||||
return randomBytes(bytes).toString('base64url')
|
||||
}
|
||||
|
||||
export function equalSecret(left, right) {
|
||||
const a = Buffer.from(String(left))
|
||||
const b = Buffer.from(String(right))
|
||||
return a.length === b.length && timingSafeEqual(a, b)
|
||||
}
|
||||
|
||||
function validIdentifier(value) {
|
||||
return typeof value === 'string' && /^[A-Za-z0-9_-]{43}$/.test(value)
|
||||
}
|
||||
Loading…
Reference in a new issue