Merge branch 'revert/auth-reauth-loop' into 'main'
revert: убрать proactive re-auth + 403 reauth — спам Keycloak /token See merge request 2-6/2-6-4/terravault/ordinis!269
This commit is contained in:
@@ -72,22 +72,21 @@ apiClient.interceptors.request.use((config) => {
|
|||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
// Auth-related interceptor. 401 классический «нет/невалидный JWT» →
|
// Единственный 401 interceptor. Логика silent renew / redirect / loop guard
|
||||||
// handler делает silent renew / redirect.
|
// делегируется handler'у который TokenSync устанавливает через
|
||||||
|
// setUnauthorizedHandler().
|
||||||
//
|
//
|
||||||
// 403 + accessToken=null обрабатывается так же. Корень: на проде
|
// 403 НЕ цепляем: на проде ORDINIS_AUTH_REQUIRED=false → backend отдаёт 403
|
||||||
// ORDINIS_AUTH_REQUIRED=false → Spring Security пропускает anonymous →
|
// для anonymous (нет JWT) ИЛИ для legitimate scope-deficiency (юзер с
|
||||||
// контроллер видит PUBLIC scope → отдаёт 403 (не 401). Это происходит
|
// токеном но без INTERNAL). Невозможно reliably различить из ответа,
|
||||||
// когда silent-renew падает, oidc-client-ts убирает user, accessToken
|
// раннее попытка триггерить reauth на 403+!accessToken приводила к
|
||||||
// зануляется → следующий запрос идёт без Bearer. Без этой ветки юзер
|
// бесконечному циклу с Keycloak (signinSilent → 400 → catch → ещё
|
||||||
// залипал на 403 и видел «forbidden» вместо relogin redirect'а.
|
// signinSilent через automaticSilentRenew). Лучше показать 403 и оставить
|
||||||
|
// юзеру manual relogin, чем заспамить /token endpoint Keycloak'а.
|
||||||
apiClient.interceptors.response.use(
|
apiClient.interceptors.response.use(
|
||||||
(resp) => resp,
|
(resp) => resp,
|
||||||
(error) => {
|
(error) => {
|
||||||
const status = error?.response?.status
|
if (error?.response?.status === 401 && unauthorizedHandler) {
|
||||||
const needsReauth =
|
|
||||||
status === 401 || (status === 403 && accessToken == null)
|
|
||||||
if (needsReauth && unauthorizedHandler) {
|
|
||||||
try {
|
try {
|
||||||
unauthorizedHandler(error)
|
unauthorizedHandler(error)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ import { LEGACY_TOKEN_STORAGE_KEY } from './oidcConfig'
|
|||||||
export function TokenSync() {
|
export function TokenSync() {
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
const redirecting = useRef(false)
|
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.
|
// 1) Token sync — single update path к accessToken holder в client.ts.
|
||||||
// ВАЖНО: пропускаем токен ТОЛЬКО когда user реально authenticated AND token
|
// ВАЖНО: пропускаем токен ТОЛЬКО когда user реально authenticated AND token
|
||||||
@@ -96,41 +93,5 @@ export function TokenSync() {
|
|||||||
return () => setUnauthorizedHandler(null)
|
return () => setUnauthorizedHandler(null)
|
||||||
}, [auth])
|
}, [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
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user