From a1e83a86bc22988e66443b81f011f6359754053a Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Mon, 25 May 2026 19:19:39 +0300 Subject: [PATCH] =?UTF-8?q?fix(admin-ui):=20403=20+=20=D0=BE=D1=82=D1=81?= =?UTF-8?q?=D1=83=D1=82=D1=81=D1=82=D0=B2=D1=83=D0=B5=D1=82=20accessToken?= =?UTF-8?q?=20=E2=86=92=20trigger=20reauth=20handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Симптом: после ~30 мин админ видит 403 на /admin/*, перелогин чинит. Цепочка событий: 1. access_token истекает 2. oidc-client-ts автоматический silent renew падает (refresh_token expired за 30 мин SSO Session Idle / iframe заблокирован) 3. auth.user становится undefined (oidc убирает после full expiry) 4. TokenSync setAccessToken(null) — accessToken в apiClient зануляется 5. Следующий запрос идёт БЕЗ Authorization header 6. Backend (ORDINIS_AUTH_REQUIRED=false) пропускает как anonymous → ScopeContext = PUBLIC → controller с requireInternal() кидает 403 401-handler не срабатывает на 403 → юзер залипает. Также мой предыдущий useEffect в TokenSync (MR !267) триггерит на auth.user?.expired === true, но если user удалён полностью — optional-chaining вернёт undefined и useEffect не сработает. Fix: расширил interceptor чтобы на (status === 403 && accessToken == null) тоже вызывать unauthorizedHandler. Этот handler — single source of truth, делает signinSilent → fallback signinRedirect. Не цепляет legitimate 403 (когда у юзера ЕСТЬ токен но scope недостаточно — например viewer пытается dropить INTERNAL dict). accessToken != null в таких случаях → ветка не сработает. --- ordinis-admin-ui/src/api/client.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/ordinis-admin-ui/src/api/client.ts b/ordinis-admin-ui/src/api/client.ts index 8520ee3..f9c25fd 100644 --- a/ordinis-admin-ui/src/api/client.ts +++ b/ordinis-admin-ui/src/api/client.ts @@ -72,13 +72,22 @@ apiClient.interceptors.request.use((config) => { return config }) -// Единственный 401 interceptor. Логика silent renew / redirect / loop guard -// делегируется handler'у который TokenSync устанавливает через -// setUnauthorizedHandler(). +// Auth-related interceptor. 401 классический «нет/невалидный JWT» → +// handler делает silent renew / redirect. +// +// 403 + accessToken=null обрабатывается так же. Корень: на проде +// ORDINIS_AUTH_REQUIRED=false → Spring Security пропускает anonymous → +// контроллер видит PUBLIC scope → отдаёт 403 (не 401). Это происходит +// когда silent-renew падает, oidc-client-ts убирает user, accessToken +// зануляется → следующий запрос идёт без Bearer. Без этой ветки юзер +// залипал на 403 и видел «forbidden» вместо relogin redirect'а. apiClient.interceptors.response.use( (resp) => resp, (error) => { - if (error?.response?.status === 401 && unauthorizedHandler) { + const status = error?.response?.status + const needsReauth = + status === 401 || (status === 403 && accessToken == null) + if (needsReauth && unauthorizedHandler) { try { unauthorizedHandler(error) } catch (e) {