|
1 | 1 | import { db } from '@sim/db' |
2 | 2 | import { chat, workflow } from '@sim/db/schema' |
3 | 3 | import { createLogger } from '@sim/logger' |
4 | | -import { safeCompare } from '@sim/security/compare' |
5 | 4 | import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' |
6 | 5 | import { and, eq, isNull } from 'drizzle-orm' |
7 | 6 | import type { NextRequest, NextResponse } from 'next/server' |
8 | 7 | import { isWorkspaceApiExecutionEntitled } from '@/lib/billing/core/api-access' |
9 | 8 | import { getEnv } from '@/lib/core/config/env' |
10 | 9 | import { isBillingEnabled, isFreeApiDeploymentGateEnabled } from '@/lib/core/config/env-flags' |
11 | | -import type { TokenBucketConfig } from '@/lib/core/rate-limiter' |
12 | | -import { RateLimiter } from '@/lib/core/rate-limiter' |
| 10 | +import { setDeploymentAuthCookie } from '@/lib/core/security/deployment' |
13 | 11 | import { |
14 | | - isEmailAllowed, |
15 | | - setDeploymentAuthCookie, |
16 | | - validateAuthToken, |
17 | | -} from '@/lib/core/security/deployment' |
18 | | -import { decryptSecret } from '@/lib/core/security/encryption' |
19 | | -import { getClientIp } from '@/lib/core/utils/request' |
| 12 | + type DeploymentAuthResult, |
| 13 | + validateDeploymentAuth, |
| 14 | +} from '@/lib/core/security/deployment-auth' |
20 | 15 | import { createErrorResponse } from '@/app/api/workflows/utils' |
21 | 16 |
|
22 | 17 | const logger = createLogger('ChatAuthUtils') |
23 | 18 |
|
24 | | -const rateLimiter = new RateLimiter() |
25 | | - |
26 | | -/** |
27 | | - * Throttles unauthenticated password guesses per client IP against a single |
28 | | - * deployment, mirroring the OTP/SSO IP limits. |
29 | | - */ |
30 | | -const PASSWORD_IP_RATE_LIMIT: TokenBucketConfig = { |
31 | | - maxTokens: 10, |
32 | | - refillRate: 10, |
33 | | - refillIntervalMs: 15 * 60_000, |
34 | | -} |
35 | | - |
36 | 19 | export function setChatAuthCookie( |
37 | 20 | response: NextResponse, |
38 | 21 | chatId: string, |
@@ -157,144 +140,15 @@ export async function checkChatAccess( |
157 | 140 | : { hasAccess: false } |
158 | 141 | } |
159 | 142 |
|
| 143 | +/** |
| 144 | + * Validates auth for a deployed chat. Thin wrapper over the shared |
| 145 | + * {@link validateDeploymentAuth} with the `'chat'` cookie/rate-limit namespace. |
| 146 | + */ |
160 | 147 | export async function validateChatAuth( |
161 | 148 | requestId: string, |
162 | 149 | deployment: any, |
163 | 150 | request: NextRequest, |
164 | 151 | parsedBody?: any |
165 | | -): Promise<{ authorized: boolean; error?: string; status?: number; retryAfterMs?: number }> { |
166 | | - const authType = deployment.authType || 'public' |
167 | | - |
168 | | - if (authType === 'public') { |
169 | | - return { authorized: true } |
170 | | - } |
171 | | - |
172 | | - if (authType !== 'sso') { |
173 | | - const cookieName = `chat_auth_${deployment.id}` |
174 | | - const authCookie = request.cookies.get(cookieName) |
175 | | - |
176 | | - if (authCookie && validateAuthToken(authCookie.value, deployment.id, deployment.password)) { |
177 | | - return { authorized: true } |
178 | | - } |
179 | | - } |
180 | | - |
181 | | - if (authType === 'password') { |
182 | | - if (request.method === 'GET') { |
183 | | - return { authorized: false, error: 'auth_required_password' } |
184 | | - } |
185 | | - |
186 | | - try { |
187 | | - if (!parsedBody) { |
188 | | - return { authorized: false, error: 'Password is required' } |
189 | | - } |
190 | | - |
191 | | - const { password, input } = parsedBody |
192 | | - |
193 | | - if (input && !password) { |
194 | | - return { authorized: false, error: 'auth_required_password' } |
195 | | - } |
196 | | - |
197 | | - if (!password) { |
198 | | - return { authorized: false, error: 'Password is required' } |
199 | | - } |
200 | | - |
201 | | - if (!deployment.password) { |
202 | | - logger.error(`[${requestId}] No password set for password-protected chat: ${deployment.id}`) |
203 | | - return { authorized: false, error: 'Authentication configuration error' } |
204 | | - } |
205 | | - |
206 | | - const ip = getClientIp(request) |
207 | | - const ipRateLimit = await rateLimiter.checkRateLimitDirect( |
208 | | - `chat-password:ip:${deployment.id}:${ip}`, |
209 | | - PASSWORD_IP_RATE_LIMIT |
210 | | - ) |
211 | | - if (!ipRateLimit.allowed) { |
212 | | - logger.warn( |
213 | | - `[${requestId}] Password attempt IP rate limit exceeded for chat ${deployment.id} from ${ip}` |
214 | | - ) |
215 | | - return { |
216 | | - authorized: false, |
217 | | - error: 'Too many attempts. Please try again later.', |
218 | | - status: 429, |
219 | | - retryAfterMs: ipRateLimit.retryAfterMs ?? PASSWORD_IP_RATE_LIMIT.refillIntervalMs, |
220 | | - } |
221 | | - } |
222 | | - |
223 | | - const { decrypted } = await decryptSecret(deployment.password) |
224 | | - if (!safeCompare(password, decrypted)) { |
225 | | - return { authorized: false, error: 'Invalid password' } |
226 | | - } |
227 | | - |
228 | | - return { authorized: true } |
229 | | - } catch (error) { |
230 | | - logger.error(`[${requestId}] Error validating password:`, error) |
231 | | - return { authorized: false, error: 'Authentication error' } |
232 | | - } |
233 | | - } |
234 | | - |
235 | | - if (authType === 'email') { |
236 | | - if (request.method === 'GET') { |
237 | | - return { authorized: false, error: 'auth_required_email' } |
238 | | - } |
239 | | - |
240 | | - try { |
241 | | - if (!parsedBody) { |
242 | | - return { authorized: false, error: 'Email is required' } |
243 | | - } |
244 | | - |
245 | | - const { email, input } = parsedBody |
246 | | - |
247 | | - if (input && !email) { |
248 | | - return { authorized: false, error: 'auth_required_email' } |
249 | | - } |
250 | | - |
251 | | - if (!email) { |
252 | | - return { authorized: false, error: 'Email is required' } |
253 | | - } |
254 | | - |
255 | | - const allowedEmails = deployment.allowedEmails || [] |
256 | | - |
257 | | - if (isEmailAllowed(email, allowedEmails)) { |
258 | | - return { authorized: false, error: 'otp_required' } |
259 | | - } |
260 | | - |
261 | | - return { authorized: false, error: 'Email not authorized' } |
262 | | - } catch (error) { |
263 | | - logger.error(`[${requestId}] Error validating email:`, error) |
264 | | - return { authorized: false, error: 'Authentication error' } |
265 | | - } |
266 | | - } |
267 | | - |
268 | | - if (authType === 'sso') { |
269 | | - try { |
270 | | - if (request.method !== 'GET' && !parsedBody) { |
271 | | - return { authorized: false, error: 'SSO authentication is required' } |
272 | | - } |
273 | | - |
274 | | - const { getSession } = await import('@/lib/auth') |
275 | | - const session = await getSession() |
276 | | - |
277 | | - if (!session || !session.user) { |
278 | | - return { authorized: false, error: 'auth_required_sso' } |
279 | | - } |
280 | | - |
281 | | - const userEmail = session.user.email |
282 | | - if (!userEmail) { |
283 | | - return { authorized: false, error: 'SSO session does not contain email' } |
284 | | - } |
285 | | - |
286 | | - const allowedEmails = deployment.allowedEmails || [] |
287 | | - |
288 | | - if (isEmailAllowed(userEmail, allowedEmails)) { |
289 | | - return { authorized: true } |
290 | | - } |
291 | | - |
292 | | - return { authorized: false, error: 'Your email is not authorized to access this chat' } |
293 | | - } catch (error) { |
294 | | - logger.error(`[${requestId}] Error validating SSO:`, error) |
295 | | - return { authorized: false, error: 'SSO authentication error' } |
296 | | - } |
297 | | - } |
298 | | - |
299 | | - return { authorized: false, error: 'Unsupported authentication type' } |
| 152 | +): Promise<DeploymentAuthResult> { |
| 153 | + return validateDeploymentAuth(requestId, deployment, request, parsedBody, 'chat') |
300 | 154 | } |
0 commit comments