fix(auth): TokenSync — проактивная re-auth при expired user
Симптом: после ~30 мин неактивности пользователь видит 403 на /admin/audit и /admin/users. Перелогин решает. Корень. automaticSilentRenew: true в oidcConfig должен обновлять access_token, но падает молча когда: - refresh_token истёк (Keycloak SSO Session Idle = 30 мин) - silent renew iframe заблокирован 3rd-party cookies - silentRenewError не подцеплен глобально В этих случаях user.expired=true, accessToken=null. Дальнейшие запросы идут БЕЗ Authorization header → backend с ORDINIS_AUTH_REQUIRED=false пропускает как anonymous → requireInternal() видит PUBLIC scope → 403 (не 401). 401-handler в interceptor'е не срабатывает (статус не тот) → юзер залипает. Fix. Новый useEffect в TokenSync watches auth.user?.expired. На transition в true: signinSilent (вдруг refresh ещё жив), на failure signinRedirect. Anti-loop ref предотвращает повторные попытки пока expired=true. Бонусом — за компанию с MR !267 (admin → RESTRICTED) — после v2.42.4 деплоя админ может работать сессией дольше 30 мин без 403'ов.
This commit is contained in:
@@ -23,6 +23,9 @@ import { LEGACY_TOKEN_STORAGE_KEY } from './oidcConfig'
|
||||
export function TokenSync() {
|
||||
const auth = useAuth()
|
||||
const redirecting = useRef(false)
|
||||
// Anti-loop guard для проактивной re-auth при expiry — без него
|
||||
// useEffect срабатывал бы повторно на каждый render пока expired=true.
|
||||
const reauthing = useRef(false)
|
||||
|
||||
// 1) Token sync — single update path к accessToken holder в client.ts.
|
||||
// ВАЖНО: пропускаем токен ТОЛЬКО когда user реально authenticated AND token
|
||||
@@ -93,5 +96,41 @@ export function TokenSync() {
|
||||
return () => setUnauthorizedHandler(null)
|
||||
}, [auth])
|
||||
|
||||
// 3) Проактивная re-auth при expiry. automaticSilentRenew: true должен
|
||||
// обновлять access_token автоматически, но молча падает если:
|
||||
// - refresh_token истёк (Keycloak SSO Session Idle ~30 мин)
|
||||
// - silent renew iframe заблокирован 3rd-party cookie policy
|
||||
// - silentRenewError не подцеплен глобально
|
||||
//
|
||||
// В этих случаях user.expired=true, accessToken=null, requests идут
|
||||
// как anonymous → backend с requireInternal() отдаёт 403 (не 401),
|
||||
// 401-handler не срабатывает → юзер видит «403 forbidden» вместо
|
||||
// привычного relogin redirect. Симптом который пользователь репортит
|
||||
// как «перелогин решает».
|
||||
//
|
||||
// Логика: если user был authenticated и стал expired — пытаемся
|
||||
// signinSilent (вдруг refresh ещё жив), на failure full redirect.
|
||||
useEffect(() => {
|
||||
if (!auth.user?.expired) {
|
||||
reauthing.current = false
|
||||
return
|
||||
}
|
||||
if (reauthing.current || auth.isLoading || auth.activeNavigator) return
|
||||
if (typeof window !== 'undefined' && window.location.search.includes('error=')) {
|
||||
return
|
||||
}
|
||||
reauthing.current = true
|
||||
auth
|
||||
.signinSilent()
|
||||
.then(() => {
|
||||
reauthing.current = false
|
||||
})
|
||||
.catch(() => {
|
||||
auth.signinRedirect().catch(() => {
|
||||
reauthing.current = false
|
||||
})
|
||||
})
|
||||
}, [auth, auth.user?.expired])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user