Spaces:
Runtime error
fix(session-cookie): migrate to __Host- prefix for secure contexts (#294)
Browse files* fix(session-cookie): migrate to __Host- prefix for secure contexts
- Update session-cookie.ts to use __Host-mc-session for HTTPS requests
- Add LEGACY_MC_SESSION_COOKIE_NAME for backward compatibility with HTTP
- Add parseMcSessionCookieHeader() to parse both cookie names
- Add isRequestSecure() helper to detect HTTPS requests
- Update cookie options to enforce Secure, HttpOnly, SameSite=Strict
- Update all call sites (login, logout, google, me, proxy, auth)
- Update e2e tests to support both cookie names
- Update documentation (README.md, SKILL.md, openapi.json)
This addresses the high-priority TODO about migrating to the __Host- prefix
for enhanced security. The __Host- prefix enforces Secure + Path=/ and
prevents subdomain attacks. Legacy mc-session is still supported for HTTP
contexts.
* fix(tests): keep login-flow cookie name aligned with response
- remove unreachable nullish expression in session cookie secure flag
- use returned cookie pair in login-flow spec instead of forcing __Host- prefix
---------
Co-authored-by: Nyk <0xnykcd@googlemail.com>
- README.md +1 -1
- SKILL.md +1 -1
- openapi.json +3 -3
- src/app/api/auth/google/route.ts +4 -4
- src/app/api/auth/login/route.ts +4 -4
- src/app/api/auth/logout/route.ts +6 -5
- src/app/api/auth/me/route.ts +4 -4
- src/lib/auth.ts +2 -5
- src/lib/session-cookie.ts +22 -22
- src/proxy.ts +2 -1
- tests/login-flow.spec.ts +6 -5
|
@@ -308,7 +308,7 @@ Three auth methods, three roles:
|
|
| 308 |
|
| 309 |
| Method | Details |
|
| 310 |
|--------|----------|
|
| 311 |
-
| Session cookie | `POST /api/auth/login` sets `mc-session` (7-day expiry) |
|
| 312 |
| API key | `x-api-key` header matches `API_KEY` env var |
|
| 313 |
| Google Sign-In | OAuth with admin approval workflow |
|
| 314 |
|
|
|
|
| 308 |
|
| 309 |
| Method | Details |
|
| 310 |
|--------|----------|
|
| 311 |
+
| Session cookie | `POST /api/auth/login` sets `__Host-mc-session` (7-day expiry) for HTTPS, `mc-session` for HTTP |
|
| 312 |
| API key | `x-api-key` header matches `API_KEY` env var |
|
| 313 |
| Google Sign-In | OAuth with admin approval workflow |
|
| 314 |
|
|
@@ -44,7 +44,7 @@ MC supports two auth methods:
|
|
| 44 |
| Method | Header | Use Case |
|
| 45 |
|--------|--------|----------|
|
| 46 |
| API Key | `x-api-key: <key>` or `Authorization: Bearer <key>` | Agents, scripts, CI/CD |
|
| 47 |
-
| Session cookie | `Cookie: mc-session=<token>` | Browser UI |
|
| 48 |
|
| 49 |
**Roles (hierarchical):** `viewer` < `operator` < `admin`
|
| 50 |
|
|
|
|
| 44 |
| Method | Header | Use Case |
|
| 45 |
|--------|--------|----------|
|
| 46 |
| API Key | `x-api-key: <key>` or `Authorization: Bearer <key>` | Agents, scripts, CI/CD |
|
| 47 |
+
| Session cookie | `Cookie: __Host-mc-session=<token>` (HTTPS) or `mc-session=<token>` (HTTP) | Browser UI |
|
| 48 |
|
| 49 |
**Roles (hierarchical):** `viewer` < `operator` < `admin`
|
| 50 |
|
|
@@ -1757,7 +1757,7 @@
|
|
| 1757 |
},
|
| 1758 |
"responses": {
|
| 1759 |
"200": {
|
| 1760 |
-
"description": "Login successful. Sets mc-session cookie.",
|
| 1761 |
"content": {
|
| 1762 |
"application/json": {
|
| 1763 |
"schema": {
|
|
@@ -1775,7 +1775,7 @@
|
|
| 1775 |
"schema": {
|
| 1776 |
"type": "string"
|
| 1777 |
},
|
| 1778 |
-
"description": "mc-session cookie"
|
| 1779 |
}
|
| 1780 |
}
|
| 1781 |
},
|
|
@@ -7345,7 +7345,7 @@
|
|
| 7345 |
"sessionCookie": {
|
| 7346 |
"type": "apiKey",
|
| 7347 |
"in": "cookie",
|
| 7348 |
-
"name": "mc-session"
|
| 7349 |
},
|
| 7350 |
"apiKey": {
|
| 7351 |
"type": "apiKey",
|
|
|
|
| 1757 |
},
|
| 1758 |
"responses": {
|
| 1759 |
"200": {
|
| 1760 |
+
"description": "Login successful. Sets __Host-mc-session cookie (HTTPS) or mc-session (HTTP).",
|
| 1761 |
"content": {
|
| 1762 |
"application/json": {
|
| 1763 |
"schema": {
|
|
|
|
| 1775 |
"schema": {
|
| 1776 |
"type": "string"
|
| 1777 |
},
|
| 1778 |
+
"description": "__Host-mc-session cookie (secure HTTPS) or mc-session (HTTP legacy)"
|
| 1779 |
}
|
| 1780 |
}
|
| 1781 |
},
|
|
|
|
| 7345 |
"sessionCookie": {
|
| 7346 |
"type": "apiKey",
|
| 7347 |
"in": "cookie",
|
| 7348 |
+
"name": "__Host-mc-session"
|
| 7349 |
},
|
| 7350 |
"apiKey": {
|
| 7351 |
"type": "apiKey",
|
|
@@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|
| 3 |
import { createSession } from '@/lib/auth'
|
| 4 |
import { getDatabase, logAuditEvent } from '@/lib/db'
|
| 5 |
import { verifyGoogleIdToken } from '@/lib/google-auth'
|
| 6 |
-
import { getMcSessionCookieOptions } from '@/lib/session-cookie'
|
| 7 |
import { loginLimiter } from '@/lib/rate-limit'
|
| 8 |
|
| 9 |
function upsertAccessRequest(input: {
|
|
@@ -100,10 +100,10 @@ export async function POST(request: NextRequest) {
|
|
| 100 |
},
|
| 101 |
})
|
| 102 |
|
| 103 |
-
const isSecureRequest =
|
| 104 |
-
|
| 105 |
|
| 106 |
-
response.cookies.set(
|
| 107 |
...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }),
|
| 108 |
})
|
| 109 |
|
|
|
|
| 3 |
import { createSession } from '@/lib/auth'
|
| 4 |
import { getDatabase, logAuditEvent } from '@/lib/db'
|
| 5 |
import { verifyGoogleIdToken } from '@/lib/google-auth'
|
| 6 |
+
import { getMcSessionCookieName, getMcSessionCookieOptions, isRequestSecure } from '@/lib/session-cookie'
|
| 7 |
import { loginLimiter } from '@/lib/rate-limit'
|
| 8 |
|
| 9 |
function upsertAccessRequest(input: {
|
|
|
|
| 100 |
},
|
| 101 |
})
|
| 102 |
|
| 103 |
+
const isSecureRequest = isRequestSecure(request)
|
| 104 |
+
const cookieName = getMcSessionCookieName(isSecureRequest)
|
| 105 |
|
| 106 |
+
response.cookies.set(cookieName, token, {
|
| 107 |
...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }),
|
| 108 |
})
|
| 109 |
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { NextResponse } from 'next/server'
|
| 2 |
import { authenticateUser, createSession } from '@/lib/auth'
|
| 3 |
import { logAuditEvent } from '@/lib/db'
|
| 4 |
-
import { getMcSessionCookieOptions } from '@/lib/session-cookie'
|
| 5 |
import { loginLimiter } from '@/lib/rate-limit'
|
| 6 |
import { logger } from '@/lib/logger'
|
| 7 |
|
|
@@ -43,10 +43,10 @@ export async function POST(request: Request) {
|
|
| 43 |
},
|
| 44 |
})
|
| 45 |
|
| 46 |
-
const isSecureRequest =
|
| 47 |
-
|
| 48 |
|
| 49 |
-
response.cookies.set(
|
| 50 |
...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }),
|
| 51 |
})
|
| 52 |
|
|
|
|
| 1 |
import { NextResponse } from 'next/server'
|
| 2 |
import { authenticateUser, createSession } from '@/lib/auth'
|
| 3 |
import { logAuditEvent } from '@/lib/db'
|
| 4 |
+
import { getMcSessionCookieName, getMcSessionCookieOptions, isRequestSecure } from '@/lib/session-cookie'
|
| 5 |
import { loginLimiter } from '@/lib/rate-limit'
|
| 6 |
import { logger } from '@/lib/logger'
|
| 7 |
|
|
|
|
| 43 |
},
|
| 44 |
})
|
| 45 |
|
| 46 |
+
const isSecureRequest = isRequestSecure(request)
|
| 47 |
+
const cookieName = getMcSessionCookieName(isSecureRequest)
|
| 48 |
|
| 49 |
+
response.cookies.set(cookieName, token, {
|
| 50 |
...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }),
|
| 51 |
})
|
| 52 |
|
|
@@ -1,13 +1,12 @@
|
|
| 1 |
import { NextResponse } from 'next/server'
|
| 2 |
import { destroySession, getUserFromRequest } from '@/lib/auth'
|
| 3 |
import { logAuditEvent } from '@/lib/db'
|
| 4 |
-
import { getMcSessionCookieOptions } from '@/lib/session-cookie'
|
| 5 |
|
| 6 |
export async function POST(request: Request) {
|
| 7 |
const user = getUserFromRequest(request)
|
| 8 |
const cookieHeader = request.headers.get('cookie') || ''
|
| 9 |
-
const
|
| 10 |
-
const token = match ? decodeURIComponent(match[1]) : null
|
| 11 |
|
| 12 |
if (token) {
|
| 13 |
destroySession(token)
|
|
@@ -19,8 +18,10 @@ export async function POST(request: Request) {
|
|
| 19 |
}
|
| 20 |
|
| 21 |
const response = NextResponse.json({ ok: true })
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
})
|
| 25 |
|
| 26 |
return response
|
|
|
|
| 1 |
import { NextResponse } from 'next/server'
|
| 2 |
import { destroySession, getUserFromRequest } from '@/lib/auth'
|
| 3 |
import { logAuditEvent } from '@/lib/db'
|
| 4 |
+
import { getMcSessionCookieName, getMcSessionCookieOptions, isRequestSecure, parseMcSessionCookieHeader } from '@/lib/session-cookie'
|
| 5 |
|
| 6 |
export async function POST(request: Request) {
|
| 7 |
const user = getUserFromRequest(request)
|
| 8 |
const cookieHeader = request.headers.get('cookie') || ''
|
| 9 |
+
const token = parseMcSessionCookieHeader(cookieHeader)
|
|
|
|
| 10 |
|
| 11 |
if (token) {
|
| 12 |
destroySession(token)
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
const response = NextResponse.json({ ok: true })
|
| 21 |
+
const isSecureRequest = isRequestSecure(request)
|
| 22 |
+
const cookieName = getMcSessionCookieName(isSecureRequest)
|
| 23 |
+
response.cookies.set(cookieName, '', {
|
| 24 |
+
...getMcSessionCookieOptions({ maxAgeSeconds: 0, isSecureRequest }),
|
| 25 |
})
|
| 26 |
|
| 27 |
return response
|
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
|
| 2 |
import { getUserFromRequest, updateUser, requireRole, destroyAllUserSessions, createSession } from '@/lib/auth'
|
| 3 |
import { logAuditEvent } from '@/lib/db'
|
| 4 |
import { verifyPassword } from '@/lib/password'
|
| 5 |
-
import { getMcSessionCookieOptions } from '@/lib/session-cookie'
|
| 6 |
import { logger } from '@/lib/logger'
|
| 7 |
|
| 8 |
export async function GET(request: Request) {
|
|
@@ -117,9 +117,9 @@ export async function PATCH(request: NextRequest) {
|
|
| 117 |
// Issue a fresh session cookie after password change (old ones were just revoked)
|
| 118 |
if (updates.password) {
|
| 119 |
const { token, expiresAt } = createSession(user.id, ipAddress, userAgent, user.workspace_id ?? 1)
|
| 120 |
-
const isSecureRequest =
|
| 121 |
-
|
| 122 |
-
response.cookies.set(
|
| 123 |
...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }),
|
| 124 |
})
|
| 125 |
}
|
|
|
|
| 2 |
import { getUserFromRequest, updateUser, requireRole, destroyAllUserSessions, createSession } from '@/lib/auth'
|
| 3 |
import { logAuditEvent } from '@/lib/db'
|
| 4 |
import { verifyPassword } from '@/lib/password'
|
| 5 |
+
import { getMcSessionCookieName, getMcSessionCookieOptions, isRequestSecure } from '@/lib/session-cookie'
|
| 6 |
import { logger } from '@/lib/logger'
|
| 7 |
|
| 8 |
export async function GET(request: Request) {
|
|
|
|
| 117 |
// Issue a fresh session cookie after password change (old ones were just revoked)
|
| 118 |
if (updates.password) {
|
| 119 |
const { token, expiresAt } = createSession(user.id, ipAddress, userAgent, user.workspace_id ?? 1)
|
| 120 |
+
const isSecureRequest = isRequestSecure(request)
|
| 121 |
+
const cookieName = getMcSessionCookieName(isSecureRequest)
|
| 122 |
+
response.cookies.set(cookieName, token, {
|
| 123 |
...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }),
|
| 124 |
})
|
| 125 |
}
|
|
@@ -2,6 +2,7 @@ import { createHash, randomBytes, timingSafeEqual } from 'crypto'
|
|
| 2 |
import { getDatabase } from './db'
|
| 3 |
import { hashPassword, verifyPassword } from './password'
|
| 4 |
import { logSecurityEvent } from './security-events'
|
|
|
|
| 5 |
|
| 6 |
// Plugin hook: extensions can register a custom API key resolver without modifying this file.
|
| 7 |
type AuthResolverHook = (apiKey: string, agentName: string | null) => User | null
|
|
@@ -346,7 +347,7 @@ export function getUserFromRequest(request: Request): User | null {
|
|
| 346 |
|
| 347 |
// Check session cookie
|
| 348 |
const cookieHeader = request.headers.get('cookie') || ''
|
| 349 |
-
const sessionToken =
|
| 350 |
if (sessionToken) {
|
| 351 |
const user = validateSession(sessionToken)
|
| 352 |
if (user) return { ...user, agent_name: agentName }
|
|
@@ -510,7 +511,3 @@ export function requireRole(
|
|
| 510 |
return { user }
|
| 511 |
}
|
| 512 |
|
| 513 |
-
function parseCookie(cookieHeader: string, name: string): string | null {
|
| 514 |
-
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
|
| 515 |
-
return match ? decodeURIComponent(match[1]) : null
|
| 516 |
-
}
|
|
|
|
| 2 |
import { getDatabase } from './db'
|
| 3 |
import { hashPassword, verifyPassword } from './password'
|
| 4 |
import { logSecurityEvent } from './security-events'
|
| 5 |
+
import { parseMcSessionCookieHeader } from './session-cookie'
|
| 6 |
|
| 7 |
// Plugin hook: extensions can register a custom API key resolver without modifying this file.
|
| 8 |
type AuthResolverHook = (apiKey: string, agentName: string | null) => User | null
|
|
|
|
| 347 |
|
| 348 |
// Check session cookie
|
| 349 |
const cookieHeader = request.headers.get('cookie') || ''
|
| 350 |
+
const sessionToken = parseMcSessionCookieHeader(cookieHeader)
|
| 351 |
if (sessionToken) {
|
| 352 |
const user = validateSession(sessionToken)
|
| 353 |
if (user) return { ...user, agent_name: agentName }
|
|
|
|
| 511 |
return { user }
|
| 512 |
}
|
| 513 |
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,17 +1,27 @@
|
|
| 1 |
import type { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies'
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
// (proxy.ts, auth/login, auth/logout, auth/google, lib/auth.ts, tests)
|
| 7 |
-
// then switch the default to use __Host- prefix when secure=true.
|
| 8 |
-
export const MC_SESSION_COOKIE_NAME = 'mc-session'
|
| 9 |
|
| 10 |
-
export function getMcSessionCookieName(
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
function envFlag(name: string): boolean | undefined {
|
|
@@ -25,23 +35,13 @@ function envFlag(name: string): boolean | undefined {
|
|
| 25 |
|
| 26 |
export function getMcSessionCookieOptions(input: { maxAgeSeconds: number; isSecureRequest?: boolean }): Partial<ResponseCookie> {
|
| 27 |
const secureEnv = envFlag('MC_COOKIE_SECURE')
|
| 28 |
-
// Explicit env wins. Otherwise auto-detect: only set secure if request came over HTTPS.
|
| 29 |
-
// Falls back to NODE_ENV=production when no request hint is available.
|
| 30 |
const secure = secureEnv ?? input.isSecureRequest ?? process.env.NODE_ENV === 'production'
|
| 31 |
|
| 32 |
-
// Strict is safest for this app (same-site UI + API), but allow override for edge cases.
|
| 33 |
-
const sameSiteRaw = (process.env.MC_COOKIE_SAMESITE || 'strict').toLowerCase()
|
| 34 |
-
const sameSite: ResponseCookie['sameSite'] =
|
| 35 |
-
sameSiteRaw === 'lax' ? 'lax' :
|
| 36 |
-
sameSiteRaw === 'none' ? 'none' :
|
| 37 |
-
'strict'
|
| 38 |
-
|
| 39 |
return {
|
| 40 |
httpOnly: true,
|
| 41 |
secure,
|
| 42 |
-
sameSite,
|
| 43 |
maxAge: input.maxAgeSeconds,
|
| 44 |
path: '/',
|
| 45 |
}
|
| 46 |
}
|
| 47 |
-
|
|
|
|
| 1 |
import type { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies'
|
| 2 |
|
| 3 |
+
export const MC_SESSION_COOKIE_NAME = '__Host-mc-session'
|
| 4 |
+
export const LEGACY_MC_SESSION_COOKIE_NAME = 'mc-session'
|
| 5 |
+
const MC_SESSION_COOKIE_NAMES = [MC_SESSION_COOKIE_NAME, LEGACY_MC_SESSION_COOKIE_NAME] as const
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
+
export function getMcSessionCookieName(isSecureRequest: boolean): string {
|
| 8 |
+
return isSecureRequest ? MC_SESSION_COOKIE_NAME : LEGACY_MC_SESSION_COOKIE_NAME
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export function isRequestSecure(request: Request): boolean {
|
| 12 |
+
return request.headers.get('x-forwarded-proto') === 'https'
|
| 13 |
+
|| new URL(request.url).protocol === 'https:'
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export function parseMcSessionCookieHeader(cookieHeader: string): string | null {
|
| 17 |
+
if (!cookieHeader) return null
|
| 18 |
+
for (const cookieName of MC_SESSION_COOKIE_NAMES) {
|
| 19 |
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${cookieName}=([^;]*)`))
|
| 20 |
+
if (match) {
|
| 21 |
+
return decodeURIComponent(match[1])
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
return null
|
| 25 |
}
|
| 26 |
|
| 27 |
function envFlag(name: string): boolean | undefined {
|
|
|
|
| 35 |
|
| 36 |
export function getMcSessionCookieOptions(input: { maxAgeSeconds: number; isSecureRequest?: boolean }): Partial<ResponseCookie> {
|
| 37 |
const secureEnv = envFlag('MC_COOKIE_SECURE')
|
|
|
|
|
|
|
| 38 |
const secure = secureEnv ?? input.isSecureRequest ?? process.env.NODE_ENV === 'production'
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
return {
|
| 41 |
httpOnly: true,
|
| 42 |
secure,
|
| 43 |
+
sameSite: 'strict',
|
| 44 |
maxAge: input.maxAgeSeconds,
|
| 45 |
path: '/',
|
| 46 |
}
|
| 47 |
}
|
|
|
|
@@ -2,6 +2,7 @@ import crypto from 'node:crypto'
|
|
| 2 |
import os from 'node:os'
|
| 3 |
import { NextResponse } from 'next/server'
|
| 4 |
import type { NextRequest } from 'next/server'
|
|
|
|
| 5 |
|
| 6 |
/** Constant-time string comparison using Node.js crypto. */
|
| 7 |
function safeCompare(a: string, b: string): boolean {
|
|
@@ -189,7 +190,7 @@ export function proxy(request: NextRequest) {
|
|
| 189 |
}
|
| 190 |
|
| 191 |
// Check for session cookie
|
| 192 |
-
const sessionToken = request.cookies.get(
|
| 193 |
|
| 194 |
// API routes: accept session cookie OR API key
|
| 195 |
if (pathname.startsWith('/api/')) {
|
|
|
|
| 2 |
import os from 'node:os'
|
| 3 |
import { NextResponse } from 'next/server'
|
| 4 |
import type { NextRequest } from 'next/server'
|
| 5 |
+
import { MC_SESSION_COOKIE_NAME, LEGACY_MC_SESSION_COOKIE_NAME } from '@/lib/session-cookie'
|
| 6 |
|
| 7 |
/** Constant-time string comparison using Node.js crypto. */
|
| 8 |
function safeCompare(a: string, b: string): boolean {
|
|
|
|
| 190 |
}
|
| 191 |
|
| 192 |
// Check for session cookie
|
| 193 |
+
const sessionToken = request.cookies.get(MC_SESSION_COOKIE_NAME)?.value || request.cookies.get(LEGACY_MC_SESSION_COOKIE_NAME)?.value
|
| 194 |
|
| 195 |
// API routes: accept session cookie OR API key
|
| 196 |
if (pathname.startsWith('/api/')) {
|
|
@@ -45,7 +45,7 @@ test.describe('Login Flow', () => {
|
|
| 45 |
|
| 46 |
const cookies = res.headers()['set-cookie']
|
| 47 |
expect(cookies).toBeDefined()
|
| 48 |
-
expect(cookies).
|
| 49 |
})
|
| 50 |
|
| 51 |
test('login API rejects wrong password', async ({ request }) => {
|
|
@@ -66,15 +66,16 @@ test.describe('Login Flow', () => {
|
|
| 66 |
|
| 67 |
// Extract session cookie from Set-Cookie header
|
| 68 |
const setCookie = loginRes.headers()['set-cookie'] || ''
|
| 69 |
-
const match = setCookie.match(/mc-session=([^;]+)/)
|
| 70 |
expect(match).toBeTruthy()
|
| 71 |
-
const
|
| 72 |
|
| 73 |
-
// Use the
|
| 74 |
const meRes = await request.get('/api/auth/me', {
|
| 75 |
-
headers: { 'cookie':
|
| 76 |
})
|
| 77 |
expect(meRes.status()).toBe(200)
|
|
|
|
| 78 |
const body = await meRes.json()
|
| 79 |
expect(body.user?.username).toBe(TEST_USER)
|
| 80 |
expect(typeof body.user?.workspace_id).toBe('number')
|
|
|
|
| 45 |
|
| 46 |
const cookies = res.headers()['set-cookie']
|
| 47 |
expect(cookies).toBeDefined()
|
| 48 |
+
expect(cookies).toMatch(/(__Host-)?mc-session/)
|
| 49 |
})
|
| 50 |
|
| 51 |
test('login API rejects wrong password', async ({ request }) => {
|
|
|
|
| 66 |
|
| 67 |
// Extract session cookie from Set-Cookie header
|
| 68 |
const setCookie = loginRes.headers()['set-cookie'] || ''
|
| 69 |
+
const match = setCookie.match(/(?:__Host-)?mc-session=([^;]+)/)
|
| 70 |
expect(match).toBeTruthy()
|
| 71 |
+
const sessionCookiePair = match?.[0] || ''
|
| 72 |
|
| 73 |
+
// Use the same cookie name/value returned by login
|
| 74 |
const meRes = await request.get('/api/auth/me', {
|
| 75 |
+
headers: { 'cookie': sessionCookiePair, 'x-forwarded-for': '10.88.88.2' }
|
| 76 |
})
|
| 77 |
expect(meRes.status()).toBe(200)
|
| 78 |
+
|
| 79 |
const body = await meRes.json()
|
| 80 |
expect(body.user?.username).toBe(TEST_USER)
|
| 81 |
expect(typeof body.user?.workspace_id).toBe('number')
|