A social knowledge tool for researchers built on ATProto
at main 177 lines 5.0 kB view raw
1import { Request, Response, NextFunction } from 'express'; 2import { ITokenService } from '../../../../modules/user/application/services/ITokenService'; 3import { CookieService } from '../services/CookieService'; 4 5export interface AuthenticatedRequest extends Request { 6 did?: string; 7} 8 9export class AuthMiddleware { 10 constructor( 11 private tokenService: ITokenService, 12 private cookieService: CookieService, 13 ) {} 14 15 /** 16 * Extract access token from request - checks both cookies and Authorization header 17 * Priority: Cookie > Bearer token (for backward compatibility) 18 */ 19 private extractAccessToken(req: AuthenticatedRequest): string | undefined { 20 // First, try to get token from cookie 21 const cookieToken = this.cookieService.getAccessToken(req); 22 if (cookieToken) { 23 return cookieToken; 24 } 25 26 // Fallback to Authorization header for backward compatibility 27 const authHeader = req.headers.authorization; 28 if (authHeader && authHeader.startsWith('Bearer ')) { 29 return authHeader.substring(7); // Remove 'Bearer ' prefix 30 } 31 32 return undefined; 33 } 34 35 /** 36 * Require authentication - accepts both cookie-based and Bearer token auth 37 * This is the unified method that supports both authentication methods 38 */ 39 public ensureAuthenticated() { 40 return async ( 41 req: AuthenticatedRequest, 42 res: Response, 43 next: NextFunction, 44 ): Promise<void> => { 45 try { 46 const token = this.extractAccessToken(req); 47 48 if (!token) { 49 res.status(401).json({ message: 'No access token provided' }); 50 return; 51 } 52 53 // Validate token 54 const didResult = await this.tokenService.validateToken(token); 55 56 if (didResult.isErr() || !didResult.value) { 57 res.status(403).json({ message: 'Invalid or expired token' }); 58 return; 59 } 60 61 // Attach user DID to request for use in controllers 62 req.did = didResult.value; 63 64 // Continue to the next middleware or controller 65 next(); 66 } catch (error) { 67 res.status(500).json({ message: 'Authentication error' }); 68 } 69 }; 70 } 71 72 /** 73 * Optional authentication - accepts both cookie-based and Bearer token auth 74 * Continues even if no token is provided 75 */ 76 public optionalAuth() { 77 return async ( 78 req: AuthenticatedRequest, 79 res: Response, 80 next: NextFunction, 81 ) => { 82 try { 83 const token = this.extractAccessToken(req); 84 85 if (!token) { 86 // No token, but that's okay - continue without authentication 87 return next(); 88 } 89 90 // Validate token 91 const didResult = await this.tokenService.validateToken(token); 92 93 if (didResult.isOk() && didResult.value) { 94 // Attach user DID to request for use in controllers 95 req.did = didResult.value; 96 } 97 98 // Continue to the controller regardless of token validity 99 next(); 100 } catch (error) { 101 // Continue without authentication in case of error 102 next(); 103 } 104 }; 105 } 106 107 /** 108 * Require Bearer token authentication only (legacy support) 109 * Use this when you specifically need Bearer token auth 110 */ 111 public requireBearerAuth() { 112 return async ( 113 req: AuthenticatedRequest, 114 res: Response, 115 next: NextFunction, 116 ): Promise<void> => { 117 try { 118 const authHeader = req.headers.authorization; 119 if (!authHeader || !authHeader.startsWith('Bearer ')) { 120 res.status(401).json({ message: 'No Bearer token provided' }); 121 return; 122 } 123 124 const token = authHeader.substring(7); 125 126 // Validate token 127 const didResult = await this.tokenService.validateToken(token); 128 129 if (didResult.isErr() || !didResult.value) { 130 res.status(403).json({ message: 'Invalid or expired token' }); 131 return; 132 } 133 134 req.did = didResult.value; 135 next(); 136 } catch (error) { 137 res.status(500).json({ message: 'Authentication error' }); 138 } 139 }; 140 } 141 142 /** 143 * Require cookie-based authentication only 144 * Use this when you specifically need cookie auth (e.g., CSRF protection) 145 */ 146 public requireCookieAuth() { 147 return async ( 148 req: AuthenticatedRequest, 149 res: Response, 150 next: NextFunction, 151 ): Promise<void> => { 152 try { 153 const token = this.cookieService.getAccessToken(req); 154 155 if (!token) { 156 res 157 .status(401) 158 .json({ message: 'No authentication cookie provided' }); 159 return; 160 } 161 162 // Validate token 163 const didResult = await this.tokenService.validateToken(token); 164 165 if (didResult.isErr() || !didResult.value) { 166 res.status(403).json({ message: 'Invalid or expired token' }); 167 return; 168 } 169 170 req.did = didResult.value; 171 next(); 172 } catch (error) { 173 res.status(500).json({ message: 'Authentication error' }); 174 } 175 }; 176 } 177}