A social knowledge tool for researchers built on ATProto
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}