oclaw/uds-auth/lib/middleware/auth-middleware.js
2026-09-08 02:50:34 +08:00

183 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { UdsClient } from '../uds/client.js'
import { UdsValidator } from '../uds/validator.js'
import { ROLES, computePermissions } from '../roles.js'
import { searchUserByEmpNoToken } from '../uds/user-search.js'
/**
* auth-middleware
*
* 正常登录:Cookie 中必须同时有 empNo + token,并用 userSearchUrl
* (带 X-Emp-No / X-Auth-Value)直连内网拉用户详情;成功才建会话。
* 出站请求绕过 HTTP(S)_PROXY。
*
* 兜底登录:仅认可已由 /api/fallback/login 写好的 administrator 会话。
*/
export function createAuthMiddleware(config, sessionStore, rolesStore) {
const udsClient = new UdsClient(config.udsAuth)
const validatorConfig = {
...config.udsAuth,
authMode: config.udsAuth?.authMode || 'token+profile',
}
const udsValidator = new UdsValidator(validatorConfig)
const { cookieMaxAge, slidingExpiration } = config.session
const userSearchUrl = config.userSearchUrl || config.udsAuth?.userSearchUrl
const uacBaseUrl = config.udsAuth?.baseUrl || ''
function extractEmpNo(cookieHeader) {
const udsMatch = cookieHeader?.match(/(?:PORTALSSOUser|ZTEDPGSSOUser)=([^;]+)/)
if (udsMatch) return { empNo: decodeURIComponent(udsMatch[1].trim()), kind: 'uds' }
const fbMatch = cookieHeader?.match(/UDS_FALLBACK_USER=([^;]+)/)
if (fbMatch) return { empNo: decodeURIComponent(fbMatch[1].trim()), kind: 'fallback' }
return null
}
function applyProfile(userContext, profile, credentials) {
userContext.userId = profile.empNo
userContext.empNo = profile.empNo
userContext.username = profile.username
userContext.displayName = profile.username
userContext.department = profile.department
userContext.organization = profile.organization || profile.department || ''
userContext.email = profile.email
userContext.phone = profile.phone
userContext.token = credentials.token
userContext.lang = credentials.lang || userContext.lang || 'zh-CN'
userContext.isAuthenticated = true
userContext.authMode = 'token+profile'
userContext.lastActiveAt = new Date().toISOString()
return userContext
}
async function verifyEmpNoAndToken(empNo, token) {
const out = await searchUserByEmpNoToken({
userSearchUrl,
empNo,
token,
empNoHeader: config.udsAuth?.empNoHeader || 'X-Emp-No',
authValueHeader: config.udsAuth?.authValueHeader || 'X-Auth-Value',
origin: uacBaseUrl || undefined,
})
if (!out.ok) {
console.warn('[uds-auth] verifyEmpNoAndToken failed:', out)
return null
}
return out.profile
}
function needsProfileUpgrade(userContext) {
if (!userContext) return true
if (userContext.authMode === 'trust') return true
if (userContext.authMode !== 'token+profile') return true
const name = userContext.displayName || userContext.username || ''
if (!name || name === userContext.empNo) return true
return false
}
async function authMiddleware(ctx, next) {
const { req } = ctx
const cookieHeader = req.headers.cookie || ''
const extracted = extractEmpNo(cookieHeader)
if (!extracted) return next()
let userContext = await sessionStore.get(extracted.empNo)
// 旧 trust 会话 / 无姓名部门:强制用 token 重查用户信息
if (userContext && extracted.kind === 'uds' && needsProfileUpgrade(userContext)) {
const credentials = udsValidator.extractCredentials(req)
if (credentials && credentials.empNo === extracted.empNo && udsValidator.validateCredentials(credentials)) {
const profile = await verifyEmpNoAndToken(credentials.empNo, credentials.token)
if (profile) {
userContext = applyProfile(userContext, profile, credentials)
await sessionStore.setex(
profile.empNo,
Math.floor(cookieMaxAge / 1000),
userContext,
)
} else {
// 查不到资料则作废 trust 会话,避免“假登录”
await sessionStore.delete(extracted.empNo)
userContext = null
}
} else if (userContext.authMode === 'trust') {
await sessionStore.delete(extracted.empNo)
userContext = null
}
}
if (userContext) {
if (slidingExpiration) {
userContext.lastActiveAt = new Date().toISOString()
await sessionStore.setex(
extracted.empNo,
Math.floor(cookieMaxAge / 1000),
userContext,
)
}
const role = rolesStore.getRole(extracted.empNo)
ctx.userContext = userContext
ctx.empNo = extracted.empNo
ctx.role = role
ctx.permissions = computePermissions(role)
return next()
}
if (extracted.kind === 'fallback') {
return next()
}
const credentials = udsValidator.extractCredentials(req)
if (!credentials || !udsValidator.validateCredentials(credentials)) {
return next()
}
if (credentials.empNo !== extracted.empNo) {
return next()
}
const profile = await verifyEmpNoAndToken(credentials.empNo, credentials.token)
if (!profile) {
return next()
}
userContext = udsValidator.buildUserContext({
empNo: profile.empNo,
token: credentials.token,
lang: credentials.lang,
username: profile.username,
displayName: profile.username,
department: profile.department,
organization: profile.organization,
email: profile.email,
phone: profile.phone,
}, false)
applyProfile(userContext, profile, credentials)
userContext.authenticatedAt = new Date().toISOString()
userContext.sessionCreatedAt = new Date().toISOString()
await sessionStore.setex(
profile.empNo,
Math.floor(cookieMaxAge / 1000),
userContext,
)
const role = rolesStore.getRole(profile.empNo)
ctx.userContext = userContext
ctx.empNo = profile.empNo
ctx.role = role
ctx.permissions = computePermissions(role)
return next()
}
authMiddleware.udsClient = udsClient
authMiddleware.udsValidator = udsValidator
authMiddleware.verifyEmpNoAndToken = verifyEmpNoAndToken
return authMiddleware
}
export function requirePermission(ctx, permission) {
if (!ctx?.permissions) return false
if (permission === 'super_admin') {
return ctx.role === ROLES.SUPER_ADMIN || ctx.role === ROLES.FALLBACK_ADMIN
}
return !!ctx.permissions[permission]
}