62 lines
1.8 KiB
TypeScript
62 lines
1.8 KiB
TypeScript
import crypto from 'crypto'
|
|
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-same-across-all-sites'
|
|
const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN || ''
|
|
|
|
export interface SharedSession {
|
|
username: string
|
|
displayName: string
|
|
iat: number
|
|
exp: number
|
|
}
|
|
|
|
function base64url(str: string): string {
|
|
return Buffer.from(str).toString('base64url')
|
|
}
|
|
|
|
export function signSharedJwt(
|
|
payload: { username: string; displayName: string },
|
|
expiresIn: number = 7 * 24 * 60 * 60
|
|
): string {
|
|
const header = { alg: 'HS256', typ: 'JWT' }
|
|
const now = Math.floor(Date.now() / 1000)
|
|
const body = { ...payload, iat: now, exp: now + expiresIn }
|
|
const segments = [base64url(JSON.stringify(header)), base64url(JSON.stringify(body))]
|
|
const signingInput = segments.join('.')
|
|
segments.push(
|
|
crypto.createHmac('sha256', JWT_SECRET).update(signingInput).digest('base64url')
|
|
)
|
|
return segments.join('.')
|
|
}
|
|
|
|
export function verifySharedJwt(token: string): SharedSession | null {
|
|
try {
|
|
const parts = token.split('.')
|
|
if (parts.length !== 3) return null
|
|
const signingInput = parts.slice(0, 2).join('.')
|
|
const expectedSig = crypto.createHmac('sha256', JWT_SECRET)
|
|
.update(signingInput).digest('base64url')
|
|
if (parts[2] !== expectedSig) return null
|
|
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString())
|
|
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) return null
|
|
return {
|
|
username: payload.username,
|
|
displayName: payload.displayName,
|
|
iat: payload.iat,
|
|
exp: payload.exp,
|
|
}
|
|
} catch { return null }
|
|
}
|
|
|
|
export function sharedCookieConfig(maxAge: number = 7 * 24 * 60 * 60) {
|
|
return {
|
|
name: 'tlyq_session',
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
sameSite: 'lax' as const,
|
|
domain: COOKIE_DOMAIN,
|
|
path: '/',
|
|
maxAge,
|
|
}
|
|
}
|