feat(auth): add opaque bearer sessions

This commit is contained in:
caoxiaozhu
2026-07-13 14:45:36 +08:00
parent 661990b27b
commit 653eda0596
59 changed files with 1408 additions and 408 deletions

View File

@@ -108,6 +108,11 @@ test('budget center is visible to platform admin, budget monitor, and executive
assert.equal(canAccessAppView({ roleCodes: ['manager'] }, 'budget'), false)
})
test('system settings are visible to platform admin instead of business managers', () => {
assert.equal(canAccessAppView({ isAdmin: true, roleCodes: ['manager'] }, 'settings'), true)
assert.equal(canAccessAppView({ roleCodes: ['manager'] }, 'settings'), false)
})
test('budget edit and department switching are limited to admin and senior finance', () => {
assert.equal(canEditBudgetCenter({ username: 'admin', roleCodes: ['manager'] }), true)
assert.equal(canSwitchBudgetDepartments({ username: 'admin', roleCodes: ['manager'] }), true)

View File

@@ -45,9 +45,11 @@ async function testSupportsBlobResponses() {
assert.equal(payload, blob)
}
async function testInjectsAuthenticatedUserHeaders() {
const sessionStorage = new Map([
[
async function testInjectsBearerTokenWithoutUserControlledIdentityHeaders() {
const sessionStorage = new Map([
['x-financial-auth-access-token', 'opaque-access-token'],
['x-financial-auth-expires-at', '2099-01-01T00:00:00.000Z'],
[
'x-financial-auth-user',
JSON.stringify({
username: 'admin',
@@ -84,27 +86,16 @@ async function testInjectsAuthenticatedUserHeaders() {
await apiRequest('/knowledge/library')
assert.equal(capturedOptions.headers['x-auth-username'], 'admin')
assert.equal(capturedOptions.headers['x-auth-name'], 'Admin User')
assert.equal(capturedOptions.headers['x-auth-position'], 'System Manager')
assert.equal(capturedOptions.headers['x-auth-grade'], 'M5')
assert.equal(capturedOptions.headers['x-auth-employee-no'], 'E-001')
assert.equal(capturedOptions.headers['x-auth-manager-name'], 'Approver User')
assert.equal(capturedOptions.headers['x-auth-role-codes'], 'manager')
assert.equal(capturedOptions.headers['x-auth-is-admin'], 'true')
assert.equal(capturedOptions.headers.Authorization, 'Bearer opaque-access-token')
assert.equal(capturedOptions.headers['x-auth-username'], undefined)
assert.equal(capturedOptions.headers['x-auth-role-codes'], undefined)
assert.equal(capturedOptions.headers['x-auth-is-admin'], undefined)
}
async function testInjectsLegacyAdminHeaderFromSnakeCaseFlag() {
async function testLoginCanDisableBearerInjection() {
const sessionStorage = new Map([
[
'x-financial-auth-user',
JSON.stringify({
username: 'superadmin',
name: 'superadmin',
roleCodes: ['manager'],
is_admin: true
})
]
['x-financial-auth-access-token', 'stale-token'],
['x-financial-auth-expires-at', '2099-01-01T00:00:00.000Z']
])
global.window = {
@@ -127,11 +118,74 @@ async function testInjectsLegacyAdminHeaderFromSnakeCaseFlag() {
}
}
await apiRequest('/reimbursements/claims/demo', { method: 'DELETE' })
await apiRequest('/auth/login', {
auth: false,
handleUnauthorized: false,
method: 'POST',
body: '{}'
})
assert.equal(capturedOptions.headers['x-auth-username'], 'superadmin')
assert.equal(capturedOptions.headers['x-auth-role-codes'], 'manager')
assert.equal(capturedOptions.headers['x-auth-is-admin'], 'true')
assert.equal(capturedOptions.headers.Authorization, undefined)
}
async function testRejectsCustomAuthorizationOverride() {
const sessionStorage = new Map([
['x-financial-auth-access-token', 'server-issued-token'],
['x-financial-auth-expires-at', '2099-01-01T00:00:00.000Z']
])
global.window = {
sessionStorage: {
getItem(key) {
return sessionStorage.get(key) ?? null
}
}
}
let capturedOptions = null
global.fetch = async (_url, options) => {
capturedOptions = options
return { ok: true, async json() { return { ok: true } } }
}
await apiRequest('/knowledge/library', {
headers: { Authorization: 'Bearer attacker-token' }
})
assert.equal(capturedOptions.headers.Authorization, 'Bearer server-issued-token')
}
async function testUnauthorizedResponsePublishesSessionExpiredEvent() {
const sessionStorage = new Map([
['x-financial-auth-access-token', 'expired-token'],
['x-financial-auth-expires-at', '2099-01-01T00:00:00.000Z']
])
const events = []
global.window = {
sessionStorage: {
getItem(key) {
return sessionStorage.get(key) ?? null
}
},
dispatchEvent(event) {
events.push(event.type)
}
}
global.fetch = async () => ({
ok: false,
status: 401,
async json() {
return { detail: '登录会话已失效。' }
}
})
await assert.rejects(
() => apiRequest('/auth/me'),
(error) => {
assert.equal(error.status, 401)
assert.equal(error.code, 'AUTH_SESSION_EXPIRED')
return true
}
)
assert.deepEqual(events, ['x-financial:auth-expired'])
}
async function testFormatsValidationErrors() {
@@ -192,8 +246,10 @@ async function testRejectsWithCustomTimeoutMessage() {
async function run() {
await testUsesCustomContentTypeHeader()
await testSupportsBlobResponses()
await testInjectsAuthenticatedUserHeaders()
await testInjectsLegacyAdminHeaderFromSnakeCaseFlag()
await testInjectsBearerTokenWithoutUserControlledIdentityHeaders()
await testLoginCanDisableBearerInjection()
await testRejectsCustomAuthorizationOverride()
await testUnauthorizedResponsePublishesSessionExpiredEvent()
await testFormatsValidationErrors()
await testRejectsWithCustomTimeoutMessage()
console.log('api-request tests passed')

View File

@@ -0,0 +1,51 @@
import assert from 'node:assert/strict'
import {
AUTH_ACCESS_TOKEN_KEY,
AUTH_EXPIRES_AT_KEY,
buildBearerHeaders,
clearAuthCredentials,
hasValidAuthSession,
persistAuthCredentials,
readAuthAccessToken,
readAuthExpiresAt
} from '../src/utils/authSessionStorage.js'
const storage = new Map()
global.window = {
sessionStorage: {
getItem(key) {
return storage.get(key) ?? null
},
setItem(key, value) {
storage.set(key, String(value))
},
removeItem(key) {
storage.delete(key)
}
}
}
const expiresAt = '2099-01-01T00:00:00.000Z'
persistAuthCredentials('opaque-token', expiresAt)
assert.equal(readAuthAccessToken(), 'opaque-token')
assert.equal(readAuthExpiresAt(), Date.parse(expiresAt))
assert.equal(hasValidAuthSession(Date.parse('2098-01-01T00:00:00.000Z')), true)
assert.deepEqual(buildBearerHeaders(), { Authorization: 'Bearer opaque-token' })
storage.set(AUTH_EXPIRES_AT_KEY, '2020-01-01T00:00:00.000Z')
assert.equal(hasValidAuthSession(Date.parse('2021-01-01T00:00:00.000Z')), false)
storage.set(AUTH_ACCESS_TOKEN_KEY, '')
assert.deepEqual(buildBearerHeaders(), {})
assert.throws(
() => persistAuthCredentials('', expiresAt),
/未返回有效的认证凭证/
)
clearAuthCredentials()
assert.equal(storage.has(AUTH_ACCESS_TOKEN_KEY), false)
assert.equal(storage.has(AUTH_EXPIRES_AT_KEY), false)
console.log('auth session storage tests passed')

View File

@@ -0,0 +1,35 @@
import assert from 'node:assert/strict'
import { isSetupCompletedState, normalizeState } from '../vite.config.js'
const completedEnv = {
SETUP_COMPLETED: 'true',
POSTGRES_HOST: 'database.internal',
POSTGRES_PORT: '5432',
POSTGRES_DB: 'x_financial',
POSTGRES_USER: 'postgres-admin',
POSTGRES_PASSWORD: 'secret',
REDIS_URL: 'redis://redis.internal:6379/0'
}
assert.equal(isSetupCompletedState(completedEnv, true), true)
assert.equal(isSetupCompletedState(completedEnv, false), false)
assert.equal(isSetupCompletedState({ SETUP_COMPLETED: 'false' }, true), false)
const publicState = normalizeState(completedEnv, { adminConfigured: true })
assert.equal(publicState.initialized, true)
assert.equal(publicState.database.host, '')
assert.equal(publicState.database.username, '')
assert.equal(publicState.redis.url, '')
assert.equal(publicState.database.password_configured, true)
const setupState = normalizeState(
{ ...completedEnv, SETUP_COMPLETED: 'false' },
{ adminConfigured: false }
)
assert.equal(setupState.initialized, false)
assert.equal(setupState.database.host, 'database.internal')
assert.equal(setupState.database.username, 'postgres-admin')
assert.equal(setupState.redis.url, 'redis://redis.internal:6379/0')
console.log('vite setup lock tests passed')