Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - Встраивание структур и интерфейсов (Embedding): https://go.vbloher.org/docs/01-core-go/embedding/ - Ошибки в Go: error, wrapping, errors.Is/As/Join: https://go.vbloher.org/docs/01-core-go/errors/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Сборщик мусора Go: concurrent tri-color mark-sweep: https://go.vbloher.org/docs/03-runtime-memory/gc/ - Тюнинг GC: GOGC и GOMEMLIMIT: https://go.vbloher.org/docs/03-runtime-memory/gogc-gomemlimit/ - GOMAXPROCS: параллелизм планировщика и проблема контейнеров: https://go.vbloher.org/docs/03-runtime-memory/gomaxprocs/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - pprof: профилирование CPU, памяти и блокировок в Go: https://go.vbloher.org/docs/03-runtime-memory/pprof/ - Execution Tracer и runtime/trace: тайминги вместо агрегатов: https://go.vbloher.org/docs/03-runtime-memory/runtime-tracing/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - Backend: https://go.vbloher.org/docs/05-backend/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - DNS: записи, резолвинг, кэширование, DNS в Go: https://go.vbloher.org/docs/06-networking/dns/ - Версии HTTP: 1.1, 2, 3: https://go.vbloher.org/docs/06-networking/http-versions/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Observability: https://go.vbloher.org/docs/09-observability/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - Notification Service: https://go.vbloher.org/docs/10-system-design/notification-service/ - Order Service: https://go.vbloher.org/docs/10-system-design/order-service/ - Payment Service: https://go.vbloher.org/docs/10-system-design/payment-service/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - DevOps: https://go.vbloher.org/docs/11-devops/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ > Модуль: Backend · Уровень: Middle+/Senior ## TL;DR - **OAuth2 — это про авторизацию (делегированный доступ), а НЕ про аутентификацию.** Он отвечает на вопрос «можно ли этому клиенту делать X от имени пользователя», а не «кто этот пользователь». Аутентификацию поверх OAuth2 даёт **OIDC** (OpenID Connect) через `id_token`. - **4 роли:** Resource Owner (пользователь), Client (приложение), Authorization Server (выдаёт токены), Resource Server (API, проверяет токены). - **Основной flow сегодня — Authorization Code + PKCE** для всех типов клиентов (web, SPA, mobile, desktop). **Client Credentials** — для machine-to-machine без пользователя. **Refresh Token grant** — для обновления access token без повторного логина. - **Deprecated:** Implicit (утечка токена через URL/Referer, отсутствие refresh) и Resource Owner Password Credentials (anti-pattern — клиент видит пароль пользователя). - **access token** — короткоживущий (минуты), bearer, передаётся в API. **refresh token** — долгоживущий, хранится максимально защищённо, обменивается на новый access token. - Безопасность держится на: `state` (CSRF), `PKCE` (перехват кода), строгая валидация `redirect_uri` (open redirect), проверка `aud`/`iss`/`exp`/подписи токена на Resource Server. --- ## Теория ### 1. OAuth2 ≠ аутентификация Самая частая концептуальная ошибка. OAuth2 (RFC 6749) спроектирован как протокол **делегированной авторизации**: ресурс-овнер разрешает третьему приложению (клиенту) доступ к своим ресурсам на Resource Server **без передачи пароля**. Access token — это **bearer-токен доступа**, а не доказательство личности. Сам по себе он не говорит надёжно, «кто залогинился»: - access token может быть непрозрачным (opaque) — клиент не должен его парсить; - access token адресован Resource Server (`aud`), а не клиенту; - факт «у меня есть валидный access token» не означает «прямо сейчас этот пользователь аутентифицировался у меня» (классическая атака — подмена токена, «confused deputy»). Для аутентификации существует **OIDC** — слой поверх OAuth2, который добавляет `id_token` (JWT), адресованный именно клиенту, с claim'ами о пользователе и о факте/времени аутентификации. ### 2. Роли | Роль | Кто это | Назначение | |---|---|---| | **Resource Owner** | Пользователь | Владеет данными, даёт согласие (consent) | | **Client** | Приложение (SPA, mobile, backend) | Хочет доступ к ресурсам от имени пользователя | | **Authorization Server (AS)** | IdP / Keycloak / Auth0 / Okta | Аутентифицирует пользователя, выдаёт токены (`/authorize`, `/token`) | | **Resource Server (RS)** | API | Хранит ресурсы, валидирует access token, отдаёт данные | Типы клиентов: - **Confidential** — может безопасно хранить секрет (backend). Аутентифицируется `client_secret`/mTLS/private_key_jwt. - **Public** — не может хранить секрет (SPA, mobile, desktop). Поэтому **обязан** использовать PKCE. ### 3. Authorization Code + PKCE Основной flow для всех типов клиентов. PKCE (RFC 7636) изначально создан для public-клиентов, но текущий best practice (OAuth 2.1) — применять PKCE **всегда**, даже для confidential-клиентов. **PKCE-параметры:** - `code_verifier` — случайная высокоэнтропийная строка (43–128 символов, base64url), генерируется клиентом на каждый запрос. - `code_challenge` = `BASE64URL(SHA256(code_verifier))` при `code_challenge_method=S256` (никогда не `plain` в проде). Идея: код авторизации, даже если его перехватят (через лог, Referer, кастомную URL-схему на мобиле), бесполезен без `code_verifier`, который никогда не покидает клиент до обмена кода на токен. ``` ┌──────────┐ ┌───────────────────┐ │ Client │ │ Authorization Srv │ └────┬─────┘ └─────────┬─────────┘ │ 1. генерирует code_verifier, │ │ code_challenge = S256(verifier), state │ │ │ │ 2. GET /authorize?response_type=code │ │ &client_id&redirect_uri&scope │ │ &state&code_challenge&code_challenge_method │ │ ────────────────────────────────────────────────► │ │ │ 3. логин + consent пользователя │ │ (Resource Owner) │ │ │ │ 4. 302 redirect_uri?code=...&state=... │ │ ◄──────────────────────────────────────────────── │ │ │ 5. проверяет, что state совпадает (CSRF) │ │ │ │ 6. POST /token grant_type=authorization_code │ │ &code&redirect_uri&client_id │ │ &code_verifier (+client_secret если confid.) │ │ ────────────────────────────────────────────────► │ │ │ 7. AS проверяет S256(verifier) │ │ == сохранённый code_challenge │ │ │ │ 8. { access_token, refresh_token, │ │ id_token (если openid), expires_in } │ │ ◄──────────────────────────────────────────────── ▼ ▼ ``` Запрос на `/authorize`: ```http GET /authorize?response_type=code &client_id=s6BhdRkqt3 &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback &scope=openid%20profile%20email%20read:orders &state=af0ifjsldkj &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256 HTTP/1.1 Host: auth.example.com ``` Обмен кода на токен: ```http POST /token HTTP/1.1 Host: auth.example.com Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback &client_id=s6BhdRkqt3 &code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk ``` Ответ: ```http HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-store { "access_token": "eyJhbGciOi...", "token_type": "Bearer", "expires_in": 300, "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA", "id_token": "eyJhbGciOi...", "scope": "openid profile email read:orders" } ``` ### 4. Client Credentials Machine-to-machine: **нет пользователя**, нет Resource Owner. Сервис аутентифицируется сам и получает токен от своего имени. Использовать только для confidential-клиентов. PKCE/`state`/`redirect_uri` здесь не применимы — нет браузерного редиректа. ```http POST /token HTTP/1.1 Host: auth.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW # client_id:client_secret Content-Type: application/x-www-form-urlencoded grant_type=client_credentials&scope=read:metrics write:metrics ``` Никакого `refresh_token` — клиент просто запрашивает новый токен по истечении. Для повышенной безопасности вместо `client_secret` используют `private_key_jwt` (RFC 7523) или mTLS. ### 5. Refresh Token grant Позволяет получить новый access token без участия пользователя. ```http POST /token HTTP/1.1 Host: auth.example.com Content-Type: application/x-www-form-urlencoded grant_type=refresh_token &refresh_token=tGzv3JOkF0XG5Qx2TlKWIA &client_id=s6BhdRkqt3 &scope=read:orders # можно только сужать scope, не расширять ``` Best practices: - **Refresh token rotation** — на каждый refresh выдаётся новый refresh token, старый инвалидируется. - **Reuse detection** — если использован уже отозванный (ротированный) refresh token, AS отзывает всю цепочку (признак кражи). - Для public-клиентов refresh token обязан быть либо ротируемым, либо sender-constrained (DPoP/mTLS). ### 6. Устаревшие flows **Implicit (`response_type=token`) — deprecated (OAuth 2.1 убирает его).** Access token возвращался прямо в URL-фрагменте редиректа. Проблемы: - токен утекает в историю браузера, логи, через `Referer`; - нет защиты от подмены (нет PKCE по дизайну); - нет refresh token. Замена: Authorization Code + PKCE для SPA. **Resource Owner Password Credentials (ROPC) — anti-pattern.** Клиент сам собирает логин/пароль пользователя и шлёт на `/token`. Нарушает базовый принцип OAuth2 (клиент не должен видеть пароль), несовместим с MFA/SSO/федерацией. Допустим только в крайне ограниченных legacy-сценариях с собственным first-party клиентом, и тоже исключён из OAuth 2.1. ### 7. access token vs refresh token | | access token | refresh token | |---|---|---| | Назначение | доступ к API (RS) | получить новый access token (на AS) | | Аудитория (`aud`) | Resource Server | Authorization Server | | TTL | короткий (мин: 5–15 мин) | длинный (часы/дни/недели) | | Формат | JWT или opaque | обычно opaque/ротируемый | | Где предъявляется | `Authorization: Bearer` в API | только на `/token` endpoint | | Хранение (web) | в памяти JS / HttpOnly cookie (BFF) | НЕ в localStorage; HttpOnly+Secure cookie или backend | | Отзыв | сложно отозвать JWT до истечения | легко отозвать на AS | Ключевой trade-off JWT access token: его нельзя «мгновенно» отозвать — поэтому делают короткий TTL + (опц.) introspection/revocation list. Это причина, почему access делают коротким, а долгоживучесть выносят в легко отзываемый refresh. ### 8. state и redirect_uri - **`state`** — непрозрачное случайное значение, привязанное к сессии браузера. Клиент кладёт его в `/authorize`, AS возвращает в редиректе, клиент **обязан** сверить. Это защита от **CSRF** (подмена кода авторизации злоумышленником). С PKCE частично перекрывается, но `state` всё равно нужен (особенно для CSRF на сам callback). Часто также служит для возврата на исходную страницу. - **`nonce`** (OIDC) — аналог state, но привязывается к `id_token` для защиты от replay; AS кладёт его в claim `nonce`. - **`redirect_uri`** — должен проверяться по принципу **точного совпадения** (exact match) с заранее зарегистрированным значением. Никаких wildcard, никакого «startsWith». Open redirect здесь = кража кода/токена. ### 9. OIDC поверх OAuth2 OIDC добавляет к OAuth2 идентификацию: - **`id_token`** — JWT, адресованный **клиенту** (`aud` = client_id), с claim'ами: `iss`, `sub` (стабильный ID пользователя), `aud`, `exp`, `iat`, `auth_time`, `nonce`, плюс профильные (`name`, `email`, ...). - Запускается добавлением scope **`openid`** в Authorization Code flow. - **scopes:** `openid` (обязателен для OIDC), `profile`, `email`, `address`, `phone` — определяют, какие claim'ы доступны. - **`/userinfo`** — endpoint, отдаёт claim'ы пользователя по предъявлении access token (когда не хочется раздувать id_token). - **Discovery** — `GET /.well-known/openid-configuration` отдаёт метаданные: `authorization_endpoint`, `token_endpoint`, `userinfo_endpoint`, `jwks_uri`, `issuer`, поддерживаемые scopes/grant types/алгоритмы. JWKS (`jwks_uri`) — публичные ключи для проверки подписи JWT. ```http GET /.well-known/openid-configuration HTTP/1.1 Host: auth.example.com ``` **Правильная валидация `id_token`:** проверить подпись по JWKS, `iss` == ожидаемый issuer, `aud` == свой client_id, `exp`/`iat`, `nonce` == отправленный, `azp` при множественных audience. ### 10. Go: golang.org/x/oauth2 Authorization Code + PKCE: ```go package main import ( "context" "crypto/rand" "crypto/sha256" "encoding/base64" "golang.org/x/oauth2" ) func newPKCE() (verifier, challenge string) { b := make([]byte, 32) _, _ = rand.Read(b) verifier = base64.RawURLEncoding.EncodeToString(b) sum := sha256.Sum256([]byte(verifier)) challenge = base64.RawURLEncoding.EncodeToString(sum[:]) return } var conf = &oauth2.Config{ ClientID: "s6BhdRkqt3", ClientSecret: "secret", // пусто для public-клиента RedirectURL: "https://app.example.com/callback", Scopes: []string{"openid", "profile", "email", "read:orders"}, Endpoint: oauth2.Endpoint{ AuthURL: "https://auth.example.com/authorize", TokenURL: "https://auth.example.com/token", }, } // Шаг 1: редирект пользователя на /authorize func authURL(state, challenge string) string { return conf.AuthCodeURL(state, oauth2.AccessTypeOffline, // запросить refresh_token oauth2.SetAuthURLParam("code_challenge", challenge), oauth2.SetAuthURLParam("code_challenge_method", "S256"), ) } // Шаг 2: в callback — проверить state, обменять code на токен func exchange(ctx context.Context, code, verifier string) (*oauth2.Token, error) { return conf.Exchange(ctx, code, oauth2.SetAuthURLParam("code_verifier", verifier), ) } ``` > В x/oauth2 v0.9+ есть готовые `oauth2.GenerateVerifier()`, `oauth2.S256ChallengeOption(verifier)` и `oauth2.VerifierOption(verifier)`, которые делают то же самое без ручного SHA256. Client Credentials: ```go import "golang.org/x/oauth2/clientcredentials" cc := &clientcredentials.Config{ ClientID: "svc-id", ClientSecret: "svc-secret", TokenURL: "https://auth.example.com/token", Scopes: []string{"read:metrics"}, } // httpClient автоматически добавляет Bearer и обновляет токен httpClient := cc.Client(context.Background()) resp, err := httpClient.Get("https://api.example.com/metrics") ``` Автообновление access token по refresh token: ```go // token уже содержит refresh_token; TokenSource сам обновит при истечении ts := conf.TokenSource(ctx, token) client := oauth2.NewClient(ctx, ts) // прозрачно рефрешит и подставляет Bearer // чтобы перехватить новый refresh token (rotation) — обернуть TokenSource: newTok, _ := ts.Token() // вернёт обновлённый токен, если истёк ``` --- ## Подводные камни / gotchas - **`oauth2.TokenSource` кеширует токен в памяти и не сообщает о ротации refresh token «наружу».** Если AS использует refresh rotation, нужно самостоятельно вычитывать обновлённый токен из `TokenSource` и персистить новый refresh token, иначе после рестарта или второго рефреша получите `invalid_grant`. - **`expires_in` — это секунды, а не unix-время.** В x/oauth2 поле `Token.Expiry` — это абсолютное время; путаница приводит к токенам, которые «всегда истекли» или «никогда не истекают». - **JWT access token не валидируется библиотекой клиента — это работа Resource Server.** RS обязан проверять подпись (JWKS с кешированием и ротацией ключей по `kid`), `iss`, `aud`, `exp`, `nbf`, и scope. Кеш JWKS без учёта ротации = внезапные отказы или, наоборот, приём токенов отозванным ключом. - **`aud` mismatch / token passthrough.** Access token, выданный для API A, нельзя слепо переслать в API B. RS обязан проверять, что `aud` == он сам, иначе «confused deputy». - **`alg: none` и алгоритм-конфьюжен (RS256→HS256).** Если верификатор берёт алгоритм из заголовка токена, атакующий может подписать токен публичным ключом как HMAC. Алгоритм должен быть зафиксирован конфигом, не взят из JWT-заголовка. - **`code_challenge_method=plain`** сводит PKCE на нет — всегда `S256`. - **redirect_uri через `startsWith`/wildcard** — классический open redirect → кража кода. Только exact match. - **state не привязан к сессии** (хранится только в куке без HttpOnly или глобально) — обход CSRF-защиты. - **Хранение токенов в `localStorage`** в SPA — доступно любому XSS. Предпочтительно паттерн **BFF** (Backend-for-Frontend): токены живут на бэкенде, в браузере только HttpOnly-сессионная кука. - **Scope ≠ permission на уровне данных.** Scope `read:orders` говорит «клиент может читать заказы», но не «этого конкретного пользователя». Авторизацию на уровень объекта (IDOR) всё равно делает RS. - **Слишком долгий access token** «потому что неудобно рефрешить» — теряется возможность отзыва. Решают коротким TTL + refresh. --- ## Вопросы на собеседовании **В:** В чём принципиальная разница между OAuth2 и OIDC, и почему «логин через OAuth2» — это часто ошибка? **О:** OAuth2 — протокол делегированной **авторизации**: выдаёт access token для доступа к ресурсам. Он не предназначен отвечать «кто пользователь». Использование access token как доказательства личности уязвимо (подмена токена, confused deputy, token substitution). OIDC — слой поверх OAuth2, добавляющий `id_token` (JWT, адресованный клиенту через `aud`) с проверяемыми claim'ами `sub`, `auth_time`, `nonce`. Именно `id_token`, а не access token, отвечает на вопрос аутентификации. **В:** Зачем нужен PKCE, если уже есть `client_secret` и `state`? **О:** PKCE защищает от **перехвата кода авторизации**. Public-клиенты (SPA, mobile) не могут хранить секрет, а на мобиле редирект по кастомной URL-схеме может перехватить вредоносное приложение. PKCE привязывает код к `code_verifier`, известному только легитимному клиенту: даже перехватив код, атакующий не обменяет его без verifier. `state` решает другую задачу — CSRF на callback. OAuth 2.1 рекомендует PKCE даже для confidential-клиентов (defence in depth, защита от утечки кода через логи/прокси). **В:** Чем `code_challenge_method=S256` отличается от `plain` и почему `plain` опасен? **О:** При `S256` в `/authorize` уходит `SHA256(verifier)`, а сам verifier — только в `/token`. При `plain` `code_challenge == code_verifier`, то есть секрет уже виден в первом запросе (в логах/истории браузера) — защита теряется. В проде только `S256`. **В:** access token vs refresh token — TTL, аудитория, хранение? **О:** access — короткоживущий (минуты), `aud` = Resource Server, предъявляется в каждом API-запросе как Bearer, в SPA хранится в памяти/через BFF. refresh — долгоживущий, `aud` = Authorization Server, предъявляется только на `/token`, хранится максимально защищённо (HttpOnly cookie / backend), легко отзывается. Короткий access + отзываемый refresh — это баланс между удобством и контролем: JWT access нельзя мгновенно отозвать, поэтому его делают коротким. **В:** Почему Implicit flow устарел и чем его заменили? **О:** Implicit возвращал access token прямо в URL-фрагменте: токен утекает через историю браузера, `Referer`, логи; нет PKCE, нет refresh token. Современная альтернатива для SPA — Authorization Code + PKCE (код одноразовый и бесполезен без verifier, токены приходят в теле POST-ответа, а не в URL). OAuth 2.1 удаляет Implicit. **В:** Когда применять Client Credentials и почему там нет refresh token и PKCE? **О:** Client Credentials — machine-to-machine, где **нет пользователя** и нет браузерного редиректа. Клиент аутентифицируется сам (`client_secret`/`private_key_jwt`/mTLS) и получает токен от своего имени. refresh не нужен — клиент в любой момент запросит новый токен по своим учётным данным. PKCE/`state`/`redirect_uri` неприменимы, так как нет фронтенд-редиректа и пользовательской сессии. **В:** Что и как должен проверять Resource Server при получении JWT access token? **О:** Подпись по ключу из JWKS (по `kid`, с кешем и учётом ротации); зафиксированный конфигом алгоритм (не из заголовка токена — иначе атака `alg:none`/RS256→HS256); `iss` == ожидаемый issuer; `aud` == собственный идентификатор (защита от confused deputy/passthrough); `exp`/`nbf`/`iat`; наличие нужных `scope`. Авторизацию на уровне конкретного объекта (что пользователь имеет право именно на эти данные) RS делает отдельно — scope её не заменяет. **В:** Что такое refresh token rotation и reuse detection? **О:** Rotation — при каждом использовании refresh token AS выдаёт новый и инвалидирует старый. Reuse detection — если приходит уже использованный (ротированный) refresh token, это признак кражи, и AS отзывает всю цепочку токенов сессии. Это критично для public-клиентов, где refresh token хуже защищён. Подводный камень в клиенте: нужно персистить именно новый refresh token, иначе после рестарта получите `invalid_grant`. **В:** Зачем нужен `nonce` в OIDC, если есть `state`? **О:** `state` защищает OAuth2-редирект от CSRF (привязка к браузерной сессии клиента). `nonce` привязывается к самому `id_token`: клиент кладёт его в `/authorize`, AS возвращает в claim `nonce` внутри `id_token`, клиент сверяет. Это защита от **replay** id_token и от подстановки чужого токена. Они решают разные задачи и используются вместе. **В:** Как безопасно хранить токены в SPA? **О:** Не в `localStorage`/`sessionStorage` — это легко вычитывается при XSS. Лучший паттерн — **BFF (Backend-for-Frontend)**: OAuth-обмен и хранение токенов на сервере, в браузере только HttpOnly+Secure+SameSite сессионная кука, а вызовы к API проксируются через бэкенд. Если BFF невозможен — access token держат в памяти JS (теряется при перезагрузке, и это норм), refresh либо ротируемый в HttpOnly cookie, либо sender-constrained (DPoP). --- ## На что копают на senior+ - **Confused deputy / token passthrough.** Понимаете ли, что access token нельзя слепо проксировать между микросервисами, и как это решается (`aud`-валидация, token exchange RFC 8693, audience-restricted токены). - **Алгоритм-конфьюжен и валидация JWT.** `alg:none`, RS256→HS256, ротация ключей JWKS по `kid`, кеширование JWKS и его инвалидация. Понимание, что верификация алгоритма не должна доверять заголовку токена. - **Отзыв JWT.** Дилемма «stateless JWT vs мгновенный отзыв»: короткий TTL, introspection (RFC 7662), revocation list, версия токена/`jti`-blacklist, событийная инвалидация. - **Sender-constrained tokens.** DPoP (RFC 9449) и mTLS-bound токены (RFC 8705) — привязка токена к ключу клиента, чтобы украденный bearer был бесполезен. - **OAuth 2.1 и BCP.** Знание, что Implicit и ROPC удалены, PKCE обязателен, exact-match redirect_uri обязателен, refresh rotation для public-клиентов (RFC 9700 / Security BCP). - **PAR и JAR.** Pushed Authorization Requests (RFC 9126) и signed request objects (RFC 9101) — защита параметров авторизационного запроса от подмены, актуально для FAPI/финтеха. - **Token exchange / delegation / impersonation** (RFC 8693) в микросервисной архитектуре: как пробросить контекст пользователя вглубь без передачи исходного токена. - **Multi-tenancy и `iss`/`aud`-изоляция.** Как не дать токену одного тенанта/клиента ходить в чужой контекст. - **Логаут и сессии в OIDC.** RP-initiated logout, back-channel logout, рассинхрон сессий между несколькими RP при едином IdP. - **Practical Go.** Поведение `oauth2.TokenSource` (кеш, потокобезопасность, отсутствие сигнала о ротации refresh), корректная персистентность токенов, кастомный `http.Client`/transport для добавления mTLS/DPoP.