ADR-UI-010. Аутентификация
Простая аутентификация по логину и паролю с access-token (JWT) в HTTP-заголовке Bearer. Токен — opaque для UI (JWT не парсим). Сессия живёт в сигнальном сторе AuthStore; профиль грузится при старте; маршруты закрывает AuthGuard; HTTP-interceptor подмешивает токен и обрабатывает 401/403. Закрывает отложенный «второй режим layout» (экраны входа) и гарды маршрутов.
1. Контекст и постановка задачи
ADR_011_AbacAuthorization вводит доступ; бэкенд-сторона — ADR_SRV_006_AuthImplementation (вход по паролю → JWT, профиль с UI-claims, отзыв токена). Фронт был спроектирован «с запасом»: основной layout не зависит от доступа, а «второй режим» (экраны входа) и гарды маршрутов явно отложены до появления ролей (ADR_UI_005_Layout, ADR_UI_006_Routing). Настало время закрыть это.
Зафиксируем: хранение и срок жизни токена; состав сессионного стора; инициализацию профиля при старте; гард аутентификации; поведение при 401/403; экраны входа/выхода. Права и их проверку выносим в ADR_UI_011_Claims.
2. Драйверы решения
- Сохранение сессии при перезагрузке страницы (F5).
- Гарантия инициализации профиля к моменту, когда первый гард/страница читает claims.
- Декларативная защита маршрутов — гард на корневой обёртке (ADR_UI_006_Routing).
- Единая точка сессии — весь код про токен/профиль в одном сторе; страницы про это не думают.
- Весь трафик через сгенерированный клиент (ADR_UI_012_ApiAndModels) — auth-эндпоинты не исключение.
3. Рассмотренные варианты
- A. Cookie-сессия (HttpOnly). Безопаснее к XSS, но требует серверных сессий/CSRF-защиты и хуже ложится на stateless-JWT бэкенда.
- B. Access-token в
localStorage, opaque для UI (выбрано). Простая модель под JWT-бэкенд; токен не парсим, права берём из профиля. Стандартный компромисс SPA (требования к XSS жёстче). - C. Token в памяти (без хранилища). Теряется при F5 — неудобно; отклонено.
4. Решение
Выбран Вариант B: access-token в localStorage, сессия — в сигнальном сторе AuthStore.
4.1. Хранение токена
- Ключ:
icd.accessTokenвlocalStorage. Значение — строка; на UI токен opaque (JWT не парсим). - Очистка при logout / 401: удалить ключ + сбросить сигналы стора.
4.2. AuthStore (сессионный стор)
Singleton (providedIn: 'root'), сигнальный (ADR-UI-008); имя/файл — по ADR-UI-002 (auth-store.ts).
export interface UserProfile {
userId: string;
fullName: string;
login: string;
claims: readonly string[]; // UI-claims из профиля (см. ADR-UI-011)
}
@Injectable({ providedIn: 'root' })
export class AuthStore {
private readonly _profile = signal<UserProfile | null>(null);
public readonly profile = this._profile.asReadonly();
public readonly isAuthenticated = computed(() => this._profile() !== null);
login(login: string, password: string): Observable<void> { /* ... + loadProfile */ }
loadProfile(): Observable<UserProfile | null> { /* GetUserProfile по токену */ }
clearSession(): void { /* remove token + _profile.set(null) */ }
getAccessToken(): string | null { /* localStorage */ }
}
Claims из профиля отдаются в ClaimService (ADR-UI-011). Auth-эндпоинты (login/logout/profile)
вызываются через сгенерированный клиент (ADR-UI-012), а не прямым HttpClient.
4.3. Bootstrap (инициализатор)
После загрузки runtime-конфига (ADR-UI-014)
ещё один provideAppInitializer: если токен есть — загрузить профиль (ошибку → clearSession);
если нет — показать /login. К моменту проверки гардом профиль уже установлен (или явно null).
4.4. AuthGuard
CanActivateFn на корневой обёртке маршрутов: isAuthenticated() → пропустить, иначе → /login.
Лежит в src/app/guards/auth.guard.ts.
4.5. HTTP-interceptor
Один функциональный interceptor icdAuthHttpInterceptor (withInterceptors([...]) в app.config.ts;
сейчас provideHttpClient() без интерсепторов):
- подмешивает
Authorization: Bearer {token}только к запросам на свойapiBaseUrl(чтобы не утекал на сторонние домены); - 401 →
clearSession()+ уведомление +/login; - 403 →
/access-deniedбез очистки сессии (запрещено ≠ не аутентифицирован; иначе петля редиректов, т.к./loginуводит залогиненного обратно).
4.6. Вход / выход
- Вход
/login(второй режим layout, без оболочки — закрывает отложенное в ADR-UI-005): если уже залогинен → на главную; иначе форма (ADR-UI-009), на submitAuthStore.login()→ успех: на главную; ошибка: сообщение из ответа. - Выход
/logout: технический маршрут, пустой шаблон; зовётclearSession()(иLogoutна бэке) →/login. Любая точка «выйти» навигирует на/logout, не зовётclearSessionнапрямую.
4.7. Чего не делаем
- Не парсим JWT на UI — права только из профиля (
GetUserProfile). - Не используем cookie/sessionStorage для токена.
- Не реализуем refresh-token — срок жизни задаёт бэк, UI реагирует на 401.
- Не дублируем navigate-логику выхода в страницах — только через
/logout.
5. Положительные следствия
- Сессия переживает перезагрузку страницы.
- Все auth-инварианты — на уровне инфраструктуры (interceptor + guard + initializer); страницам об этом думать не нужно.
- Единая точка сессии — замена mock на реальный API/правки контракта не расходятся по коду.
6. Отрицательные следствия и компромиссы
localStorageдоступен любому JS на странице → требования к XSS жёстче (стандартное ограничение SPA с токеном в хранилище; принимается).- Один interceptor — точка отказа: ошибка в нём ломает все запросы (покрывается тестами).
- Нет refresh-token: при истечении токена пользователь возвращается на вход (осознанно).
7. Проверка
- После входа и F5 пользователь остаётся в системе; шапка показывает его имя.
- 401 от сервера → авто-возврат на
/loginс уведомлением. - 403 →
/access-denied, сессия сохранена, без петли редиректов. - Замена тела
AuthStore.login/loadProfile(mock → API) не требует правок гарда/директив/страниц.
8. Открытые вопросы / отложено
- Mock-режим на время отсутствия бэка:
login()/loadProfile()возвращают фиксированный профиль; при появлении API — замена тела методов, контракт стора не меняется. - Refresh-token, OIDC — отдельными ADR при появлении требований.
- Многотабовая синхронизация выхода — слушатель
storageна ключicd.accessToken(при нужде). - Состав сообщений формы входа — согласуется с бэком; пока показываем то, что пришло.
Связанные артефакты
Документы
- ADR ADR-011. Разграничение доступа по модели ABAC (claims → функции → группы)
- ADR ADR-SRV-006. Аутентификация и энфорс ABAC на бэкенде
- ADR ADR-UI-005. Layout приложения
- ADR ADR-UI-006. Маршрутизация
- ADR ADR-UI-011. Claims и проверка прав на UI
- ADR ADR-UI-012. API-клиент и модели
- ADR ADR-UI-014. Рантайм-конфигурация