Files

1283 lines
64 KiB
JavaScript

import { createServer } from 'node:http'
import { createHash, randomBytes } from 'node:crypto'
import { timingSafeEqual } from 'node:crypto'
import { AuthenticationLimiter } from './rate-limiter.mjs'
import { AccessError, accessErrorBody } from './errors.mjs'
import { normalizePassword, normalizeUsername, verifyCredential } from './credentials.mjs'
import { BrowserSessionStore } from './sessions.mjs'
import { ManagementExchangeStore } from './exchanges.mjs'
import { CapabilityStore } from './capabilities.mjs'
import { ManagementTransitionStore } from './transitions.mjs'
import { detectRuntimeCapabilities } from './runtime-capabilities.mjs'
import { TotpFlowStore, TotpRetryLimiter, totpUri, verifyTotpCode } from './totp.mjs'
const MAX_BODY_BYTES = 64 * 1024
const RECOVERY_OPERATIONS = new Map([
['/v1/recovery/set-username', 'set-username'],
['/v1/recovery/reset-access', 'reset-access'],
['/v1/recovery/reset-password', 'reset-password'],
['/v1/recovery/reset-management-password', 'reset-management-password'],
['/v1/recovery/disable-management-password', 'disable-management-password'],
['/v1/recovery/generate-key', 'generate-key'],
['/v1/recovery/clear-retry', 'clear-retry'],
])
const ACCESS_FAILURE_EVENTS = new Map([
['/v1/initialize', 'access.initialization.failed'],
['/v1/dsh/initialize', 'access.initialization.failed'],
['/v1/dsh/reset-authentication', 'access.authentication-reset.failed'],
])
function identifier() { return randomBytes(32).toString('base64url') }
function isLoopbackHostname(hostname) {
if (hostname === 'localhost' || hostname === '[::1]') return true
const parts = hostname.split('.')
return parts.length === 4 && parts[0] === '127'
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
}
function digest(value) { return createHash('sha256').update(value).digest('base64url') }
function accountTotp(account) {
return account?.totp ?? { enabled: false, version: 0, secret: null, changedAt: null }
}
function authenticationSource(value) {
if (typeof value?.authenticationSource === 'string'
&& /^[A-Za-z0-9:_-]{1,128}$/.test(value.authenticationSource)) return value.authenticationSource
const ip = value?.client?.ip
return typeof ip === 'string' && ip.length > 0 && ip.length <= 128
? `ip:${digest(ip)}` : 'unknown'
}
function enforceAuthenticationRetry(retry) {
if (retry?.retryAfterSeconds <= 0) return
throw new AccessError(
'AUTHENTICATION_RETRY_REQUIRED',
`authentication retry is available in ${retry.retryAfterSeconds} seconds`,
429,
{ retryAfterSeconds: retry.retryAfterSeconds },
)
}
function releaseAuthentication(admission, authenticated) {
const retry = admission.release(authenticated)
if (!authenticated) enforceAuthenticationRetry(retry)
}
function normalizeManagementAccess(mode, entry) {
if (!['compat', 'isolated'].includes(mode)) throw new AccessError('ACCESS_MODE_INVALID', 'management access mode is invalid')
if (mode === 'compat') return null
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)
|| !['local-only', 'public'].includes(entry.kind)) {
throw new AccessError('ACCESS_ENTRY_INVALID', 'isolated Management entry is invalid')
}
if (entry.kind === 'local-only') {
try {
const origin = entry.managementLocalOrigin
const parsed = new URL(origin)
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin !== origin
|| !isLoopbackHostname(parsed.hostname)) throw new Error('not loopback')
return { kind: 'local-only', managementLocalOrigin: parsed.origin }
} catch { throw new AccessError('ACCESS_ENTRY_INVALID', 'local Management origin is invalid') }
}
try {
const parsed = new URL(entry.managementPublicOrigin)
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin === 'null'
|| parsed.username !== '' || parsed.password !== '' || parsed.pathname !== '/'
|| parsed.search !== '' || parsed.hash !== '') throw new Error('invalid origin')
return { kind: 'public', managementPublicOrigin: parsed.origin }
} catch {
throw new AccessError('ACCESS_ENTRY_INVALID', 'isolated Management entry is invalid')
}
}
function sameToken(left, right) {
const a = Buffer.from(left ?? '')
const b = Buffer.from(right ?? '')
return a.byteLength === b.byteLength && a.byteLength > 0 && timingSafeEqual(a, b)
}
function send(response, status, value, headers = {}) {
response.writeHead(status, {
'cache-control': 'no-store',
'content-type': 'application/json; charset=utf-8',
...headers,
})
response.end(`${JSON.stringify(value)}\n`)
}
async function body(request) {
const chunks = []
let size = 0
for await (const chunk of request) {
size += chunk.byteLength
if (size > MAX_BODY_BYTES) throw new AccessError('REQUEST_TOO_LARGE', 'access request is too large', 413)
chunks.push(chunk)
}
if (size === 0) return {}
try { return JSON.parse(Buffer.concat(chunks).toString('utf8')) }
catch { throw new AccessError('REQUEST_INVALID', 'access request is invalid') }
}
function publicAccount(account) {
if (account === undefined) return null
return {
accountId: account.accountId,
username: account.username,
revision: account.revision,
mainCredentialVersion: account.mainCredential.version,
managementAdditionalCredential: {
enabled: account.managementAdditionalCredential.enabled,
version: account.managementAdditionalCredential.version,
},
totp: {
enabled: accountTotp(account).enabled,
version: accountTotp(account).version,
},
managementAccess: { ...account.managementAccess },
}
}
export class AccessService {
constructor({
store,
classificationToken,
limiter = new AuthenticationLimiter(),
sessions = null,
exchanges = new ManagementExchangeStore(),
capabilities = new CapabilityStore(),
transitions,
totpFlows,
totpLimiter,
verifyTotp = verifyTotpCode,
verify = verifyCredential,
report = async () => {},
now = () => Date.now(),
runtimeCapabilities = detectRuntimeCapabilities,
authenticationEpoch = identifier(),
}) {
this.store = store
this.classificationToken = classificationToken
this.limiter = limiter
this.sessions = sessions ?? new BrowserSessionStore()
this.exchanges = exchanges
this.capabilities = capabilities
this.transitions = transitions ?? new ManagementTransitionStore({ now })
this.totpFlows = totpFlows ?? new TotpFlowStore({ now })
this.totpLimiter = totpLimiter ?? new TotpRetryLimiter({ now })
this.verifyTotp = verifyTotp
this.verify = verify
this.report = report
this.sessions.setEventSink?.((message, fields) => {
void Promise.resolve(this.report(message, fields)).catch(() => {})
})
this.now = now
this.runtimeCapabilities = runtimeCapabilities
this.authenticationEpoch = authenticationEpoch
this.authenticationReset = null
this.transition = Promise.resolve()
}
authenticationContext(current) {
const revision = current.account?.revision ?? current.initialization?.instanceId ?? current.state
return `${this.authenticationEpoch}.${revision}`
}
requireAuthenticationContext(value, current) {
if (!sameToken(value.authenticationContext, this.authenticationContext(current))) {
throw new AccessError('AUTHENTICATION_CONTEXT_STALE', 'authentication context is stale', 409)
}
}
serialized(operation) {
const pending = this.transition.then(operation, operation)
this.transition = pending.catch(() => {})
return pending
}
reconcileRuntimePolicy() {
return this.serialized(async () => {
const [current, runtimeCapabilities] = await Promise.all([
this.store.state(),
this.runtimeCapabilities(),
])
if (current.state !== 'initialized' || current.account === undefined
|| runtimeCapabilities.dshRootCapabilityEffective !== true
|| current.account.managementAccess.mode === 'compat') {
return { changed: false, account: publicAccount(current.account), ...runtimeCapabilities }
}
const changedAt = new Date(this.now()).toISOString()
const account = {
...current.account,
revision: identifier(),
updatedAt: changedAt,
managementAccess: {
mode: 'compat',
version: current.account.managementAccess.version + 1,
isolatedEntry: null,
dshPublicOrigin: null,
changedAt,
},
}
const next = await this.store.replaceAccount(account, current.account.revision)
const revokedSessions = this.sessions.revokeAll()
this.exchanges.clear?.()
await this.report('access.management-origin.reconciled', {
mode: 'compat', reason: 'dsh-root-capability', revokedSessions,
})
return { changed: true, account: publicAccount(next), ...runtimeCapabilities }
})
}
async status() {
const current = await this.store.state()
const capabilities = await this.runtimeCapabilities()
return {
componentReady: true,
state: current.state,
instanceId: current.initialization?.instanceId ?? null,
authenticationContext: this.authenticationContext(current),
account: publicAccount(current.account),
...capabilities,
}
}
classify(value) {
if (!sameToken(value.token, this.classificationToken)) {
throw new AccessError('CLASSIFICATION_FORBIDDEN', 'classification is forbidden', 403)
}
return this.serialized(async () => {
const classified = await this.store.classify(value.evidence)
await this.report('access.classification.completed', { state: classified.initialization.state })
return this.status()
})
}
initialize(value) {
return this.serialized(async () => {
const account = await this.store.initialize(value)
await this.report('access.initialization.completed', { accountId: account.accountId })
return this.status()
})
}
initializeDsh(value) {
return this.serialized(async () => {
const account = await this.store.initialize(value)
const session = this.sessions.issue('dsh', account, {
origin: value.origin,
client: value.client,
authenticationSource: authenticationSource(value),
})
await this.report('access.initialization.completed', { accountId: account.accountId })
await this.report('access.session.created', { accountId: account.accountId, kind: 'dsh' })
return { state: 'initialized', account: publicAccount(account), session }
})
}
generateAuthenticationResetKey() {
return this.serialized(async () => {
const current = await this.store.state()
if (!['migration-required', 'recovery-required'].includes(current.state)) {
throw new AccessError('AUTHENTICATION_RESET_UNAVAILABLE', 'administrator authentication reset is unavailable', 409)
}
const key = `dshak_${identifier()}`
this.authenticationReset = { digest: digest(key), expiresAt: this.now() + 10 * 60 * 1000 }
await this.report('access.authentication-reset-key.generated', { state: current.state })
return { key, expiresAt: new Date(this.authenticationReset.expiresAt).toISOString() }
})
}
resetDshAuthentication(value) {
return this.serialized(async () => {
const setup = this.authenticationReset
if (setup === null || setup.expiresAt <= this.now() || typeof value.setupKey !== 'string'
|| !sameToken(setup.digest, digest(value.setupKey))) {
if (setup !== null && setup.expiresAt <= this.now()) this.authenticationReset = null
throw new AccessError('AUTHENTICATION_RESET_KEY_INVALID', 'authentication reset key is invalid or expired', 401)
}
const current = await this.store.state()
let account
if (current.state === 'migration-required') account = await this.store.migrate(value)
else if (current.state === 'recovery-required') account = await this.store.recover(value)
else throw new AccessError('AUTHENTICATION_RESET_UNAVAILABLE', 'administrator authentication reset is unavailable', 409)
this.authenticationReset = null
const session = this.sessions.issue('dsh', account, {
origin: value.origin,
client: value.client,
authenticationSource: authenticationSource(value),
})
await this.report('access.authentication-reset.completed', { accountId: account.accountId, previousState: current.state })
return { state: 'initialized', account: publicAccount(account), session }
})
}
async authenticate(value, suppliedCurrent) {
const current = suppliedCurrent ?? await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const admission = this.limiter.enter(current.account.accountId, authenticationSource(value))
let authenticated = false
try {
let usernameMatches = false
try { usernameMatches = normalizeUsername(value.username) === current.account.username } catch {}
const passwordMatches = await this.verify(value.password, current.account.mainCredential)
authenticated = usernameMatches && passwordMatches
if (!authenticated) {
await this.report('access.authentication.failed', { accountId: current.account.accountId, level: 'warning' })
throw new AccessError('AUTHENTICATION_FAILED', 'username or password is incorrect', 401)
}
await this.report('access.authentication.succeeded', { accountId: current.account.accountId })
return {
authenticated: true,
accountId: current.account.accountId,
accountRevision: current.account.revision,
mainCredentialVersion: current.account.mainCredential.version,
managementAccessVersion: current.account.managementAccess.version,
}
} finally { releaseAuthentication(admission, authenticated) }
}
async verifyFreshAuthentication(account, mainPassword, sourceId = 'unknown') {
const mainMatches = typeof mainPassword === 'string'
&& await this.verify(mainPassword, account.mainCredential)
if (!mainMatches) {
await this.report('access.fresh-authentication.failed', { accountId: account.accountId, level: 'warning' })
throw new AccessError('FRESH_AUTH_FAILED', 'current administrator credentials are incorrect', 401)
}
await this.report('access.fresh-authentication.succeeded', { accountId: account.accountId })
}
async loginDsh(value) {
const current = await this.store.state()
this.requireAuthenticationContext(value, current)
const authenticated = await this.authenticate(value, current)
if (accountTotp(current.account).enabled) {
const challenge = this.totpFlows.createLogin(current.account, {
origin: value.origin,
client: value.client,
authenticationSource: authenticationSource(value),
})
return { ...authenticated, totpRequired: true, challenge }
}
const session = this.sessions.issue('dsh', current.account, {
origin: value.origin,
client: value.client,
authenticationSource: authenticationSource(value),
})
await this.report('access.session.created', { accountId: authenticated.accountId, kind: 'dsh' })
return { ...authenticated, session }
}
async verifyTotpAuthentication(account, code, source, secret = accountTotp(account).secret) {
const currentRetry = this.totpLimiter.retry(account.accountId, source)
if (currentRetry.retryAfterSeconds > 0) {
throw new AccessError(
currentRetry.kind === 'rate' ? 'TOTP_RATE_LIMITED' : 'TOTP_RETRY_REQUIRED',
currentRetry.kind === 'rate'
? 'too many two-factor authentication attempts'
: 'two-factor authentication retry is required',
429,
{ retryAfterSeconds: currentRetry.retryAfterSeconds },
)
}
const valid = this.verifyTotp(code, secret, { now: this.now() })
if (!valid) {
const retry = this.totpLimiter.fail(account.accountId, source)
await this.report('access.totp.failed', { accountId: account.accountId, level: 'warning' })
if (retry.retryAfterSeconds > 0) {
throw new AccessError(
retry.kind === 'rate' ? 'TOTP_RATE_LIMITED' : 'TOTP_RETRY_REQUIRED',
retry.kind === 'rate'
? 'too many two-factor authentication attempts'
: 'two-factor authentication retry is required',
429,
{ retryAfterSeconds: retry.retryAfterSeconds },
)
}
throw new AccessError('TOTP_INVALID', 'two-factor authentication code is invalid', 401)
}
this.totpLimiter.succeed(account.accountId, source)
await this.report('access.totp.succeeded', { accountId: account.accountId })
}
async completeDshTotp(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined || !accountTotp(current.account).enabled) {
throw new AccessError('TOTP_LOGIN_INVALID', 'two-factor authentication login is invalid or expired', 401)
}
this.requireAuthenticationContext(value, current)
const pending = this.totpFlows.login(value.challengeToken, current.account)
if (pending === undefined || pending.value.origin !== value.origin
|| pending.value.authenticationSource !== authenticationSource(value)) {
throw new AccessError('TOTP_LOGIN_INVALID', 'two-factor authentication login is invalid or expired', 401)
}
await this.verifyTotpAuthentication(
current.account,
value.code,
pending.value.authenticationSource,
)
this.totpFlows.consumeLogin(pending.key)
const session = this.sessions.issue('dsh', current.account, {
origin: pending.value.origin,
client: pending.value.client,
authenticationSource: pending.value.authenticationSource,
})
await this.report('access.session.created', { accountId: current.account.accountId, kind: 'dsh' })
return { authenticated: true, session }
}
async validateSession(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) return { authenticated: false }
const session = this.sessions.validate(value.token, value.kind, current.account, {
origin: value.origin,
csrfToken: value.csrfToken,
requireCsrf: value.requireCsrf === true,
})
return session === undefined ? { authenticated: false } : { authenticated: true, session }
}
async logout(value) {
const revoked = this.sessions.revoke(value.token)
if (revoked) await this.report('access.session.revoked', { kind: value.kind ?? null })
return { authenticated: false }
}
async logoutDshBrowser(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
if (!['management', 'all'].includes(value.scope)) {
throw new AccessError('SESSION_ACTION_INVALID', 'session action is invalid')
}
const session = this.sessions.validate(value.dshToken, 'dsh', current.account, {
origin: value.dshOrigin,
touch: false,
})
if (session === undefined) throw new AccessError('SESSION_INVALID', 'DSH session is invalid', 401)
let managementRevoked = this.sessions.revokeManagementFromDsh(session.sessionId)
if (typeof value.managementToken === 'string' && this.sessions.revoke(value.managementToken)) {
managementRevoked += 1
}
const dshRevoked = value.scope === 'all' && this.sessions.revoke(value.dshToken)
await this.report('access.browser-sessions.logged-out', {
scope: value.scope,
dshRevoked,
managementRevoked,
})
return { authenticated: value.scope !== 'all', dshRevoked, managementRevoked }
}
async managementResult(account, { origin, sourceDshOrigin, sourceDshSessionId }) {
if (account.managementAdditionalCredential.enabled) {
return { pending: this.exchanges.createPending(account, { targetOrigin: origin, sourceDshOrigin, sourceDshSessionId }) }
}
const session = this.sessions.issue('management', account, { origin, sourceDshOrigin, sourceDshSessionId })
await this.report('access.session.created', { accountId: account.accountId, kind: 'management' })
return { session }
}
async createManagementHandoff(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const session = this.sessions.validate(value.dshToken, 'dsh', current.account, { origin: value.dshOrigin })
if (session === undefined) throw new AccessError('SESSION_INVALID', 'DSH session is invalid', 401)
const handoff = this.exchanges.createHandoff(session, current.account, value.targetOrigin)
await this.report('access.handoff.created', { accountId: current.account.accountId })
return { handoff }
}
async consumeManagementHandoff(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const handoff = this.exchanges.consumeHandoff(value.token, current.account, value.origin)
if (handoff === undefined) throw new AccessError('HANDOFF_INVALID', 'management handoff is invalid or expired', 401)
const source = this.sessions.details(handoff.dshSessionId)
if (source?.kind !== 'dsh' || source.origin !== handoff.sourceDshOrigin) {
throw new AccessError('HANDOFF_INVALID', 'management handoff is invalid or expired', 401)
}
await this.report('access.handoff.consumed', { accountId: current.account.accountId })
return this.managementResult(current.account, {
origin: value.origin,
sourceDshOrigin: handoff.sourceDshOrigin,
sourceDshSessionId: handoff.dshSessionId,
})
}
async completeManagementLogin(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('PENDING_LOGIN_INVALID', 'management login is invalid or expired', 401)
}
this.requireAuthenticationContext(value, current)
if (!current.account.managementAdditionalCredential.enabled) {
throw new AccessError('PENDING_LOGIN_INVALID', 'management login is invalid or expired', 401)
}
const source = this.sessions.details(
this.exchanges.peekPendingSource(value.pendingToken, current.account, value.origin) ?? '',
)
const sourceId = source?.authenticationSource ?? 'unknown'
this.limiter.checkRetry(current.account.accountId, sourceId, 'management')
const pending = this.exchanges.inspectPending(value.pendingToken, current.account, value.origin)
if (pending === undefined) throw new AccessError('PENDING_LOGIN_INVALID', 'management login is invalid or expired', 401)
const validatedSource = this.sessions.details(pending.value.sourceDshSessionId)
if (validatedSource?.kind !== 'dsh' || validatedSource.origin !== pending.value.sourceDshOrigin) {
this.exchanges.consumePending(pending.key)
throw new AccessError('PENDING_LOGIN_INVALID', 'management login is invalid or expired', 401)
}
const admission = this.limiter.enter(current.account.accountId, sourceId, 'management')
let valid = false
try {
valid = await this.verify(value.password, current.account.managementAdditionalCredential.verifier)
if (!valid) {
await this.report('access.authentication.failed', {
accountId: current.account.accountId, kind: 'management', level: 'warning',
})
throw new AccessError('AUTHENTICATION_FAILED', 'username or password is incorrect', 401)
}
await this.report('access.authentication.succeeded', {
accountId: current.account.accountId, kind: 'management',
})
} finally { releaseAuthentication(admission, valid) }
this.exchanges.consumePending(pending.key)
const session = this.sessions.issue('management', current.account, {
origin: value.origin,
sourceDshOrigin: pending.value.sourceDshOrigin,
sourceDshSessionId: pending.value.sourceDshSessionId,
})
await this.report('access.session.created', { accountId: current.account.accountId, kind: 'management' })
return { session }
}
async issueCapability(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const session = this.sessions.validate(value.managementToken, 'management', current.account, {
origin: value.origin,
csrfToken: value.csrfToken,
requireCsrf: value.requireCsrf === true,
touch: false,
})
if (session === undefined) throw new AccessError('SESSION_INVALID', 'Management session is invalid', 401)
return { capability: this.capabilities.issue(session, current.account, value) }
}
async issuePluginCapability(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const session = this.sessions.validate(value.dshToken, 'dsh', current.account, {
origin: value.origin,
csrfToken: value.csrfToken,
requireCsrf: value.requireCsrf === true,
touch: false,
})
if (session === undefined) throw new AccessError('SESSION_INVALID', 'DSH session is invalid', 401)
return {
capability: this.capabilities.issue(session, current.account, {
audience: 'plugin', method: value.method, target: value.target,
}),
}
}
async consumeCapability(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const capability = this.capabilities.consume(value.token, current.account, value)
if (capability === undefined) throw new AccessError('CAPABILITY_INVALID', 'Management capability is invalid or expired', 401)
return { authorized: true, capability }
}
consumeInternalCapability(value, account) {
const authorization = this.capabilities.consume(value.internalCapability, account, {
audience: 'management', method: value.method, target: value.target,
})
if (authorization === undefined) throw new AccessError('CAPABILITY_INVALID', 'Management capability is invalid', 401)
return authorization
}
async authenticationSettings(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const authorization = this.consumeInternalCapability(value, current.account)
const capabilities = await this.runtimeCapabilities()
return {
account: publicAccount(current.account),
sessions: this.sessions.list(current.account, authorization.sessionId),
...capabilities,
}
}
async beginTotpEnrollment(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const authorization = this.consumeInternalCapability(value, current.account)
if (accountTotp(current.account).enabled) {
throw new AccessError('TOTP_ALREADY_ENABLED', 'two-factor authentication is already enabled', 409)
}
const username = value.username === undefined ? current.account.username : normalizeUsername(value.username)
const enrollment = this.totpFlows.createEnrollment(current.account, authorization.sessionId)
await this.report('access.totp.enrollment.created', { accountId: current.account.accountId })
return {
enrollmentToken: enrollment.token,
secret: enrollment.secret,
uri: totpUri({ secret: enrollment.secret, username }),
expiresAt: enrollment.expiresAt,
}
}
async cancelTotpEnrollment(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const authorization = this.consumeInternalCapability(value, current.account)
const canceled = this.totpFlows.cancelEnrollment(value.enrollmentToken, current.account, authorization.sessionId)
if (canceled) await this.report('access.totp.enrollment.canceled', { accountId: current.account.accountId })
return { canceled }
}
async confirmTotpEnrollment(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const authorization = this.consumeInternalCapability(value, current.account)
const enrollment = this.totpFlows.enrollment(value.enrollmentToken, current.account, authorization.sessionId)
if (enrollment === undefined) {
throw new AccessError('TOTP_ENROLLMENT_INVALID', 'two-factor authentication enrollment is invalid or expired', 401)
}
const managementSession = this.sessions.details(authorization.sessionId)
const sourceSession = this.sessions.details(managementSession?.sourceDshSessionId)
await this.verifyTotpAuthentication(
current.account,
value.totpCode,
sourceSession?.authenticationSource ?? 'unknown',
enrollment.value.secret,
)
if (!this.totpFlows.confirmEnrollment(enrollment.key)) {
throw new AccessError('TOTP_ENROLLMENT_INVALID', 'two-factor authentication enrollment is invalid or expired', 401)
}
await this.report('access.totp.enrollment.confirmed', { accountId: current.account.accountId })
return { confirmed: true, expiresAt: new Date(enrollment.value.expiresAt).toISOString() }
}
async beginTotpDisableConfirmation(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const authorization = this.consumeInternalCapability(value, current.account)
if (!accountTotp(current.account).enabled) {
throw new AccessError('TOTP_NOT_ENABLED', 'two-factor authentication is not enabled', 409)
}
const confirmation = this.totpFlows.createDisableConfirmation(current.account, authorization.sessionId)
await this.report('access.totp.disable-confirmation.created', { accountId: current.account.accountId })
return { confirmationToken: confirmation.token, expiresAt: confirmation.expiresAt }
}
async cancelTotpDisableConfirmation(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const authorization = this.consumeInternalCapability(value, current.account)
const canceled = this.totpFlows.cancelDisableConfirmation(
value.confirmationToken,
current.account,
authorization.sessionId,
)
if (canceled) await this.report('access.totp.disable-confirmation.canceled', { accountId: current.account.accountId })
return { canceled }
}
async confirmTotpDisable(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const authorization = this.consumeInternalCapability(value, current.account)
const confirmation = this.totpFlows.disableConfirmation(
value.confirmationToken,
current.account,
authorization.sessionId,
)
if (confirmation === undefined) {
throw new AccessError('TOTP_DISABLE_CONFIRMATION_INVALID', 'two-factor disable confirmation is invalid or expired', 401)
}
const managementSession = this.sessions.details(authorization.sessionId)
const sourceSession = this.sessions.details(managementSession?.sourceDshSessionId)
await this.verifyTotpAuthentication(
current.account,
value.totpCode,
sourceSession?.authenticationSource ?? 'unknown',
)
if (!this.totpFlows.confirmDisable(confirmation.key)) {
throw new AccessError('TOTP_DISABLE_CONFIRMATION_INVALID', 'two-factor disable confirmation is invalid or expired', 401)
}
await this.report('access.totp.disable-confirmation.confirmed', { accountId: current.account.accountId })
return { confirmed: true, expiresAt: new Date(confirmation.value.expiresAt).toISOString() }
}
async revokeBrowserSessions(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const authorization = this.consumeInternalCapability(value, current.account)
if (typeof value.sessionId !== 'string' || value.sessionId.length === 0 || value.sessionId.length > 128) {
throw new AccessError('SESSION_ACTION_INVALID', 'session action is invalid')
}
const currentDshSessionId = this.sessions.sourceDshSessionId(authorization.sessionId)
const revoked = this.sessions.revokeDshSession(value.sessionId, current.account.accountId)
if (!revoked) throw new AccessError('SESSION_NOT_FOUND', 'browser session was not found', 404)
const currentSessionRevoked = value.sessionId === currentDshSessionId
await this.report('access.sessions.revoked', { sessionId: value.sessionId, currentSessionRevoked })
return { revoked: 1, currentSessionRevoked }
}
async recoveryStatus() {
const current = await this.store.state()
return {
state: current.state,
authenticationRetry: current.account === undefined
? null : this.limiter.status(current.account.accountId),
account: publicAccount(current.account),
}
}
async clearAuthenticationRetry(value = {}) {
const current = await this.store.state()
if (current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator account is unavailable', 409)
}
const scope = value.scope ?? 'all'
const credential = value.credential ?? 'all'
if (!['all', 'global'].includes(scope)) {
throw new AccessError('REQUEST_INVALID', 'authentication retry clear scope is invalid')
}
if (!['all', 'main', 'management', 'totp'].includes(credential)) {
throw new AccessError('REQUEST_INVALID', 'authentication retry credential is invalid')
}
let result
if (credential === 'totp') {
result = this.totpLimiter.clearDailyLimits(current.account.accountId, { globalOnly: scope === 'global' })
}
else if (credential === 'all') {
const passwordResult = scope === 'global'
? this.limiter.clearGlobal('all')
: this.limiter.clear(current.account.accountId, 'all')
const totpResult = this.totpLimiter.clearDailyLimits(current.account.accountId, { globalOnly: scope === 'global' })
result = { ...passwordResult, twoFactorDailyCleared: totpResult.cleared }
} else {
result = scope === 'global'
? this.limiter.clearGlobal(credential)
: this.limiter.clear(current.account.accountId, credential)
}
await this.report('access.authentication-retry.cleared', {
accountId: current.account.accountId,
scope,
credential,
cleared: result.cleared,
})
return { status: 'cleared', scope, credential, ...result }
}
async clearRecoverySessions(value = {}) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator account is unavailable', 409)
}
if (value.managementOnly !== undefined && typeof value.managementOnly !== 'boolean') {
throw new AccessError('REQUEST_INVALID', 'session clear scope is invalid')
}
const managementOnly = value.managementOnly === true
const revoked = managementOnly ? this.sessions.revokeKind('management') : this.sessions.revokeAll()
this.exchanges.clear?.()
await this.report('access.sessions.cleared', { managementOnly, revoked })
return { status: 'cleared', scope: managementOnly ? 'management' : 'all', revoked }
}
async authenticationRetryStatus(value = {}) {
const current = await this.store.state()
if (current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator account is unavailable', 409)
}
if (value.credential !== 'totp' || typeof value.source !== 'string'
|| value.source.length === 0 || value.source.length > 128) {
throw new AccessError('REQUEST_INVALID', 'authentication retry status request is invalid')
}
return this.totpLimiter.retry(current.account.accountId, value.source)
}
async replaceRecoveryAccount(value, operation, { revoke = 'none', auditOperation } = {}) {
const current = await this.store.state()
if (current.account === undefined) throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator account is unavailable', 409)
if (value.revision !== current.account.revision) throw new AccessError('REVISION_CONFLICT', 'account revision changed', 409)
const account = { ...current.account, revision: identifier(), updatedAt: new Date().toISOString() }
await operation(account, current.account)
const next = await this.store.replaceAccount(account, current.account.revision)
const managementSessionsBefore = this.sessions.countKind?.('management') ?? 0
const allSessionsRevoked = revoke === 'all'
const managementSessionsRevoked = revoke === 'management'
? this.sessions.revokeKind('management')
: allSessionsRevoked ? managementSessionsBefore : 0
if (allSessionsRevoked) this.sessions.revokeAll()
if (allSessionsRevoked) this.exchanges.clear?.()
const result = {
account: publicAccount(next),
currentManagementSessionRevoked: managementSessionsRevoked > 0,
managementSessionsRevoked,
allSessionsRevoked,
}
await this.report('access.recovery-account.changed', {
operation: auditOperation,
managementSessionsRevoked,
allSessionsRevoked,
})
return result
}
async setRecoveryUsername(value) {
const normalized = normalizeUsername(value.username)
return this.replaceRecoveryAccount(value, async account => { account.username = normalized }, {
auditOperation: 'set-username',
})
}
async resetRecoveryPassword(value) {
const verifier = await this.store.createVerifier(value.password)
return this.replaceRecoveryAccount(value, async account => {
account.mainCredential = { ...verifier, version: account.mainCredential.version + 1 }
}, { revoke: 'all', auditOperation: 'reset-password' })
}
async resetRecoveryAccess(value) {
const action = value.managementPasswordAction
if (!['preserve', 'disable', 'reset'].includes(action)) {
throw new AccessError('REQUEST_INVALID', 'management password action is invalid')
}
const totpAction = value.totpAction ?? 'preserve'
if (!['preserve', 'disable'].includes(totpAction)) {
throw new AccessError('REQUEST_INVALID', 'two-factor action is invalid')
}
const username = value.username === undefined ? null : normalizeUsername(value.username)
const mainPassword = value.password === undefined ? null : normalizePassword(value.password)
const managementPassword = action === 'reset' ? normalizePassword(value.managementPassword) : null
const currentTotpEnabled = accountTotp((await this.store.state()).account).enabled
if (username === null && mainPassword === null && action === 'preserve'
&& (totpAction === 'preserve' || !currentTotpEnabled)) {
throw new AccessError('REQUEST_INVALID', 'no access changes were selected')
}
if (managementPassword !== null && mainPassword !== null && managementPassword === mainPassword) {
throw new AccessError('PASSWORDS_MUST_DIFFER', 'main and Management console passwords must differ')
}
const revoke = mainPassword !== null ? 'all'
: action === 'reset' ? 'management'
: action === 'disable' ? 'none'
: 'none'
return this.replaceRecoveryAccount(value, async (account, current) => {
if (managementPassword !== null && mainPassword === null
&& await verifyCredential(managementPassword, current.mainCredential)) {
throw new AccessError('PASSWORDS_MUST_DIFFER', 'main and Management console passwords must differ')
}
if (username !== null) account.username = username
if (mainPassword !== null) {
const verifier = await this.store.createVerifier(mainPassword)
account.mainCredential = { ...verifier, version: account.mainCredential.version + 1 }
}
if (action !== 'preserve') {
const version = account.managementAdditionalCredential.version + 1
account.managementAdditionalCredential = action === 'disable'
? { enabled: false, version, verifier: null, changedAt: new Date().toISOString() }
: {
enabled: true,
version,
verifier: { ...await this.store.createVerifier(managementPassword), version },
changedAt: new Date().toISOString(),
}
}
if (totpAction === 'disable' && accountTotp(current).enabled) {
account.totp = {
enabled: false,
version: accountTotp(current).version + 1,
secret: null,
changedAt: new Date().toISOString(),
}
}
}, { revoke, auditOperation: 'reset-access' })
}
async resetRecoveryManagementPassword(value) {
const verifier = await this.store.createVerifier(value.password)
return this.replaceRecoveryAccount(value, async account => {
account.managementAdditionalCredential = {
enabled: true,
version: account.managementAdditionalCredential.version + 1,
verifier: { ...verifier, version: account.managementAdditionalCredential.version + 1 },
changedAt: new Date().toISOString(),
}
}, { revoke: 'management', auditOperation: 'reset-management-password' })
}
async disableRecoveryManagementPassword(value) {
return this.replaceRecoveryAccount(value, async account => {
account.managementAdditionalCredential = {
enabled: false,
version: account.managementAdditionalCredential.version + 1,
verifier: null,
changedAt: new Date().toISOString(),
}
}, { revoke: 'none', auditOperation: 'disable-management-password' })
}
async createManagementTransition(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const authorization = this.consumeInternalCapability(value, current.account)
const session = this.sessions.details(authorization.sessionId)
if (session?.kind !== 'management') throw new AccessError('SESSION_INVALID', 'Management session is invalid', 401)
const runtimeCapabilities = await this.runtimeCapabilities()
if (runtimeCapabilities.dshRootCapabilityEffective === true
&& value.mode !== current.account.managementAccess.mode) {
throw new AccessError('ACCESS_MODE_LOCKED', 'Management access mode cannot change while DSH Root capability is enabled', 409)
}
const isolatedEntry = normalizeManagementAccess(value.mode, value.mode === 'isolated'
&& value.isolatedEntry?.kind === 'local-only'
? { ...value.isolatedEntry, managementLocalOrigin: value.isolatedEntry.managementLocalOrigin ?? value.candidateOrigin }
: value.isolatedEntry)
const candidateOrigin = value.mode === 'isolated'
? (() => {
if (isolatedEntry.kind === 'public') return isolatedEntry.managementPublicOrigin
try {
const parsed = new URL(value.candidateOrigin)
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.origin !== value.candidateOrigin
|| !isLoopbackHostname(parsed.hostname)) throw new Error('not loopback')
return parsed.origin
} catch { throw new AccessError('ACCESS_ENTRY_INVALID', 'local Management origin is invalid') }
})()
: null
const transition = this.transitions.create({
account: current.account,
instanceId: current.initialization.instanceId,
sessionId: authorization.sessionId,
sourceOrigin: session.origin,
sourceDshOrigin: session.sourceDshOrigin,
sourceDshSessionId: session.sourceDshSessionId,
mode: value.mode,
isolatedEntry,
candidateOrigin,
})
await this.report('access.management-transition.created', { mode: value.mode })
return { transition, instanceId: current.initialization.instanceId }
}
async probeManagementTransition(value) {
const current = await this.store.state()
const result = this.transitions.probe({
...value,
instanceId: current.initialization?.instanceId,
})
if (result === undefined) throw new AccessError('TRANSITION_PROBE_INVALID', 'Management origin probe is invalid or expired', 401)
await this.report('access.management-transition.probed')
return result
}
async commitManagementTransition(value) {
return this.serialized(async () => {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const authorization = this.consumeInternalCapability(value, current.account)
const transitionValue = {
transitionId: value.transitionId,
proof: value.proof,
account: current.account,
sessionId: authorization.sessionId,
}
const transition = this.transitions.consume(transitionValue)
if (transition === undefined) throw new AccessError('TRANSITION_INVALID', 'Management origin transition is invalid or expired', 409)
const sourceDshSession = this.sessions.details(transition.sourceDshSessionId)
if (sourceDshSession?.kind !== 'dsh' || sourceDshSession.origin !== transition.sourceDshOrigin) {
throw new AccessError('SESSION_INVALID', 'source DSH session is invalid', 401)
}
const targetOrigin = transition.mode === 'isolated'
? transition.candidateOrigin
: transition.sourceDshOrigin
if (typeof targetOrigin !== 'string' || targetOrigin.length === 0) {
throw new AccessError('TRANSITION_TARGET_UNAVAILABLE', 'Management transition target is unavailable', 409)
}
const account = {
...current.account,
revision: identifier(),
updatedAt: new Date().toISOString(),
managementAccess: {
mode: transition.mode,
version: current.account.managementAccess.version + 1,
isolatedEntry: transition.isolatedEntry,
dshPublicOrigin: transition.mode === 'isolated' ? transition.sourceDshOrigin : null,
changedAt: new Date().toISOString(),
},
}
const next = await this.store.replaceAccount(account, current.account.revision)
this.sessions.refreshDshSession(transition.sourceDshSessionId, next)
const continuation = this.transitions.createContinuation({
account: next,
targetOrigin,
sourceDshOrigin: transition.sourceDshOrigin,
sourceDshSessionId: transition.sourceDshSessionId,
})
if (continuation === null) throw new AccessError('TRANSITION_TARGET_UNAVAILABLE', 'Management transition continuation is unavailable', 409)
this.sessions.revokeKind('management')
this.exchanges.clear?.()
await this.report('access.management-origin.changed', { mode: transition.mode })
return {
account: publicAccount(next),
continuation,
targetOrigin,
loginOrigin: transition.mode === 'isolated' ? transition.candidateOrigin : transition.sourceDshOrigin,
}
})
}
async consumeManagementContinuation(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const continuation = this.transitions.consumeContinuation({
token: value.token, account: current.account, targetOrigin: value.origin,
})
if (continuation === undefined) throw new AccessError('CONTINUATION_INVALID', 'Management continuation is invalid or expired', 401)
const source = this.sessions.details(continuation.sourceDshSessionId)
if (source?.kind !== 'dsh' || source.origin !== continuation.sourceDshOrigin) {
throw new AccessError('CONTINUATION_INVALID', 'Management continuation is invalid or expired', 401)
}
const session = this.sessions.issue('management', current.account, {
origin: value.origin,
sourceDshOrigin: continuation.sourceDshOrigin,
sourceDshSessionId: continuation.sourceDshSessionId,
})
await this.report('access.management-continuation.consumed')
return { session }
}
async updateAuthenticationSettings(value) {
const current = await this.store.state()
if (current.state !== 'initialized' || current.account === undefined) {
throw new AccessError('ACCESS_NOT_INITIALIZED', 'administrator access is not initialized', 409)
}
const authorization = this.consumeInternalCapability(value, current.account)
const account = { ...current.account, revision: identifier(), updatedAt: new Date().toISOString() }
const usernameChanged = value.username !== undefined && normalizeUsername(value.username) !== current.account.username
const mainPasswordChanged = value.password !== undefined
const additionalChanged = value.additionalPassword !== undefined
|| (value.additionalEnabled !== undefined
&& value.additionalEnabled !== current.account.managementAdditionalCredential.enabled)
if (value.totpEnabled !== undefined && typeof value.totpEnabled !== 'boolean') {
throw new AccessError('REQUEST_INVALID', 'two-factor authentication setting is invalid')
}
const totpChanged = value.totpEnabled !== undefined
&& value.totpEnabled !== accountTotp(current.account).enabled
if (value.mode !== undefined || value.isolatedEntry !== undefined) {
throw new AccessError('TRANSITION_REQUIRED', 'Management access changes require a verified transition', 409)
}
if (!usernameChanged && !mainPasswordChanged && !additionalChanged && !totpChanged) {
return {
account: publicAccount(current.account),
changed: false,
currentManagementSessionRevoked: false,
managementSessionsRevoked: 0,
allSessionsRevoked: false,
}
}
const managementSession = this.sessions.details(authorization.sessionId)
const sourceSession = this.sessions.details(managementSession?.sourceDshSessionId)
const authenticationSourceId = sourceSession?.authenticationSource ?? 'unknown'
if (mainPasswordChanged || additionalChanged || totpChanged) {
await this.verifyFreshAuthentication(
current.account,
value.currentPassword,
authenticationSourceId,
)
}
let enrollment
let disableConfirmation
if (totpChanged && value.totpEnabled === true) {
enrollment = this.totpFlows.enrollment(value.totpEnrollmentToken, current.account, authorization.sessionId)
if (enrollment === undefined) {
throw new AccessError('TOTP_ENROLLMENT_INVALID', 'two-factor authentication enrollment is invalid or expired', 401)
}
if (enrollment.value.confirmed !== true) {
throw new AccessError('TOTP_ENROLLMENT_UNCONFIRMED', 'two-factor authentication enrollment is not confirmed', 409)
}
} else if (totpChanged) {
disableConfirmation = this.totpFlows.disableConfirmation(
value.totpDisableConfirmationToken,
current.account,
authorization.sessionId,
)
if (disableConfirmation === undefined) {
throw new AccessError('TOTP_DISABLE_CONFIRMATION_INVALID', 'two-factor disable confirmation is invalid or expired', 401)
}
if (disableConfirmation.value.confirmed !== true) {
throw new AccessError('TOTP_DISABLE_CONFIRMATION_UNCONFIRMED', 'two-factor disable confirmation is not confirmed', 409)
}
}
if (value.username !== undefined) account.username = normalizeUsername(value.username)
if (value.password !== undefined) {
if (current.account.managementAdditionalCredential.enabled
&& value.additionalPassword === undefined
&& await this.verify(value.password, current.account.managementAdditionalCredential.verifier)) {
throw new AccessError('PASSWORDS_MUST_DIFFER', 'main and Management console passwords must differ')
}
const verifier = await this.store.createVerifier(value.password)
account.mainCredential = { ...verifier, version: current.account.mainCredential.version + 1 }
}
if (value.additionalPassword !== undefined || value.additionalEnabled !== undefined) {
const enabled = value.additionalEnabled === true
if (!enabled) {
account.managementAdditionalCredential = {
enabled: false,
version: current.account.managementAdditionalCredential.version + 1,
verifier: null,
changedAt: new Date().toISOString(),
}
} else if (value.additionalPassword !== undefined) {
if (typeof value.additionalPassword !== 'string') throw new AccessError('PASSWORD_REQUIRED', 'Management console password is required')
const equalsMain = value.password !== undefined
? normalizePassword(value.additionalPassword) === normalizePassword(value.password)
: await this.verify(value.additionalPassword, current.account.mainCredential)
if (equalsMain) throw new AccessError('PASSWORDS_MUST_DIFFER', 'main and Management console passwords must differ')
const verifier = await this.store.createVerifier(value.additionalPassword)
account.managementAdditionalCredential = {
enabled: true,
version: current.account.managementAdditionalCredential.version + 1,
verifier: { ...verifier, version: current.account.managementAdditionalCredential.version + 1 },
changedAt: new Date().toISOString(),
}
} else if (!current.account.managementAdditionalCredential.enabled) {
throw new AccessError('PASSWORD_REQUIRED', 'Management console password is required')
}
}
if (totpChanged) {
const version = accountTotp(current.account).version + 1
account.totp = value.totpEnabled
? { enabled: true, version, secret: enrollment.value.secret, changedAt: new Date().toISOString() }
: { enabled: false, version, secret: null, changedAt: new Date().toISOString() }
}
const next = await this.store.replaceAccount(account, current.account.revision)
if (enrollment !== undefined) this.totpFlows.consumeEnrollment(enrollment.key)
if (disableConfirmation !== undefined) this.totpFlows.consumeDisableConfirmation(disableConfirmation.key)
const allSessionsRevoked = mainPasswordChanged
const revokedSessionCount = allSessionsRevoked
? this.sessions.revokeAll()
: additionalChanged ? this.sessions.revokeKind('management') : 0
this.exchanges.clear?.()
if (allSessionsRevoked) this.totpFlows.clear()
await this.report('access.authentication.settings.changed', { usernameChanged })
return {
account: publicAccount(next),
changed: true,
currentManagementSessionRevoked: allSessionsRevoked || additionalChanged,
managementSessionsRevoked: allSessionsRevoked || additionalChanged ? revokedSessionCount : 0,
allSessionsRevoked,
}
}
}
export function createAccessHttpServer({ service, surface = 'access' }) {
return createServer((request, response) => {
void (async () => {
const pathname = new URL(request.url ?? '/', 'http://access.internal').pathname
if (request.method === 'GET' && pathname === '/v1/status') return send(response, 200, await service.status())
if (surface === 'recovery') {
const value = await body(request)
if (request.method === 'GET' && pathname === '/v1/recovery/status') return send(response, 200, await service.recoveryStatus())
if (request.method === 'POST' && pathname === '/v1/recovery/set-username') return send(response, 200, await service.setRecoveryUsername(value))
if (request.method === 'POST' && pathname === '/v1/recovery/reset-access') return send(response, 200, await service.resetRecoveryAccess(value))
if (request.method === 'POST' && pathname === '/v1/recovery/reset-password') return send(response, 200, await service.resetRecoveryPassword(value))
if (request.method === 'POST' && pathname === '/v1/recovery/reset-management-password') return send(response, 200, await service.resetRecoveryManagementPassword(value))
if (request.method === 'POST' && pathname === '/v1/recovery/disable-management-password') return send(response, 200, await service.disableRecoveryManagementPassword(value))
if (request.method === 'POST' && pathname === '/v1/recovery/generate-key') return send(response, 201, await service.generateAuthenticationResetKey())
if (request.method === 'POST' && pathname === '/v1/recovery/clear-retry') return send(response, 200, await service.clearAuthenticationRetry(value))
if (request.method === 'POST' && pathname === '/v1/recovery/clear-sessions') return send(response, 200, await service.clearRecoverySessions(value))
return send(response, 404, { error: 'not found', code: 'NOT_FOUND' })
}
const value = await body(request)
if (request.method === 'POST' && pathname === '/v1/classify') return send(response, 200, await service.classify(value))
if (request.method === 'POST' && pathname === '/v1/initialize') return send(response, 201, await service.initialize(value))
if (request.method === 'POST' && pathname === '/v1/authenticate') return send(response, 200, await service.authenticate(value))
if (request.method === 'POST' && pathname === '/v1/dsh/initialize') return send(response, 201, await service.initializeDsh(value))
if (request.method === 'POST' && pathname === '/v1/dsh/reset-authentication') return send(response, 201, await service.resetDshAuthentication(value))
if (request.method === 'POST' && pathname === '/v1/dsh/login') return send(response, 200, await service.loginDsh(value))
if (request.method === 'POST' && pathname === '/v1/dsh/totp/complete') return send(response, 200, await service.completeDshTotp(value))
if (request.method === 'POST' && pathname === '/v1/authentication-retry/status') return send(response, 200, await service.authenticationRetryStatus(value))
if (request.method === 'POST' && pathname === '/v1/sessions/validate') return send(response, 200, await service.validateSession(value))
if (request.method === 'POST' && pathname === '/v1/sessions/logout') return send(response, 200, await service.logout(value))
if (request.method === 'POST' && pathname === '/v1/dsh/browser-logout') return send(response, 200, await service.logoutDshBrowser(value))
if (request.method === 'POST' && pathname === '/v1/management/handoffs') return send(response, 201, await service.createManagementHandoff(value))
if (request.method === 'POST' && pathname === '/v1/management/handoffs/consume') return send(response, 200, await service.consumeManagementHandoff(value))
if (request.method === 'POST' && pathname === '/v1/management/pending/complete') return send(response, 200, await service.completeManagementLogin(value))
if (request.method === 'POST' && pathname === '/v1/capabilities') return send(response, 201, await service.issueCapability(value))
if (request.method === 'POST' && pathname === '/v1/dsh/capabilities') return send(response, 201, await service.issuePluginCapability(value))
if (request.method === 'POST' && pathname === '/v1/capabilities/consume') return send(response, 200, await service.consumeCapability(value))
if (request.method === 'POST' && pathname === '/v1/management/settings') return send(response, 200, await service.authenticationSettings(value))
if (request.method === 'POST' && pathname === '/v1/management/totp/enrollments') return send(response, 201, await service.beginTotpEnrollment(value))
if (request.method === 'POST' && pathname === '/v1/management/totp/enrollments/confirm') return send(response, 200, await service.confirmTotpEnrollment(value))
if (request.method === 'POST' && pathname === '/v1/management/totp/enrollments/cancel') return send(response, 200, await service.cancelTotpEnrollment(value))
if (request.method === 'POST' && pathname === '/v1/management/totp/disable-confirmations') return send(response, 201, await service.beginTotpDisableConfirmation(value))
if (request.method === 'POST' && pathname === '/v1/management/totp/disable-confirmations/confirm') return send(response, 200, await service.confirmTotpDisable(value))
if (request.method === 'POST' && pathname === '/v1/management/totp/disable-confirmations/cancel') return send(response, 200, await service.cancelTotpDisableConfirmation(value))
if (request.method === 'POST' && pathname === '/v1/management/sessions/revoke') return send(response, 200, await service.revokeBrowserSessions(value))
if (request.method === 'POST' && pathname === '/v1/management/transitions') return send(response, 201, await service.createManagementTransition(value))
if (request.method === 'POST' && pathname === '/v1/management/transitions/probe') return send(response, 200, await service.probeManagementTransition(value))
if (request.method === 'POST' && pathname === '/v1/management/transitions/commit') return send(response, 200, await service.commitManagementTransition(value))
if (request.method === 'POST' && pathname === '/v1/management/continuations/consume') return send(response, 200, await service.consumeManagementContinuation(value))
if (request.method === 'POST' && pathname === '/v1/management/auth-settings') return send(response, 200, await service.updateAuthenticationSettings(value))
send(response, 404, { error: 'not found', code: 'NOT_FOUND' })
})().catch(async error => {
const status = error instanceof AccessError ? error.statusCode : 500
const failedPathname = new URL(request.url ?? '/', 'http://access.internal').pathname
const recoveryOperation = surface === 'recovery' && request.method === 'POST'
? RECOVERY_OPERATIONS.get(failedPathname) : undefined
if (recoveryOperation !== undefined) {
await service.report('access.recovery-operation.failed', {
operation: recoveryOperation,
code: error instanceof AccessError ? error.code : 'INTERNAL_ERROR',
})
}
const accessFailureEvent = surface === 'access' && request.method === 'POST'
? ACCESS_FAILURE_EVENTS.get(failedPathname) : undefined
if (accessFailureEvent !== undefined) {
await service.report(accessFailureEvent, {
code: error instanceof AccessError ? error.code : 'INTERNAL_ERROR',
})
}
if (!(error instanceof AccessError)) await service.report('access.request.failed', {
error, method: request.method ?? null, pathname: request.url ?? null,
})
send(response, status, accessErrorBody(error), Number.isInteger(error?.details?.retryAfterSeconds)
? { 'retry-after': String(error.details.retryAfterSeconds) } : {})
})
})
}