+
+
+
+```
+
+### 7.2 Добавление роута для SSO Login
+
+**Файл:** [`garage/src/router.ts`](../garage/src/router.ts)
+
+**Действия:**
+1. Добавить роут для `/login/sso/:workspaceId?`
+
+**Код:**
+```typescript
+{
+ path: '/login/sso/:workspaceId?',
+ name: 'sso-login',
+ component: () => import(/* webpackChunkName: 'auth-pages' */ './components/auth/SsoLogin.vue'),
+ props: route => ({
+ workspaceId: route.params.workspaceId,
+ }),
+},
+```
+
+### 7.3 Обновление обычной страницы Login
+
+**Файл:** [`garage/src/components/auth/Login.vue`](../garage/src/components/auth/Login.vue)
+
+**Действия:**
+1. Добавить кнопку "Continue with SSO" на страницу логина
+2. При клике открывать форму для ввода `workspaceId` или редирект на `/login/sso`
+
+**Код (добавить в template):**
+```vue
+
+
+
+```
+
+**Код (добавить в methods):**
+```javascript
+goToSsoLogin() {
+ // Можно показать модальное окно для ввода workspaceId
+ // Или редирект на /login/sso
+ this.$router.push({ name: 'sso-login' });
+},
+```
+
+---
+
+## Этап 8: Фронтенд - Настройки SSO в Workspace
+
+### 8.1 Создание компонента настроек SSO
+
+**Файл:** [`garage/src/components/workspace/settings/Sso.vue`](../garage/src/components/workspace/settings/Sso.vue)
+
+**Действия:**
+1. Создать компонент для настройки SSO
+2. Форма с полями:
+ - Checkbox "Enable SSO"
+ - Checkbox "Enforce SSO" (только если SSO enabled)
+ - IdP Entity ID
+ - SSO URL
+ - X.509 Certificate (textarea)
+ - NameID Format (select)
+ - Attribute Mapping (email, name)
+3. Кнопка "Save"
+4. Показывать информацию о SP Entity ID и ACS URL для администратора IdP
+
+**Код (структура):**
+```vue
+
+
+
{{ $t('workspaces.settings.sso.title') }}
+
+
+
+
+
+
+```
+
+### 8.2 Добавление роута для SSO настроек
+
+**Файл:** [`garage/src/router.ts`](../garage/src/router.ts)
+
+**Действия:**
+1. Добавить роут в children workspace settings
+
+**Код:**
+```typescript
+{
+ path: 'sso',
+ name: 'workspace-settings-sso',
+ component: () => import(/* webpackChunkName: 'workspace-settings' */ './components/workspace/settings/Sso.vue'),
+ props: true,
+},
+```
+
+### 8.3 Добавление пункта меню в Layout
+
+**Файл:** [`garage/src/components/workspace/settings/Layout.vue`](../garage/src/components/workspace/settings/Layout.vue)
+
+**Действия:**
+1. Добавить ссылку на SSO настройки в меню (только для админов)
+
+**Код:**
+```vue
+
+ {{ $t('workspaces.settings.sso.title') }}
+
+```
+
+### 8.4 Добавление Vuex actions
+
+**Файл:** [`garage/src/store/modules/workspace/actionTypes.js`](../garage/src/store/modules/workspace/actionTypes.js)
+
+**Действия:**
+1. Добавить константы для новых actions
+
+**Код:**
+```javascript
+export const FETCH_WORKSPACE_SSO_SETTINGS = 'FETCH_WORKSPACE_SSO_SETTINGS';
+export const UPDATE_WORKSPACE_SSO = 'UPDATE_WORKSPACE_SSO';
+```
+
+**Файл:** [`garage/src/store/modules/workspace/index.js`](../garage/src/store/modules/workspace/index.js) (или соответствующий файл)
+
+**Действия:**
+1. Добавить actions для работы с SSO настройками
+
+**Код:**
+```javascript
+import * as workspaceApi from '@/api/workspace';
+
+async [FETCH_WORKSPACE_SSO_SETTINGS]({ commit }, workspaceId) {
+ const response = await workspaceApi.getSsoSettings(workspaceId);
+ return response.data.workspaceSsoSettings;
+},
+
+async [UPDATE_WORKSPACE_SSO]({ commit }, { workspaceId, config }) {
+ await workspaceApi.updateSsoSettings(workspaceId, config);
+},
+```
+
+### 8.5 Добавление API методов
+
+**Файл:** [`garage/src/api/workspace/queries.js`](../garage/src/api/workspace/queries.js) (или соответствующий файл)
+
+**Действия:**
+1. Добавить GraphQL queries и mutations для SSO
+
+**Код:**
+```javascript
+export const GET_SSO_SETTINGS = gql`
+ query GetSsoSettings($workspaceId: ID!) {
+ workspaceSsoSettings(workspaceId: $workspaceId) {
+ enabled
+ enforced
+ type
+ saml {
+ idpEntityId
+ ssoUrl
+ x509Cert
+ nameIdFormat
+ attributeMapping {
+ email
+ name
+ }
+ }
+ }
+ }
+`;
+
+export const UPDATE_SSO_SETTINGS = gql`
+ mutation UpdateSsoSettings($workspaceId: ID!, $config: WorkspaceSsoConfigInput!) {
+ updateWorkspaceSso(workspaceId: $workspaceId, config: $config)
+ }
+`;
+```
+
+**Файл:** [`garage/src/api/workspace/index.js`](../garage/src/api/workspace/index.js)
+
+**Действия:**
+1. Добавить методы для вызова queries/mutations
+
+**Код:**
+```javascript
+import { GET_SSO_SETTINGS, UPDATE_SSO_SETTINGS } from './queries';
+import client from '../client';
+
+export async function getSsoSettings(workspaceId) {
+ return client.query({
+ query: GET_SSO_SETTINGS,
+ variables: { workspaceId },
+ });
+}
+
+export async function updateSsoSettings(workspaceId, config) {
+ return client.mutate({
+ mutation: UPDATE_SSO_SETTINGS,
+ variables: { workspaceId, config },
+ });
+}
+```
+
+---
+
+## Этап 9: Реализация политики доступа (Provisioning) — пропускаем
+
+### 9.1 Добавление политики в Workspace
+
+**Файл:** `api/src/sso/types.ts`
+
+**Действия:**
+1. Добавить тип для политики доступа
+
+**Код:**
+```typescript
+/**
+ * User provisioning policy
+ */
+export type ProvisioningPolicy = 'invite-only' | 'jit';
+
+/**
+ * SSO configuration for workspace
+ */
+export interface WorkspaceSsoConfig {
+ // ... существующие поля
+
+ /**
+ * User provisioning policy
+ */
+ provisioningPolicy?: ProvisioningPolicy;
+}
+```
+
+**Примечание:** В MVP можно использовать одну политику по умолчанию (например, `jit`), но лучше сделать настраиваемой.
+
+### 9.2 Реализация логики provisioning в Controller
+
+**Файл:** `api/src/sso/saml/controller.ts`
+
+**Действия:**
+1. Обновить метод `handleUserProvisioning` для поддержки политик:
+ - `invite-only`: пользователь должен быть приглашён в workspace
+ - `jit`: автоматически добавлять пользователя при первом SSO входе
+
+**Код:**
+```typescript
+private async handleUserProvisioning(
+ workspaceId: string,
+ samlData: SamlResponseData,
+ workspace: WorkspaceModel
+): Promise {
+ const policy = workspace.sso?.provisioningPolicy || 'jit';
+
+ /**
+ * Find user by email
+ */
+ let user = await this.factories.usersFactory.findByEmail(samlData.email);
+
+ if (!user) {
+ /**
+ * Create new user (only for JIT)
+ */
+ if (policy === 'jit') {
+ user = await this.createUserFromSaml(samlData);
+ } else {
+ throw new AuthenticationError('User not found and provisioning is invite-only');
+ }
+ }
+
+ /**
+ * Check if user is a member of the workspace
+ */
+ const member = await workspace.getMemberInfo(user._id.toString());
+
+ if (!member || WorkspaceModel.isPendingMember(member)) {
+ if (policy === 'invite-only') {
+ throw new AuthenticationError('User is not a member of this workspace');
+ } else if (policy === 'jit') {
+ /**
+ * Add user to workspace
+ */
+ await workspace.addMember(user._id.toString());
+ }
+ }
+
+ /**
+ * Link SAML identity
+ */
+ await user.linkSamlIdentity(workspaceId, samlData.nameId, samlData.email);
+
+ return user;
+}
+
+private async createUserFromSaml(samlData: SamlResponseData): Promise {
+ /**
+ * Create user without password
+ */
+ const userData: Partial = {
+ email: samlData.email,
+ name: samlData.name || samlData.email,
+ /**
+ * Password is not set - only SSO login is allowed
+ */
+ };
+
+ /**
+ * TODO: Use UsersFactory to create user
+ */
+ /**
+ * This requires access to factories, which is already in the controller
+ */
+ const userId = await this.factories.usersFactory.collection.insertOne(userData);
+
+ return new UserModel({
+ ...userData,
+ _id: userId.insertedId,
+ } as UserDBScheme);
+}
+```
+
+---
+
+## Этап 10: Enforcement (SSO Required)
+
+### 10.1 Проверка enforced в login резолвере
+
+**Файл:** [`api/src/resolvers/user.ts`](../api/src/resolvers/user.ts)
+
+**Действия:**
+1. Добавить проверку `workspace.sso.enforced` перед обычным логином
+2. Если enforced = true, запретить вход по email/password для этого workspace
+
+**Код:**
+```typescript
+async login(
+ _obj: undefined,
+ { email, password }: {email: string; password: string},
+ { factories }: ResolverContextBase
+): Promise {
+ const user = await factories.usersFactory.findByEmail(email);
+
+ if (!user || !(await user.comparePassword(password))) {
+ throw new AuthenticationError('Wrong email or password');
+ }
+
+ /**
+ * Check if there is a workspace with enforced SSO
+ */
+ const workspacesIds = await user.getWorkspacesIds([]);
+ const workspaces = await factories.workspacesFactory.findManyByIds(workspacesIds);
+
+ const enforcedWorkspace = workspaces.find(w => w.sso?.enforced);
+
+ if (enforcedWorkspace) {
+ throw new AuthenticationError(
+ `This workspace requires SSO login. Please use SSO to sign in.`
+ );
+ }
+
+ return user.generateTokensPair();
+}
+```
+
+**Примечание:** Это проверяет все workspace пользователя. Можно сделать более точную проверку, если передавать `workspaceId` в login mutation.
+**Примечание:** Корнер-кейс: если пользователь входит в несколько workspace, но только в одном из них с enforced SSO - он не сможет войти в другие workspace.
+
+---
+
+## Этап 11: Integration тесты
+
+### 11.1 E2E тесты для SSO
+
+**Файл:** [`api/test/integration/cases/sso.test.ts`](../api/test/integration/cases/sso.test.ts)
+
+**Технологии:**
+- **Jest** — тестовый фреймворк
+- **ts-jest** — поддержка TypeScript
+- **Docker Compose** — для поднятия тестового окружения (API, MongoDB, RabbitMQ, Keycloak)
+- **axios** — для HTTP запросов к API (через `apiInstance` из `test/integration/utils`)
+- **Keycloak** — SAML IdP для тестирования (в Docker контейнере)
+
+**Действия:**
+1. Добавить Keycloak сервис в `docker-compose.test.yml`
+2. Настроить Keycloak с SAML конфигурацией для тестового workspace
+3. Написать e2e тест:
+ - Создать workspace с SSO конфигурацией (через GraphQL или напрямую в MongoDB)
+ - Инициировать SSO login через GET `/auth/sso/saml/:workspaceId`
+ - Получить SAML Response от Keycloak (через браузерную автоматизацию или мокирование)
+ - Отправить SAML Response на POST `/auth/sso/saml/:workspaceId/acs`
+ - Проверить валидацию и парсинг Response
+ - Проверить создание пользователя (JIT provisioning)
+ - Проверить создание сессии Hawk (наличие токенов в редиректе)
+ - Проверить редирект на фронтенд с токенами
+
+**Примечание:** Integration тесты пишутся после завершения основной реализации всех компонентов. Для упрощения можно использовать моки SAML Response вместо реального Keycloak на первом этапе.
+
+---
+
+## Этап 12: Логирование и аудит
+
+### 12.1 Добавление логирования SSO операций
+
+**Файл:** [`api/src/sso/saml/controller.ts`](../api/src/sso/saml/controller.ts)
+
+**Действия:**
+1. Добавить логирование:
+ - Успешные SSO логины
+ - Ошибки валидации SAML Response
+ - Ошибки provisioning
+ - workspaceId (без чувствительных данных)
+ - В логах сделать подсветку важной информации через ['sgr']('../api/src/utils/ansi.ts')
+
+**Код:**
+```typescript
+import { requestLogger } from '../../utils/logger';
+
+// В handleAcs:
+try {
+ samlData = await this.samlService.validateAndParseResponse(...);
+} catch (error) {
+ console.error('SAML validation error:', {
+ workspaceId,
+ error: error.message,
+ // Не логируем SAMLResponse целиком
+ });
+ // ...
+}
+```
+
+---
+
+## Этап 13: Уменьшение времени жизни сессий для SSO
+
+### 13.1 Обновление времени жизни токенов для SSO пользователей
+
+**Файл:** [`api/src/models/user.ts`](../api/src/models/user.ts)
+
+**Действия:**
+1. Обновить метод `generateTokensPair` для проверки SSO workspace
+2. Если пользователь входит через SSO в workspace с enforced SSO, использовать короткое время жизни токенов (2 дня вместо 30)
+
+**Код:**
+```typescript
+public async generateTokensPair(workspaceId?: string): Promise {
+ /**
+ * Check if there is an enforced SSO workspace
+ */
+ let isSsoUser = false;
+
+ if (workspaceId) {
+ const workspace = await this.factories.workspacesFactory.findById(workspaceId);
+ if (workspace?.sso?.enforced) {
+ isSsoUser = true;
+ }
+ }
+
+ const refreshTokenExpiry = isSsoUser ? '2d' : '30d';
+
+ const accessToken = await jwt.sign(
+ { userId: this._id },
+ process.env.JWT_SECRET_ACCESS_TOKEN as Secret,
+ { expiresIn: '15m' }
+ );
+
+ const refreshToken = await jwt.sign(
+ { userId: this._id },
+ process.env.JWT_SECRET_REFRESH_TOKEN as Secret,
+ { expiresIn: refreshTokenExpiry }
+ );
+
+ return { accessToken, refreshToken };
+}
+```
+
+**Проблема:** Нужен доступ к factories в UserModel. Решение: передавать workspaceId и проверять через статический метод или передавать информацию о SSO извне.
+
+**Альтернативное решение:** Передавать информацию о SSO в метод `generateTokensPair` из контроллера.
+
+---
+
+## Этап 14: Документация и финализация
+
+### 14.1 Обновление README
+
+**Действия:**
+1. Добавить информацию о SSO в `api/README.md`
+2. Описать настройку env переменных
+3. Описать процесс настройки SSO для администраторов
+
+### 14.2 Обновление i18n
+
+**Файл:** [`garage/src/i18n/messages/en.json`](../garage/src/i18n/messages/en.json) и [`garage/src/i18n/messages/ru.json`](../garage/src/i18n/messages/ru.json)
+
+**Действия:**
+1. Добавить переводы для всех новых строк интерфейса
+
+### 14.3 Проверка безопасности
+
+**Действия:**
+1. Убедиться, что SSO конфигурация доступна только админам
+2. Проверить, что SAML Response валидируется полностью
+3. Проверить, что чувствительные данные не логируются
+
+---
+
+## Порядок выполнения этапов
+
+Рекомендуемый порядок:
+
+1. **Этап 1** — Подготовка (зависимости, env)
+2. **Этап 2** — Структура SSO модуля
+3. **Этап 3** — Обновление моделей + тесты моделей (TDD)
+4. **Этап 4** — SAML Service + unit тесты (TDD подход)
+5. **Этап 5** — HTTP Endpoints + unit тесты (TDD подход)
+6. **Этап 6** — GraphQL API
+7. **Этап 7** — Фронтенд SSO Login
+8. **Этап 8** — Фронтенд настройки
+9. **Этап 9** — Provisioning политика
+10. **Этап 10** — Enforcement
+11. **Этап 11** — Integration тесты (e2e)
+12. **Этап 12** — Логирование
+13. **Этап 13** — Время жизни сессий
+14. **Этап 14** — Документация
+
+---
+
+## Заметки и предупреждения
+
+1. **Хранение состояния:** In-memory store для RelayState и AuthnRequest подходит для разработки, но для production нужен Redis или другая персистентная система.
+
+2. **Валидация сертификата:** Убедиться, что библиотека `@node-saml/node-saml` правильно валидирует X.509 сертификаты.
+
+3. **Clock skew:** Учесть разницу во времени между серверами при валидации временных условий.
+
+4. **Безопасность:** Не логировать SAMLResponse целиком, только ошибки валидации.
+
+5. **Тестирование:** Использовать Keycloak в Docker для интеграционных тестов.
+
+6. **Миграции:** Поля в MongoDB появятся автоматически при первом обновлении, миграции не нужны.
+
+---
+
+## Дополнительные улучшения (не в MVP)
+
+- Redis для хранения состояния
+- SCIM поддержка
+- OIDC поддержка
+- Группы и роли из IdP
+- SAML Single Logout (SLO)
+- Метрики SSO логинов
+
diff --git a/docs/sso-testing-guide.md b/docs/sso-testing-guide.md
new file mode 100644
index 0000000..ce21d67
--- /dev/null
+++ b/docs/sso-testing-guide.md
@@ -0,0 +1,224 @@
+# SSO Testing Guide
+
+This guide explains how to test Hawk's SSO implementation using Keycloak as IdP.
+
+## Prerequisites
+
+- Docker and Docker Compose installed
+- All services from main docker-compose.yml running
+
+## Quick Start
+
+### 1. Start Keycloak
+
+From the project root:
+
+```bash
+docker-compose up -d keycloak
+```
+
+Wait for Keycloak to start (check logs):
+
+```bash
+docker-compose logs -f keycloak
+```
+
+Keycloak will be available at: **http://localhost:8180**
+
+### 2. Setup Keycloak
+
+Run the setup script to create realm, client, and test users.
+
+**Option 1: From your host machine** (recommended):
+
+```bash
+cd api/test/integration/keycloak
+KEYCLOAK_URL=http://localhost:8180 ./setup.sh
+```
+
+**Option 2: From API container** (if curl not available on host):
+
+```bash
+docker-compose exec -e KEYCLOAK_URL=http://keycloak:8180 api /keycloak/setup.sh
+```
+
+**Note:** The script requires `curl` and `bash` which are not available in the Keycloak container. Run from host or from another container that has these tools (like `api`).
+
+The script will output:
+- Admin console URL and credentials
+- Test user credentials
+- SSO configuration URLs
+- X.509 certificate location
+
+### 3. Configure SSO in Hawk
+
+#### Get Configuration Values
+
+- Go to: Realm Settings → Keys → RS256 → Certificate
+- Copy the certificate (without BEGIN/END lines)
+
+#### Configure in Hawk UI
+
+1. Open Hawk workspace settings → SSO
+2. Enable SSO
+3. Fill in the configuration:
+ - **IdP Entity ID**: `http://localhost:8180/realms/hawk`
+ - **SSO URL**: `http://localhost:8180/realms/hawk/protocol/saml`
+ - **X.509 Certificate**: Use value from step above
+ - **Name ID Format**: Email address
+ - **Attribute Mapping**:
+ - Email: `email`
+ - Name: `name` (full name)
+4. Save configuration
+
+### 4. Test SSO Login
+
+#### Manual Test
+
+1. Navigate to: `http://localhost:4000/login/sso/{workspaceId}`
+2. You'll be redirected to Keycloak login page
+3. Login with test user credentials:
+ - Email: `testuser@hawk.local`
+ - Password: `password123`
+4. After successful authentication, you'll be redirected back to Hawk
+5. Check that you're logged in
+
+#### Test Users
+
+| Username | Email | Password | Department | Title |
+|----------|-------|----------|------------|-------|
+| testuser | testuser@hawk.local | password123 | Engineering | Software Engineer |
+| alice | alice@hawk.local | password123 | Product | Product Manager |
+| bob | bob@hawk.local | password123 | Engineering | Senior Developer |
+
+#### Automated Test
+
+Run integration tests (all services start automatically in Docker):
+
+```bash
+cd api
+yarn test:integration
+```
+
+This command will:
+1. ✅ Start MongoDB, RabbitMQ, Keycloak, and API in Docker
+2. ✅ Wait for all services to be ready
+3. ✅ Configure Keycloak with test realm and users
+4. ✅ Run SSO integration tests
+5. ✅ Clean up containers after tests complete
+
+**Note:** First run may take a few minutes to download Docker images and build containers.
+
+## Keycloak Admin Console
+
+Access Keycloak admin console at: **http://localhost:8180**
+
+- Username: `admin`
+- Password: `admin`
+
+### Useful Admin Tasks
+
+#### View SAML Metadata
+
+```bash
+curl http://localhost:8180/realms/hawk/protocol/saml/descriptor
+```
+
+#### View/Edit Users
+
+1. Go to: Users → View all users
+2. Select user to edit attributes
+3. Click "Save"
+
+#### View/Edit SAML Client
+
+1. Go to: Clients → hawk-sp
+2. Edit settings as needed
+3. View/regenerate keys and certificates
+
+#### View SAML Assertions
+
+1. Go to: Clients → hawk-sp → Client Scopes
+2. View configured attributes and mappers
+
+## Testing Different Scenarios
+
+### Test JIT (Just-In-Time) Provisioning
+
+1. Login with a user that doesn't exist in Hawk database
+2. User should be automatically created
+3. User should be added to the workspace
+
+### Test SSO Enforcement
+
+1. Enable "Enforce SSO" in workspace settings
+2. Try to login with email/password → Should be blocked
+3. Try to login with SSO → Should work
+
+### Test Token Lifetime
+
+1. Login via SSO with enforced workspace
+2. Check refresh token expiry → Should be 2 days
+3. Login with regular email/password (different workspace)
+4. Check refresh token expiry → Should be 30 days
+
+### Test SAML Attributes
+
+1. Update user attributes in Keycloak admin console
+2. Login via SSO
+3. Check that attributes are correctly mapped to Hawk user
+
+## Cleanup
+
+### Stop Keycloak
+
+```bash
+docker-compose stop keycloak
+```
+
+### Remove Keycloak data
+
+```bash
+docker volume rm hawk-mono_keycloak-data
+```
+
+or
+
+```bash
+docker-compose down -v
+# This will remove ALL volumes, including MongoDB data
+```
+
+### Reset Keycloak configuration
+
+1. Stop Keycloak
+2. Remove volume
+3. Start Keycloak
+4. Run setup script again
+
+
+## CI/CD Integration
+
+For CI/CD pipelines, use docker-compose.test.yml:
+
+```yaml
+# .github/workflows/test.yml
+- name: Run integration tests
+ run: |
+ docker-compose -f api/docker-compose.test.yml up --build --exit-code-from tests tests
+```
+
+The test suite will:
+1. Start Keycloak
+2. Wait for Keycloak to be ready
+3. Configure realm via API
+4. Run integration tests
+5. Cleanup
+
+## References
+
+- [SSO in Hawk](./sso.md)
+- [SSO Implementation Plan](./sso-implementation.md)
+- [Keycloak Setup Guide](../api/docs/keycloak.md)
+- [Keycloak Documentation](https://www.keycloak.org/documentation)
+- [SAML 2.0 Specification](https://www.oasis-open.org/standards#samlv2.0)
diff --git a/docs/sso.md b/docs/sso.md
new file mode 100644
index 0000000..4e1fe6f
--- /dev/null
+++ b/docs/sso.md
@@ -0,0 +1,618 @@
+# Hawk SSO (Single Sign-On) — Specification
+
+## 1. Цели и границы
+
+### 1.1 Цель
+
+Добавить поддержку корпоративного SSO в Hawk для enterprise-клиентов, начиная с **AD FS (SAML 2.0)**, с возможностью расширения на **Keycloak** и **OIDC** в будущем.
+
+### 1.2 Не входит в MVP
+
+- SCIM / автоматическая синхронизация пользователей
+- SAML Single Logout (SLO)
+- Группы / роли из IdP
+- OIDC (будет следующим этапом)
+
+---
+
+## 2. Термины и сокращения
+
+### SSO (Single Sign-On)
+
+Механизм, при котором пользователь аутентифицируется один раз у внешнего провайдера идентификации (IdP), а затем получает доступ к Hawk без ввода логина и пароля.
+
+### IdP (Identity Provider)
+
+Внешняя система аутентификации пользователей (например, **AD FS**, **Keycloak**), которая подтверждает личность пользователя и возвращает Hawk информацию о нём.
+
+### SP (Service Provider)
+
+Приложение, которое доверяет IdP и принимает результат аутентификации. В контексте этой спецификации SP — это Hawk.
+
+### SAML (Security Assertion Markup Language)
+
+Корпоративный стандарт SSO на основе XML и цифровых подписей. Широко используется в enterprise-инфраструктурах и поддерживается AD FS. Hawk выступает в роли SAML Service Provider.
+
+### OIDC (OpenID Connect)
+
+Современный протокол аутентификации поверх OAuth 2.0. Проще в реализации и отладке, хорошо поддерживается Keycloak. Планируется для будущего расширения.
+
+### SCIM (System for Cross-domain Identity Management)
+
+Протокол для автоматической синхронизации пользователей и групп между IdP и приложением (создание, блокировка, удаление пользователей). Не входит в MVP.
+
+### ACS - Assertion Consumer Service
+
+Это endpoint Hawk, который принимает SAML-ответ (SAMLResponse) от Identity Provider (IdP) после успешной аутентификации пользователя.
+
+ACS используется на завершающем этапе SSO-входа:
+
+ 1. Hawk перенаправляет пользователя в IdP (инициация SSO).
+ 2. Пользователь аутентифицируется в IdP.
+ 3. IdP отправляет браузер пользователя на ACS endpoint Hawk с SAMLResponse.
+ 4. Hawk обрабатывает ответ и логинит пользователя.
+
+Пример: POST https://api.hawk.so/auth/sso/saml/:workspaceId/acs
+---
+
+## 3. Общий флоу настройки и использования SSO
+
+Краткая схема работы SSO в Hawk:
+
+0. В конфиг Hawk API добавляется
+ - `SSO_SP_ENTITY_ID` — уникальное имя хоука для IdP провайдеров. Например `urn:hawk:tracker:saml`;
+
+1. **Администратор Workspace**
+
+ - получает SAML-параметры у своей IT-службы или администратора IdP (AD FS / Keycloak);
+ - мы передаем администратору
+ - `SSO_SP_ENTITY_ID`
+ - `ACS endpoint (Assertion Consumer Service URL)`. Вида `https://api.hawk.so/auth/sso/saml/{workspaceId}/acs`
+ - Администратор IdP (AD FS) создаёт `Relying Party Trust`
+ - `Relying Party Identifier` = `SSO_SP_ENTITY_ID`
+ - `Assertion Consumer Service URL` = `ACS endpoint`
+ - после создания будут получены метаданные, которые надо будет ввести в Хоуке в Настройках Воркспейса
+ - типовой набор данных:
+ - IdP Entity ID;
+ - SSO URL;
+ - публичный X.509 сертификат;
+ - имена атрибутов (email, имя);
+ - при необходимости — формат NameID.
+
+2. Администратор открывает настройки Workspace в Hawk и:
+
+ - включает SSO;
+ - заполняет SAML-параметры;
+ - сохраняет конфигурацию.
+
+3. Hawk сохраняет конфигурацию в `workspaces.sso`.
+
+4. **Пользователь** открывает страницу авторизации с Deep Link вида `https://garage.hawk.so/login/`
+ - Адрес этой страницы раздает сотрудникам администратор Identity Provider
+ - Она должна быть скрыта от поисковиков
+ - Как фоллбэк, на обычную страницу логина можно добавить кнопку «Continue with SSO», которая открывает форму для ввода `workspaceId`
+ - `workspaceId` может быть заменен на `workspace.slug` (когда такая фича появится в Хоуке)
+
+5. Фронтенд Hawk инициирует SSO-вход, перенаправляя браузер в Hawk API на sso-initiation адрес (см 5.2.1) с передачей `RelayState` в параметрах
+
+ - `redirect to https://api.hawk.so/auth/sso/saml/:workspaceId?returnUrl=/workspace/`.
+
+6. Бэкенд Hawk:
+
+ - формирует `SAML AuthnRequest`;
+ - сохраняет `RelayState` (контекст возврата пользователя);
+ - читает `returnUrl`
+ - сохраняет его во временное хранилище (cookie / in-memory / redis)
+ - кладёт идентификатор этого состояния в `RelayState`
+ - перенаправляет браузер пользователя на страницу логина IdP.
+
+7. Пользователь аутентифицируется в IdP (пароль, MFA и т.д.).
+
+8. IdP возвращает браузер пользователя обратно в Hawk (ACS callback) с `SAMLResponse`.
+
+9. Бэкенд Hawk:
+
+ - валидирует `SAML Response`;
+ - разобирает `Assertion`
+ - извлекает `NameID`
+ - идентифицирует пользователя по `NameID`;
+ - ищет users.identities[workspaceId].saml.id
+ - если нашли — логиним
+ - если нет — применяем политику доступа (invite / JIT)
+ - создаёт сессию Hawk
+
+10. Пользователь перенаправляется в интерфейс Hawk (на страницу из `RelayState`).
+
+---
+
+## 3.1 Общая модель SSO в Hawk
+
+### 3.1 Уровень SSO
+
+SSO настраивается **на уровне workspace**.
+
+Один пользователь может:
+
+- состоять в нескольких workspace;
+- использовать SSO в одном workspace;
+- использовать email/password в другом workspace.
+
+SSO не является глобальной настройкой аккаунта пользователя.
+
+---
+
+## 4. Хранилище данных (MongoDB)
+
+### 4.1 Workspace
+
+В коллекцию `workspaces` добавляется опциональное поле `sso`.
+
+```js
+workspaces: {
+ _id: "66a...",
+ name: "Frontend [production]",
+ tariff: "TEAM",
+ isBlocked: false,
+ ...
+
+ sso: {
+ enabled: true,
+ enforced: false,
+ type: "saml",
+
+ saml: {
+ // идентификатор IdP, который клиент присылает вам в метаданных
+ idpEntityId: "https://adfs.company.com/adfs/services/trust",
+
+ // куда редиректить пользователя для логина
+ ssoUrl: "https://adfs.company.com/adfs/ls/",
+
+ // публичный сертификат IdP в PEM (для проверки подписи SAMLResponse)
+ x509Cert: "-----BEGIN CERTIFICATE-----
+MIIC...
+-----END CERTIFICATE-----",
+
+ // желаемый формат NameID
+ nameIdFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
+
+ // как искать нужные атрибуты в Assertion
+ attributeMapping: {
+ email: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
+ name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
+ }
+ }
+ }
+}
+```
+
+#### Описание полей `workspaces.sso`
+
+- **enabled** — включает или выключает SSO для workspace.
+- **enforced** — если `true`, вход в этот workspace возможен только через SSO; вход по email/password запрещён.
+- **type** — тип провайдера (`saml` для MVP).
+
+#### Описание полей `workspaces.sso.saml`
+
+- **idpEntityId** — уникальный идентификатор IdP (из SAML-метаданных IdP).
+ - **Зачем:** Hawk использует на этапе 9 Флоу для валидации ответа, чтобы убедиться, что пришедший `SAMLResponse` относится именно к ожидаемому провайдеру и конфигурации.
+ - На практике это участвует в проверках "этот ответ предназначен Hawk" (см. [5.2.2](#acs-callback)).
+
+ Внутри `SAMLResponse` есть блок `Assertion`, в котором есть поле `Audience`. Упрощенно:
+
+ ```xml
+ urn:hawk:tracker:saml
+ ```
+ что означает «Я подтверждаю личность пользователя **для вот этого приложения**»
+ Без этой проверки было бы возможно принять SAMLResponse, от другого IdP, от другой конфигурации, предназначенный для другого приложения.
+
+- **ssoUrl** — URL IdP, на который Hawk перенаправляет пользователя для аутентификации.
+
+- **x509Cert** — публичный X.509 сертификат IdP, используемый Hawk для проверки цифровой подписи SAML Response.
+
+- **nameIdFormat** — формат значения `NameID`, которое IdP будет помещать в SAML Assertion.
+ - Пример (email-формат): `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` — тогда `NameID` выглядит как `alice@company.com`.
+ - Пример (persistent identifier): `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` — тогда `NameID` выглядит как стабильный идентификатор вида `f2a7c1c8-7b6a-4c5a-9c2d-...` (не меняется при смене email).
+ - Это константа (enum) из спецификации SAML
+ - Формально:
+ - urn: — Uniform Resource Name,
+ - oasis — организация, которая стандартизирует SAML
+ - SAML:1.1:nameid-format:emailAddress — конкретный формат идентификатора
+ - IdP (AD FS / Keycloak) понимает эту строку
+ - Hawk передаёт её в AuthnRequest как пожелание: «пришли мне NameID в таком формате»
+
+- **attributeMapping** — сопоставление атрибутов из SAML Assertion с полями Hawk.
+ - **Зачем:** разные IdP/инсталляции отдают email/имя под разными именами claim’ов.
+ - Пример для AD FS:
+ - `email: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"`
+ - `name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"`
+ - Это claim identifiers
+ - Microsoft (AD / ADFS) использует URI-подобные строки как имена атрибутов
+ - Пример SAML Assertion (упрощённо):
+
+ ```xml
+
+ alice@company.com
+
+ ```
+ - значит "при разборе XML найти `` с соответствующим `Name`, взять его значение и положить в `user.email`"
+ - Пример для Keycloak (часто):
+ - `email: "email"`
+ - `name: "given_name"` или `"name"`
+
+Поле `sso`:
+
+- присутствует только у workspace с включённым SSO;
+- не возвращается в обычных GraphQL-запросах;
+- доступно только администраторам workspace.
+
+---
+
+### 4.2 User identities
+
+В коллекции `users` хранится информация о внешних идентичностях пользователя.
+
+```js
+users: {
+ _id,
+ email,
+ passwordHash,
+ workspaces: { ... },
+
+ identities?: {
+ "": {
+ saml: {
+ id: string,
+ email: string
+ }
+ }
+ }
+}
+```
+
+#### Что такое `identities`
+
+`identities` — это связь между пользователем Hawk и его учётной записью во внешнем IdP **в контексте конкретного Workspace**.
+
+- ключ верхнего уровня — `workspaceId`
+- внутри хранится информация о том, как пользователь идентифицируется в IdP
+
+#### Поля `saml`
+
+- **id** — уникальный идентификатор пользователя в IdP (значение `NameID` из SAML Assertion). Это стабильный идентификатор, не зависящий от email.
+- **email** — email пользователя на момент первичной привязки (для аудита и диагностики).
+
+---
+
+## 5. Аутентификация и API
+
+### 5.1 GraphQL
+
+- GraphQL API не используется для коммуникации с IdP
+- Используется для внутренних операций в Хоуке:
+ - обновить настройки `sso` воркспейса
+
+---
+
+### 5.2 HTTP endpoints для SAML
+
+SAML использует отдельные HTTP endpoints, так как IdP не умеет работать с GraphQL.
+
+---
+
+#### 5.2.1 Инициация входа (SSO Login Initiation)
+
+```
+GET /auth/sso/saml/:workspaceId?redirectUrl=/workspace/
+```
+
+**Что это:** Начальная точка SSO-аутентификации. Этот endpoint запускает процесс SAML-входа и в итоге приводит пользователя на страницу логина IdP.
+
+Endpoint должен вызываться как переход браузера (navigation, а не аякс-запрос), чтобы HTTP 302 реально отправил пользователя на `ssoUrl IdP`.
+
+**Кто и когда инициирует:**
+
+- **Инициатор — браузер пользователя**, по действию во фронтенде Hawk.
+- Типовой момент вызова: пользователь приходит по ссылке вида `/login/` или на странице логина нажимает кнопку **«Continue with SSO»**, вводя после этого `workspaceId` (или `slug`).
+
+**Что такое RelayState:** `RelayState` — термин из SAML-протокола. Это параметр, который **бэкенд Hawk** передаёт IdP вместе с SAML AuthnRequest и затем получает обратно на ACS endpoint.
+
+В контексте Hawk:
+
+- **инициатор** — фронтенд Hawk (действие пользователя);
+- **формирует и сохраняет RelayState** — бэкенд Hawk;
+- **возвращает RelayState обратно** — IdP;
+- **использует RelayState** — бэкенд Hawk.
+
+Зачем он нужен:
+
+- сохранить контекст входа (например, куда вернуть пользователя после SSO: конкретный проект, страницу или workspace).
+
+Обычно RelayState содержит либо:
+
+- `returnUrl` (если разрешено), либо
+- короткий идентификатор состояния, по которому Hawk на сервере находит сохранённые данные.
+
+Пример адреса, на который фронтендом Hawk редиректит пользователя при начале SSO-логина:
+
+```
+https://api.hawk.so/auth/sso/saml/:workspaceId?returnUrl=/workspace/
+```
+
+В этом случае:
+
+- фронтенд Hawk передаёт `returnUrl` в урле;
+- бэкенд Hawk сохраняет его как RelayState;
+- после успешного SSO пользователь будет возвращён на `/workspace/`.
+- Обычно это `returnUrl` или короткий идентификатор состояния, который Hawk сопоставляет с сохранёнными данными (чтобы не хранить длинный URL в открытую).
+
+**Что делает endpoint:**
+
+1. Проверяет, что `workspaces.sso.enabled = true`.
+2. Формирует SAML AuthnRequest (запрос аутентификации).
+3. Сохраняет `RelayState` (куда вернуть пользователя после логина).
+4. Перенаправляет браузер пользователя на `ssoUrl` IdP.
+
+---
+
+#### 5.2.2 ACS callback (Assertion Consumer Service)
+
+```
+POST /auth/sso/saml/:workspaceId/acs
+```
+
+**Что это:** Endpoint, который принимает результат аутентификации от IdP.
+
+**Что делает endpoint:**
+
+1. Принимает `SAMLResponse` (form POST).
+ - Content-Type: application/x-www-form-urlencoded
+ - В теле запроса:
+ ```
+ SAMLResponse=
+ RelayState=
+ ```
+
+2. Валидирует:
+ - цифровую подпись (через `x509Cert`);
+ - Зачем: убедиться, что ответ действительно подписан IdP, которому мы доверяем, и не был изменён.
+ - Как технически
+ - В XML SAML Response есть блок:
+
+ ```xml
+ ...
+ ```
+ - Хоук извлекает подпись
+ - берёт публичный сертификат из базы - `workspaces.sso.saml.x509Cert`;
+ - криптографически проверяет подпись XML
+ - если подпись невалидна или подписано другим ключом — отказ (SSO login failed)
+ - Реализация через SAML-библиотеку (@node-saml/node-saml)
+
+ - `Audience`
+ - Это защита от ситуации: “Этот ответ был выдан не для Hawk”
+ - Внутри Assertion есть:
+ ```xml
+ urn:hawk:tracker:saml
+ ```
+ - Hawk знает свой `SSO_SP_ENTITY_ID` (из .env).
+ - сравнивает `Audience === SSO_SP_ENTITY_ID`.
+ - Если не совпало → ответ отклоняется.
+ - `Recipient`
+ - Это URL, куда IdP ожидал отправить ответ:
+ ```xml
+
+ ```
+
+ - Hawk знает свой URL ACS endpoint (из роутов)
+ - сравнивает `Recipient === ACS_URL`.
+ - Если не совпало → ответ отклоняется.
+ - `InResponseTo`;
+ - Зачем: защита от replay-атак.
+ - На этапе `GET /auth/sso/saml/:workspaceId` Hawk генерирует `AuthnRequest`, у которого есть уникальный ID, например
+ ```
+ _a8f7c3...
+ ```
+ - Hawk API сохраняет этот ID (в сессии / кеше)
+ - В `SAMLResponse` есть:
+ ```
+ InResponseTo="_a8f7c3..."
+ ```
+ - Hawk проверяет что
+ - такой `AuthnRequest` реально был
+ - он ещё не использован
+ - он относится к этому workspace
+ - Если `InResponseTo` неизвестен или уже использован — отказ
+ - временные ограничения Assertion.
+ - В Assertion есть:
+ ```xml
+
+ ```
+ - На основе текущего серверного времени Хоук проверяет
+ ```
+ now >= NotBefore
+ now < NotOnOrAfter
+ ```
+ - учитывает небольшой clock skew (обычно ±2–5 минут).
+ - Если Assertion просрочена или ещё “не действительна” — отказ
+
+3. Извлекает из XML :
+ - `NameID` → `users.identities[workspaceId].saml.id`;
+ - пример в xml
+ ```xml
+
+ alice@company.com
+
+ ```
+ - Это основной идентификатор пользователя для SSO
+ - email пользователя.
+ - пример в xml
+ ```xml
+
+ alice@company.com
+
+ ```
+ - Hawk:
+ - берёт `attributeMapping.email` из `workspaces.sso`;
+ - ищет `Attribute` с таким `Name`;
+ - берёт его значение;
+ - использует как `email`.
+ - Если email отсутствует или не найден по mapping:
+ - либо отказ,
+ - либо логика fallback (решается политикой).
+
+4. Находит по (workspaceId, saml.id) или создаёт пользователя Hawk.
+5. Проверяет политику доступа workspace.
+ - invite-only / JIT
+ - enforced SSO
+6. Создаёт сессию Hawk.
+7. Перенаправляет пользователя в web-app
+ - используя RelayState
+ - 302
+
+CSRF-проверка не применяется — безопасность обеспечивается криптографией SAML.
+
+---
+
+## 6. Политика доступа и provisioning пользователей
+
+### 6.1 Provisioning пользователей
+
+Provisioning — это процесс определения, **может ли пользователь получить доступ к workspace** после успешной аутентификации в IdP.
+
+SSO подтверждает личность пользователя, но не гарантирует, что он имеет право доступа к workspace.
+
+### 6.2 Политика доступа пользователя
+
+В MVP используется одна из политик (выбирается заранее):
+
+- **Только приглашённые** — пользователь должен уже быть членом workspace.
+- **Автоматическое добавление (JIT provisioning)** — пользователь добавляется в workspace при первом успешном SSO-входе.
+
+**JIT** = *Just-In-Time*.
+
+Политика применяется после успешной SAML-аутентификации.
+
+---
+
+## 7. Enforcement (SSO Required)
+
+Если `workspaces.sso.enforced = true`:
+
+- вход по email/password в этот workspace запрещён;
+- SSO является единственным способом входа;
+- другие workspace пользователя не затрагиваются.
+
+---
+
+## 8. Web UI
+
+### 8.1 Login flow
+
+- пользователь переходит на страницу логина Workspace (заранее зная ее адрес вида `/login/sso/`) или перейдя по кнопке "Continue with SSO" и введя `workspaceId` (или `slug`)
+- Hawk проверяет наличие `sso.enabled`;
+- если SSO включён — редирект в API.
+
+---
+
+### 8.2 Настройки workspace (admin only)
+
+**Что это:** Экран настройки SSO для администраторов workspace.
+
+**Процесс:**
+
+1. Администратор получает параметры SAML у своей IT-службы / IdP:
+ - IdP Entity ID
+ - SSO URL
+ - X.509 сертификат
+2. Администратор вводит эти данные в UI Hawk.
+3. Фронтенд вызывает GraphQL-мутацию обновления workspace.
+4. Hawk сохраняет конфигурацию в `workspaces.sso`.
+5. Администратор может включить `enabled` и `enforced`.
+
+---
+
+## 9. Безопасность
+
+### 9.1 Идентификация пользователя
+
+На практике это означает:
+
+- Hawk связывает пользователя с IdP **по стабильному идентификатору**.
+ - В SAML внутри `SAMLResponse` есть Assertion, в которой есть поле `NameID` — это и есть идентификатор пользователя в IdP для данного приложения.
+ - Hawk сохраняет его в `users.identities[workspaceId].saml.id` и при каждом следующем SSO-входе ищет пользователя по этой паре `(workspaceId, saml.id)`.
+- email используется как атрибут, но не как идентификатор входа;
+- смена email в IdP не приводит к потере доступа.
+
+### 9.2 Email не является primary identifier
+
+Это означает:
+
+- Hawk не использует email для поиска пользователя при SSO-входе;
+- совпадение email без совпадения `NameID` не считается тем же пользователем;
+- это защищает от подмены аккаунта.
+
+### 9.3 Ограничение доступа к SAML-конфигурации
+
+Для этого необходимо:
+
+- **Разделить публичную и админскую модель workspace** в GraphQL (например, разные поля/типы или разные резолверы).
+- В стандартных query (например, `workspace`, `myWorkspaces`) **не резолвить поле `sso` вообще**.
+- Сделать отдельный admin-only query/mutation (например, `workspaceSsoSettings(workspaceId)`), который:
+ - проверяет права администратора workspace;
+ - возвращает/изменяет `workspaces.sso`.
+
+Технически это означает:
+
+- в резолверах GraphQL не выбирать `sso` по умолчанию (projection/DTO), либо вычищать его перед возвратом;
+- добавить guard `isAdmin` на резолвер, который отдаёт `sso`.
+
+### 9.4 Аудит и логирование SSO
+
+- ошибки SSO логируются на уровне backend API;
+- логируются тип ошибки и workspaceId (без хранения SAML Assertion целиком);
+- логирование используется для диагностики и поддержки клиентов.
+
+### 9.5 Разлогин пользователя в Хоуке при удалении его у IdP
+
+Hawk не выполняет онлайн-проверку статуса пользователя в IdP для каждой сессии.
+Вместо этого мы уменьшим время жизни сесси при SSO с 30 дней до 2 дней.
+Если пользователь был удалён или заблокирован в IdP, следующий SSO-вход будет отклонён IdP, и Hawk не создаст новую сессию.
+
+---
+
+## 10. Тестирование
+
+### 10.1 Unit tests
+
+Располагаются в API-сервисе.
+
+Покрывают:
+
+- парсинг и валидацию SAML Response;
+- attribute mapping;
+- linking `users.identities`;
+- политику доступа и enforcement.
+
+---
+
+### 10.2 Integration tests
+
+- Keycloak (SAML) в Docker;
+- e2e сценарий: login → callback → session.
+
+---
+
+### 10.3 Manual tests
+
+- реальный AD FS (staging или клиентский стенд);
+- проверка claim rules и сертификатов.
+
+---
+
+## 11. Будущее расширение
+
+- вынесение IdP в отдельную коллекцию;
+- поддержка OIDC;
+- SCIM;
+- группы и роли из IdP.
+