mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-15 06:20:44 +08:00
fix(uds-auth): keep sessions until manual sign-out; refresh expired UAC tokens by QR (0.3.20)
Users were asked to sign in again every ~30 minutes, even while active. Session lifetime - Browser: UDS_SESSION kept its login-time Max-Age and was never re-issued, so the cookie vanished 30 min after login. Re-issue it on use (throttled by slidingInterval). - Desktop: remote.mux WS traffic never passed the HTTP auth middleware, so the idle TTL never slid. assertPrincipalLive now reports activity and slides the session and the account's bridges (throttled). - New setting sessionIdleTimeoutHours (Settings page + config). Default 0 = no automatic sign-out; only a manual sign-out ends a session, so conversations and night-time cron are never cut off. 0.5-168 enables an idle timeout with the 7-day cap. The TTL is read live. - Stores support long TTLs: the absolute cap follows the TTL, older 7-day sessions are lifted on activity, and expiry timers re-arm instead of overflowing setTimeout (~24.8 days). UAC token check - Every 2 hours (and 1 min after start) each QR session's token is checked against icenterapi. A rejected token is only flagged, never a sign-out. Network errors, 5xx and bad JSON keep the previous verdict (the old 6h check revoked all of an account's sessions on any failure, including outages). - GET /uds-auth/session-status (non-sliding) returns alive + tokenValid. The client probes it every minute and on focus: a gone session signs the UI out with a notice; a rejected token opens a refresh QR once. - Scanning in refresh mode writes the new token into every live session of the account and the skill credential cache in place: no new session, no reload. Other accounts are rejected (token_refresh_emp_mismatch). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dfdf298184
commit
0e6de055a9
20 changed files with 904 additions and 119 deletions
|
|
@ -32,8 +32,10 @@ export function createAuthMiddleware(config, sessionStore, rolesStore, hooks = {
|
|||
authMode: config.udsAuth?.authMode || 'token+profile',
|
||||
}
|
||||
const udsValidator = new UdsValidator(validatorConfig)
|
||||
const { cookieMaxAge, slidingExpiration } = config.session
|
||||
const ttlSeconds = Math.floor(cookieMaxAge / 1000)
|
||||
const { slidingExpiration } = config.session
|
||||
// Read per request: cookieMaxAge follows the live sessionIdleTimeoutHours setting.
|
||||
const ttlSeconds = () => Math.floor(config.session.cookieMaxAge / 1000)
|
||||
const slidingIntervalMs = Math.max(0, Number(config.session.slidingInterval) || 0)
|
||||
const userSearchUrl = config.userSearchUrl || config.udsAuth?.userSearchUrl
|
||||
const uacBaseUrl = config.udsAuth?.baseUrl || ''
|
||||
/** R05: read live — do not freeze allowOpenRegistration at middleware create time. */
|
||||
|
|
@ -196,7 +198,7 @@ export function createAuthMiddleware(config, sessionStore, rolesStore, hooks = {
|
|||
}
|
||||
const userContext = { ...session.userData, empNo, _sessionId: session.sessionId }
|
||||
if (persistTouch || slidingExpiration) {
|
||||
await sessionStore.touch(session.sessionId, ttlSeconds)
|
||||
await sessionStore.touch(session.sessionId, ttlSeconds())
|
||||
userContext.lastActiveAt = new Date().toISOString()
|
||||
}
|
||||
const role = kind === 'fallback' || kind === 'sealed_box' || rolesStore.hasRole(empNo)
|
||||
|
|
@ -213,17 +215,19 @@ export function createAuthMiddleware(config, sessionStore, rolesStore, hooks = {
|
|||
ctx.authVia = 'session'
|
||||
ctx.authKind = kind
|
||||
if (res && req && session._bearerForCookie) {
|
||||
const cookie = buildSessionCookie(session._bearerForCookie, ttlSeconds, {
|
||||
secure: isHttpsRequest(req),
|
||||
})
|
||||
const prev = res.getHeader?.('Set-Cookie')
|
||||
const list = Array.isArray(prev) ? [...prev, cookie] : (prev ? [prev, cookie] : [cookie])
|
||||
res.setHeader?.('Set-Cookie', list)
|
||||
appendSessionCookie(req, res, session._bearerForCookie)
|
||||
}
|
||||
}
|
||||
|
||||
function appendSessionCookie(req, res, bearer) {
|
||||
const cookie = buildSessionCookie(bearer, ttlSeconds(), { secure: isHttpsRequest(req) })
|
||||
const prev = res.getHeader?.('Set-Cookie')
|
||||
const list = Array.isArray(prev) ? [...prev, cookie] : (prev ? [prev, cookie] : [cookie])
|
||||
res.setHeader?.('Set-Cookie', list)
|
||||
}
|
||||
|
||||
async function mintSession(ctx, userContext, kind, { res, req } = {}) {
|
||||
const { sessionId, bearer, record } = await sessionStore.create(userContext, ttlSeconds)
|
||||
const { sessionId, bearer, record } = await sessionStore.create(userContext, ttlSeconds())
|
||||
if (userContext.token) {
|
||||
try { onSkillCredentials?.(userContext.empNo, userContext.token) } catch { /* ignore */ }
|
||||
}
|
||||
|
|
@ -262,7 +266,15 @@ export function createAuthMiddleware(config, sessionStore, rolesStore, hooks = {
|
|||
throw err
|
||||
}
|
||||
// Keep the Desktop bridge alive as long as its session (same bearer).
|
||||
try { sessionBridge?.touch?.(extracted.bearer, ttlSeconds * 1000) } catch { /* ignore */ }
|
||||
try { sessionBridge?.touch?.(extracted.bearer, ttlSeconds() * 1000) } catch { /* ignore */ }
|
||||
// Slide the browser cookie with the server idle TTL. Without this UDS_SESSION kept
|
||||
// the Max-Age from login and vanished 30 min later even for active users.
|
||||
// Throttled by slidingInterval so not every request carries Set-Cookie.
|
||||
if (slidingExpiration && extracted.via === 'cookie' && res && req
|
||||
&& !res.headersSent
|
||||
&& Date.now() + ttlSeconds() * 1000 - Number(session.expiresAt || 0) >= slidingIntervalMs) {
|
||||
try { appendSessionCookie(req, res, extracted.bearer) } catch { /* ignore */ }
|
||||
}
|
||||
return next()
|
||||
}
|
||||
// Explicit bearer presented but invalid — do not mix with portal cookies.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue