Skip to content

Commit 3db061b

Browse files
waleedlatif1claude
andcommitted
fix: bind auth tokens to deployment password for immediate revocation
Include a SHA-256 hash of the encrypted password in the HMAC-signed token payload. Changing the deployment password now immediately invalidates all existing auth cookies, restoring the pre-HMAC behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1cc6ed4 commit 3db061b

File tree

6 files changed

+49
-19
lines changed

6 files changed

+49
-19
lines changed

apps/sim/app/api/chat/[identifier]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export async function POST(
141141
if ((password || email) && !input) {
142142
const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request)
143143

144-
setChatAuthCookie(response, deployment.id, deployment.authType)
144+
setChatAuthCookie(response, deployment.id, deployment.authType, deployment.password)
145145

146146
return response
147147
}
@@ -327,7 +327,7 @@ export async function GET(
327327
if (
328328
deployment.authType !== 'public' &&
329329
authCookie &&
330-
validateAuthToken(authCookie.value, deployment.id)
330+
validateAuthToken(authCookie.value, deployment.id, deployment.password)
331331
) {
332332
return addCorsHeaders(
333333
createSuccessResponse({

apps/sim/app/api/chat/utils.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
1313

1414
const logger = createLogger('ChatAuthUtils')
1515

16-
export function setChatAuthCookie(response: NextResponse, chatId: string, type: string): void {
17-
setDeploymentAuthCookie(response, 'chat', chatId, type)
16+
export function setChatAuthCookie(
17+
response: NextResponse,
18+
chatId: string,
19+
type: string,
20+
encryptedPassword?: string | null
21+
): void {
22+
setDeploymentAuthCookie(response, 'chat', chatId, type, encryptedPassword)
1823
}
1924

2025
/**
@@ -93,7 +98,7 @@ export async function validateChatAuth(
9398
const cookieName = `chat_auth_${deployment.id}`
9499
const authCookie = request.cookies.get(cookieName)
95100

96-
if (authCookie && validateAuthToken(authCookie.value, deployment.id)) {
101+
if (authCookie && validateAuthToken(authCookie.value, deployment.id, deployment.password)) {
97102
return { authorized: true }
98103
}
99104

apps/sim/app/api/form/[identifier]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export async function POST(
157157
// If only authentication credentials provided (no form data), just return authenticated
158158
if ((password || email) && !formData) {
159159
const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request)
160-
setFormAuthCookie(response, deployment.id, deployment.authType)
160+
setFormAuthCookie(response, deployment.id, deployment.authType, deployment.password)
161161
return response
162162
}
163163

@@ -337,7 +337,7 @@ export async function GET(
337337
if (
338338
deployment.authType !== 'public' &&
339339
authCookie &&
340-
validateAuthToken(authCookie.value, deployment.id)
340+
validateAuthToken(authCookie.value, deployment.id, deployment.password)
341341
) {
342342
return addCorsHeaders(
343343
createSuccessResponse({

apps/sim/app/api/form/utils.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
1313

1414
const logger = createLogger('FormAuthUtils')
1515

16-
export function setFormAuthCookie(response: NextResponse, formId: string, type: string): void {
17-
setDeploymentAuthCookie(response, 'form', formId, type)
16+
export function setFormAuthCookie(
17+
response: NextResponse,
18+
formId: string,
19+
type: string,
20+
encryptedPassword?: string | null
21+
): void {
22+
setDeploymentAuthCookie(response, 'form', formId, type, encryptedPassword)
1823
}
1924

2025
/**
@@ -90,7 +95,7 @@ export async function validateFormAuth(
9095
const cookieName = `form_auth_${deployment.id}`
9196
const authCookie = request.cookies.get(cookieName)
9297

93-
if (authCookie && validateAuthToken(authCookie.value, deployment.id)) {
98+
if (authCookie && validateAuthToken(authCookie.value, deployment.id, deployment.password)) {
9499
return { authorized: true }
95100
}
96101

apps/sim/app/api/proxy/tts/stream/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async function validateChatAuth(request: NextRequest, chatId: string): Promise<b
4040
const cookieName = `chat_auth_${chatId}`
4141
const authCookie = request.cookies.get(cookieName)
4242

43-
if (authCookie && validateAuthToken(authCookie.value, chatId)) {
43+
if (authCookie && validateAuthToken(authCookie.value, chatId, chatData.password)) {
4444
return true
4545
}
4646

apps/sim/lib/core/security/deployment.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createHmac, timingSafeEqual } from 'crypto'
1+
import { createHash, createHmac, timingSafeEqual } from 'crypto'
22
import type { NextRequest, NextResponse } from 'next/server'
33
import { env } from '@/lib/core/config/env'
44
import { isDev } from '@/lib/core/config/feature-flags'
@@ -12,16 +12,31 @@ function signPayload(payload: string): string {
1212
return createHmac('sha256', env.BETTER_AUTH_SECRET).update(payload).digest('hex')
1313
}
1414

15-
function generateAuthToken(deploymentId: string, type: string): string {
16-
const payload = `${deploymentId}:${type}:${Date.now()}`
15+
function passwordSlot(encryptedPassword?: string | null): string {
16+
if (!encryptedPassword) return ''
17+
return createHash('sha256').update(encryptedPassword).digest('hex').slice(0, 8)
18+
}
19+
20+
function generateAuthToken(
21+
deploymentId: string,
22+
type: string,
23+
encryptedPassword?: string | null
24+
): string {
25+
const payload = `${deploymentId}:${type}:${Date.now()}:${passwordSlot(encryptedPassword)}`
1726
const sig = signPayload(payload)
1827
return Buffer.from(`${payload}:${sig}`).toString('base64')
1928
}
2029

2130
/**
22-
* Validates an HMAC-signed authentication token for a deployment (chat or form)
31+
* Validates an HMAC-signed authentication token for a deployment (chat or form).
32+
* Includes a password-derived slot so changing the deployment password immediately
33+
* invalidates existing sessions.
2334
*/
24-
export function validateAuthToken(token: string, deploymentId: string): boolean {
35+
export function validateAuthToken(
36+
token: string,
37+
deploymentId: string,
38+
encryptedPassword?: string | null
39+
): boolean {
2540
try {
2641
const decoded = Buffer.from(token, 'base64').toString()
2742
const lastColon = decoded.lastIndexOf(':')
@@ -39,10 +54,14 @@ export function validateAuthToken(token: string, deploymentId: string): boolean
3954
}
4055

4156
const parts = payload.split(':')
42-
const [storedId, _type, timestamp] = parts
57+
if (parts.length < 4) return false
58+
const [storedId, _type, timestamp, storedPwSlot] = parts
4359

4460
if (storedId !== deploymentId) return false
4561

62+
const expectedPwSlot = passwordSlot(encryptedPassword)
63+
if (storedPwSlot !== expectedPwSlot) return false
64+
4665
const createdAt = Number.parseInt(timestamp)
4766
const expireTime = 24 * 60 * 60 * 1000
4867
if (Date.now() - createdAt > expireTime) return false
@@ -60,9 +79,10 @@ export function setDeploymentAuthCookie(
6079
response: NextResponse,
6180
cookiePrefix: 'chat' | 'form',
6281
deploymentId: string,
63-
authType: string
82+
authType: string,
83+
encryptedPassword?: string | null
6484
): void {
65-
const token = generateAuthToken(deploymentId, authType)
85+
const token = generateAuthToken(deploymentId, authType, encryptedPassword)
6686
response.cookies.set({
6787
name: `${cookiePrefix}_auth_${deploymentId}`,
6888
value: token,

0 commit comments

Comments
 (0)