Barazo AppView backend barazo.forum
at main 161 lines 5.5 kB view raw
1import type { FastifyReply, FastifyRequest } from 'fastify' 2import type { SessionService } from './session.js' 3import type { DidDocumentVerifier, DidVerificationResult } from '../lib/did-document-verifier.js' 4import type { Logger } from '../lib/logger.js' 5 6// --------------------------------------------------------------------------- 7// Types 8// --------------------------------------------------------------------------- 9 10/** User info attached to authenticated requests. */ 11export interface RequestUser { 12 did: string 13 handle: string 14 sid: string 15} 16 17/** Auth middleware hooks returned by createAuthMiddleware. */ 18export interface AuthMiddleware { 19 requireAuth: (request: FastifyRequest, reply: FastifyReply) => Promise<void> 20 optionalAuth: (request: FastifyRequest, reply: FastifyReply) => Promise<void> 21} 22 23// --------------------------------------------------------------------------- 24// Extend Fastify's request type 25// --------------------------------------------------------------------------- 26 27declare module 'fastify' { 28 interface FastifyRequest { 29 /** Authenticated user info (set by requireAuth or optionalAuth middleware). */ 30 user?: RequestUser 31 } 32} 33 34// --------------------------------------------------------------------------- 35// Helpers 36// --------------------------------------------------------------------------- 37 38/** 39 * Extract Bearer token from the Authorization header. 40 * Returns the token string if valid, or undefined if missing/malformed. 41 */ 42function extractBearerToken(request: FastifyRequest): string | undefined { 43 const authHeader = request.headers.authorization 44 if (!authHeader || !authHeader.startsWith('Bearer ')) { 45 return undefined 46 } 47 48 const token = authHeader.slice('Bearer '.length) 49 if (token.length === 0) { 50 return undefined 51 } 52 53 return token 54} 55 56/** Check if a DID verification failure is a transient resolution error. */ 57function isResolutionFailure(result: DidVerificationResult): boolean { 58 return !result.active && result.reason === 'DID document resolution failed' 59} 60 61// --------------------------------------------------------------------------- 62// Factory 63// --------------------------------------------------------------------------- 64 65/** 66 * Create auth middleware hooks for Fastify route preHandler. 67 * 68 * @param sessionService - Session service for token validation 69 * @param didVerifier - DID document verifier for checking DID status 70 * @param logger - Pino logger instance 71 * @returns Object with requireAuth and optionalAuth hooks 72 */ 73export function createAuthMiddleware( 74 sessionService: SessionService, 75 didVerifier: DidDocumentVerifier, 76 logger: Logger 77): AuthMiddleware { 78 /** 79 * Require authentication. Returns 401 if no valid token, 502 if service error. 80 * On success, sets `request.user` with the authenticated user info. 81 * Verifies the DID document is still active via the PLC directory (cached). 82 */ 83 async function requireAuth(request: FastifyRequest, reply: FastifyReply): Promise<void> { 84 const token = extractBearerToken(request) 85 if (token === undefined) { 86 await reply.status(401).send({ error: 'Authentication required' }) 87 return 88 } 89 90 try { 91 const session = await sessionService.validateAccessToken(token) 92 if (!session) { 93 await reply.status(401).send({ error: 'Invalid or expired token' }) 94 return 95 } 96 97 // Verify DID document is still active 98 const didResult = await didVerifier.verify(session.did) 99 if (!didResult.active) { 100 if (isResolutionFailure(didResult)) { 101 // Transient failure with no cached data -- fail closed 102 logger.error({ did: session.did, reason: didResult.reason }, 'DID verification failed') 103 await reply.status(502).send({ error: 'Service temporarily unavailable' }) 104 } else { 105 // DID is definitively deactivated/tombstoned/not found 106 logger.warn({ did: session.did, reason: didResult.reason }, 'DID is no longer active') 107 await reply.status(401).send({ error: 'DID is no longer active' }) 108 } 109 return 110 } 111 112 request.user = { 113 did: session.did, 114 handle: session.handle, 115 sid: session.sid, 116 } 117 } catch (err: unknown) { 118 logger.error({ err }, 'Token validation failed in requireAuth') 119 await reply.status(502).send({ error: 'Service temporarily unavailable' }) 120 } 121 } 122 123 /** 124 * Optional authentication. If a valid token is present, sets `request.user`. 125 * If no token, invalid token, DID inactive, or service error: continues 126 * with `request.user` undefined. 127 */ 128 async function optionalAuth(request: FastifyRequest, _reply: FastifyReply): Promise<void> { 129 const token = extractBearerToken(request) 130 if (token === undefined) { 131 return 132 } 133 134 try { 135 const session = await sessionService.validateAccessToken(token) 136 if (!session) { 137 return 138 } 139 140 // Verify DID document is still active 141 const didResult = await didVerifier.verify(session.did) 142 if (!didResult.active) { 143 logger.warn( 144 { did: session.did, reason: didResult.reason }, 145 'DID verification failed in optionalAuth, continuing unauthenticated' 146 ) 147 return 148 } 149 150 request.user = { 151 did: session.did, 152 handle: session.handle, 153 sid: session.sid, 154 } 155 } catch (err: unknown) { 156 logger.warn({ err }, 'Token validation failed in optionalAuth, continuing unauthenticated') 157 } 158 } 159 160 return { requireAuth, optionalAuth } 161}