Подключить «Войти через Google» несложно — SDK прячет всю механику. Но как только нужно понять, почему implicit flow объявили небезопасным, зачем мобильному приложению PKCE, как авторизовать сервис без пользователя или как валидировать чужой JWT без обращения к провайдеру на каждый запрос, — абстракция протекает, и нужно знать флоу. Эта статья разбирает OAuth2 и OIDC как протоколы: какие бывают потоки, зачем каждый и где подводные камни.
Дополняет мультипровайдерный OAuthСкоро («какой провайдер») механикой самих флоу; часть серии «Авторизация: токены, сессии, refresh».
В статье
- OAuth2 vs OIDC: две разные задачи одним протоколом
- Authorization code + PKCE: полный поток
- Почему implicit и password flow мертвы
- Client credentials: сервис-сервис без пользователя
- Device authorization flow: TV, CLI, устройства без браузера
- Валидация токена: introspection против локальной JWKS-проверки
- OIDC discovery:
.well-known/openid-configuration - Куда дальше по серии
- Источники
OAuth2 vs OIDC: две разные задачи одним протоколом
Путаница между OAuth2 и OIDC настолько частая, что стоит сразу зафиксировать словами, а не диаграммой. OAuth2 (RFC 6749) отвечает на вопрос «может ли этот клиент действовать от имени пользователя с такими-то правами» — это протокол делегирования доступа. Он ничего не говорит о том, кто именно вошёл: access_token авторизует запрос к API, но сам по себе не обязан нести читаемую личность. OIDC — надстройка над OAuth2, которая отвечает на другой вопрос: «кто перед нами». Она добавляет ровно один новый артефакт — id_token, подписанный JWT с claims об акте аутентификации (sub, iss, aud, exp, дальше — email/name и что угодно ещё), и стандартизирует пару вспомогательных вещей (/userinfo, discovery), о которых дальше в статье.
Четыре роли, вокруг которых строится любой флоу что в чистом OAuth2, что в OIDC:
- Resource owner — пользователь, которому принадлежит защищаемый ресурс (или, в client credentials, роли просто нет — см. ниже).
- Client — приложение, которому нужен доступ (мобильное приложение, SPA, серверный сервис).
- Authorization server — тот, кто аутентифицирует resource owner и выпускает токены. В стенде эту роль играет
mockidp(security/auth/login-methods/mockidp) — учебный, но протокольно честный OIDC-провайдер. - Resource server — тот, кто принимает
access_tokenи отдаёт защищённые данные (в стенде этоlogin-methodsпри обработке OAuth-callback, а в четвёртой статье серии — конкретно тот код, что дальше превращает профиль в сессию).
Разница OAuth2/OIDC видна прямо в коде mockidp: /token выдаёт id_token+access_token вместе для authorization_code и device_code grant — там есть resource owner, есть личность, которую нужно подтвердить. А для client_credentials — только access_token, без id_token вовсе:
// tokenClientCredentials — id_token НЕ выдаётся (client_credentials —
// чистый OAuth2-grant, без пользовательского контекста, вне OIDC id_token).
Это не недоработка демо, а прямое следствие модели: client_credentials — про сервис, действующий от своего собственного имени, там физически некого аутентифицировать как «пользователя», значит и id_token там взяться неоткуда. Каждый следующий раздел статьи — это один grant OAuth2, и для двух из них (authorization code, device code) он попутно устраивает и OIDC-аутентификацию через id_token.
Authorization code + PKCE: полный поток
Authorization code — основной флоу для любого клиента с браузером (веб-приложение, SPA, мобильное приложение с deep link). Идея в том, что сам токен браузеру никогда не передаётся напрямую — вместо него клиент получает короткоживущий одноразовый код, который потом обменивает на токен уже на прямом канале клиент→authorization server, минуя browser redirect.
Полный поток на живом коде mockidp (security/auth/login-methods/mockidp, тот же провайдер, что использует ст.4 серии для OAuth-входа):
- Клиент редиректит пользователя на
GET /authorize?client_id&redirect_uri&state&code_challenge&code_challenge_method=S256.mockidpтребуетcode_challengeсmethod=S256— без PKCE запрос отклоняется ещё на этом шаге:
if challenge == "" || method != "S256" {
http.Error(w, "invalid_request: code_challenge with method=S256 required", http.StatusBadRequest)
return
}
- Пользователь аутентифицируется у провайдера (в mock-варианте — заглушка, в реальном IdP — форма логина), провайдер редиректит обратно на
redirect_uriсcode+stateв query. - Клиент обменивает код на токены прямым POST-запросом —
/tokenсgrant_type=authorization_code,code,code_verifier. Именно на этом шаге впервые всплываетcode_verifier— секрет, который клиент сгенерировал ещё до шага 1 и никогда не передавал по каналу редиректа.
Вот в чём смысл PKCE (Proof Key for Code Exchange, RFC 7636): на шаге 1 клиент отправляет не сам секрет, а его производную — challenge = base64url(sha256(verifier)). На шаге 3 он предъявляет уже сам verifier, и сервер пересчитывает хеш и сверяет:
// verifyPKCE проверяет, что base64url(sha256(verifier)) (без padding)
// совпадает с challenge, полученным на /authorize (S256-метод из RFC 7636).
func verifyPKCE(verifier, challenge string) bool {
sum := sha256.Sum256([]byte(verifier))
computed := base64.RawURLEncoding.EncodeToString(sum[:])
return computed == challenge
}
Зачем это нужно, если код и так одноразовый? Проблема в том, где код может быть перехвачен. Шаг 1→2 идёт через браузер и redirect — URL с кодом виден истории браузера, логам прокси, а на мобильных платформах — ещё и другим приложениям, зарегистрировавшим тот же custom URL scheme (classic attack: вредоносное приложение перехватывает redirect с кодом раньше легитимного клиента). PKCE делает перехваченный код бесполезным: у атакующего есть code, но нет verifier — а без него mockidp откажет с invalid_grant. Именно поэтому PKCE обязателен для публичных клиентов (SPA, мобильные приложения — тех, что не могут хранить секрет), и современная практика (см. ниже RFC 9700) рекомендует его и конфиденциальным клиентам тоже, просто как защиту-по-умолчанию, а не «только если нельзя иначе».
Код в mockidp одноразовый в буквальном смысле — удаляется из состояния сразу при обмене, до проверки PKCE, независимо от исхода:
h.mu.Lock()
pending, ok := h.codes[code]
if ok {
delete(h.codes, code)
}
h.mu.Unlock()
Повторное предъявление того же кода — уже invalid_grant, даже с правильным verifier: код тратится один раз, попытка это сделать дважды сигнализирует либо о баге клиента, либо о попытке replay.
Почему implicit и password flow мертвы
Implicit flow (response_type=token) исторически существовал для JS-приложений без бэкенда: вместо кода /authorize сразу редиректил обратно с access_token прямо во фрагменте URL (#access_token=...), без промежуточного обмена. Звучит проще — а на практике это оказалось системной уязвимостью: токен во фрагменте URL попадает в историю браузера, в Referer-заголовки прокси-серверов (в старых браузерах), в логи любого промежуточного слоя, который логирует полный URL. И главное — implicit не даёт возможности для PKCE-подобной защиты в принципе: там просто нет шага обмена, к которому можно было бы что-то привязать. OAuth 2.0 Security Best Current Practice (RFC 9700) формально не рекомендует implicit ни для каких клиентов — используйте authorization code + PKCE везде, включая SPA.
Resource Owner Password Credentials (ROPC) — второй мёртвый грант: клиентское приложение само собирает логин/пароль пользователя и напрямую обменивает их на токен, минуя authorization server как отдельную сторону. Проблема на уровне модели доверия: смысл делегирования — в том, что пароль пользователя никогда не видит никто, кроме authorization server. ROPC ломает это прямым текстом, а заодно структурно несовместим с MFA, SSO и passkeys — там просто нет места, куда встроить второй фактор или биометрию, если весь флоу — это два текстовых поля и один POST.
Показательно, что mockidp эти два гранта не реализует вообще — только authorization_code (с обязательным PKCE), client_credentials и device_code. Это не пробел демо, а отражение текущей практики: у современных IdP implicit и ROPC часто просто физически отсутствуют в списке поддерживаемых grant’ов, а не «не рекомендуются, но работают» — то же самое видно в grant_types_supported discovery-документа mockidp (раздел ниже).
Client credentials: сервис-сервис без пользователя
Client credentials (RFC 6749 §4.4) — грант для случая, когда делегировать нечего: сервис обращается к другому сервису от своего собственного имени, без пользователя за штурвалом (батч-джоба, межсервисный вызов, интеграция с внешним партнёром). Клиент аутентифицируется сам собой напрямую — client_id/client_secret — и сразу получает access_token:
func (h *Handler) tokenClientCredentials(w http.ResponseWriter, r *http.Request) {
clientID, clientSecret, ok := clientCredentialsFromRequest(r)
if !ok || clientID != demoClientCredentialsID || clientSecret != demoClientCredentialsSecret {
writeTokenError(w, http.StatusUnauthorized, "invalid_client", "")
return
}
// ...
h.tokens[accessToken] = issuedProfile{
Sub: clientID, Scope: scope, ClientID: clientID,
ExpiresAt: time.Now().Add(accessTokenTTL),
}
clientCredentialsFromRequest принимает креды либо через HTTP Basic Auth, либо через form-поля client_id/client_secret — оба способа допустимы по RFC 6749, mockidp поддерживает оба с приоритетом Basic Auth. Sub в выданном профиле — сам clientID: «личность» здесь — это сервис, а не человек, что ещё раз подчёркивает разницу с OIDC-флоу выше.
Честная оговорка из README стенда: сравнение секрета в mockidp — обычное != над строками Go, не constant-time (subtle.ConstantTimeCompare). Практическая эксплуатируемость такой timing-атаки по сети мала (сетевой джиттер обычно перекрывает разницу в наносекундах), но это осознанное упрощение учебного провайдера, а не паттерн для переноса в прод: там — константное по времени сравнение или сравнение хешей.
Device authorization flow: TV, CLI, устройства без браузера
Есть класс устройств, для которых весь описанный выше поток с редиректами физически неудобен или невозможен: smart TV с пультом вместо клавиатуры, CLI-утилита без встроенного браузера, IoT-устройство без экрана вообще. Device authorization flow (RFC 8628) решает это разделением ролей на два экрана: устройство показывает короткий код, а подтверждение проходит на другом устройстве — телефоне или ноутбуке пользователя, где ввести данные удобно.
Клиентская часть в стенде — oidc.RunDeviceFlow (security/auth/oidc), серверная — mockidp (/device_authorization → /device/approve → поллинг /token). Первый шаг — запрос кода:
// deviceAuthorization — POST /device_authorization (RFC 8628 §3.1/§3.2).
deviceCode, _ := token.OpaqueHex(16) // видит только устройство, для поллинга
userCode, _ := token.Code() // видит пользователь, вводит вручную
writeJSON(w, http.StatusOK, map[string]any{
"device_code": deviceCode,
"user_code": userCode,
"verification_uri": verificationURI,
"verification_uri_complete": verificationURI + "?user_code=" + url.QueryEscape(userCode),
"expires_in": int(deviceCodeTTL.Seconds()),
"interval": deviceInterval,
})
Разделение device_code/user_code не случайно: device_code — длинный непредсказуемый токен, который остаётся только у устройства и используется для поллинга; user_code — короткий, удобный для ручного набора код, который человек видит на экране устройства и вводит на verification_uri с телефона. RunDeviceFlow дальше запускает подтверждение (в реальном приложении это точка, где user_code рисуется на экране TV) и входит в цикл поллинга:
for {
if time.Now().After(deadline) {
return "", ErrDeviceFlowTimedOut
}
time.Sleep(interval)
tok, pollErr := pollDeviceToken(base, auth.DeviceCode)
switch {
case pollErr == nil:
return tok, nil
case errors.Is(pollErr, errAuthorizationPending):
continue
case errors.Is(pollErr, errSlowDown):
interval += time.Second
continue
default:
return "", pollErr
}
}
Три протокольных сигнала, которые обязан уважать любой корректный клиент device flow: authorization_pending — пользователь ещё не подтвердил, поллить дальше с тем же интервалом; slow_down — клиент опрашивает слишком часто, увеличить интервал (в RunDeviceFlow — на секунду) и продолжить, а не считать это ошибкой; истечение expires_in — прекратить попытки (ErrDeviceFlowTimedOut), а не поллить бесконечно. Игнорирование slow_down — частая ошибка наивных реализаций device flow: провайдер вправе просто перестать отвечать успехом (или начать банить) клиента, который не снижает частоту после явного сигнала.
На серверной стороне подтверждение — это одноразовое событие: deviceApprove находит device_code по введённому user_code и помечает его approved, а tokenDeviceCode при выдаче токена сразу же удаляет запись под тем же локом, что и проверку статуса:
// approved — claim атомарно здесь же, под тем же Lock, до генерации
// токенов: второй конкурентный запрос с этим device_code уже не найдёт
// его в h.devices и получит invalid_grant.
delete(h.devices, deviceCode)
delete(h.deviceUserCodes, token.NormalizeCode(dev.userCode))
h.mu.Unlock()
Это защита от гонки: если бы проверка статуса и удаление были раздельными операциями, два конкурентных poll’а с одним и тем же (только что подтверждённым) device_code теоретически могли бы оба увидеть approved и оба получить токен. Атомарный claim под одним локом гарантирует, что второй запрос физически не найдёт запись — она уже удалена первым — и получит invalid_grant, тот же код ошибки, что и для несуществующего device_code вообще (RFC 8628 §3.5 не предписывает различать эти случаи).
Честная оговорка из README: deviceInterval в mockidp — 1 секунда, а не типичные 5 у реальных провайдеров (Google, GitHub и т.д.) — чтобы demo и e2e-прогоны не ждали лишнего. RFC 8628 не запрещает короткий интервал, но в проде значение подбирают с учётом нагрузки на /token от множества одновременно поллящих устройств — при коротком интервале и большом парке устройств /token превращается в постоянный источник трафика.
Валидация токена: introspection против локальной JWKS-проверки
Это тот самый вопрос из первой статьи серии — opaque против самодостаточного токена — только теперь на стороне OAuth2/OIDC, а не собственной сессионной модели. Токен, который клиент получил от authorization server, нужно как-то проверить на стороне resource server. Два принципиально разных пути.
Introspection (RFC 7662) — resource server отдаёт токен обратно authorization server на /introspect и спрашивает «этот токен ещё активен?». mockidp реализует это так:
// introspect требует аутентификации клиента (RFC 7662 §2.1) — те же демо-креды,
// что и client_credentials, через HTTP Basic Auth или form-поля.
// Без валидных креденшелов — 401, ДО чтения token= вовсе.
clientID, clientSecret, ok := clientCredentialsFromRequest(r)
if !ok || clientID != demoClientCredentialsID || clientSecret != demoClientCredentialsSecret {
http.Error(w, "invalid_client", http.StatusUnauthorized)
return
}
if !ok || time.Now().After(profile.ExpiresAt) {
writeJSON(w, http.StatusOK, map[string]any{"active": false})
return
}
Два момента здесь заслуживают отдельного внимания. Во-первых, introspection endpoint сам защищён — это protected resource, а не открытый lookup: без клиентской аутентификации любой сторонний клиент мог бы интроспектировать чужие токены и узнавать sub/scope/exp. Во-вторых, для несуществующего и для просроченного токена mockidp отдаёт один и тот же ответ — {"active": false}, не раскрывая, какой именно из двух случаев произошёл. Это намеренно: если бы ответ различался («не найден» / «истёк»), атакующий, перебирающий токены, получил бы дополнительный бит информации о том, существовал ли когда-либо конкретный токен вообще.
Локальная валидация по JWKS — противоположный подход: токен самодостаточен (это JWT), и resource server проверяет его сам, без похода к authorization server вовсе. oidc.ValidateIDToken (security/auth/oidc) делает это для id_token:
func ValidateIDToken(jwksURL, issuer, audience, raw string) (map[string]any, error) {
doc, err := fetchJWKS(jwksURL)
// ...
claims := jwt.MapClaims{}
_, err = jwt.ParseWithClaims(raw, claims, func(t *jwt.Token) (interface{}, error) {
// Явная проверка метода подписи ДО использования какого-либо ключа.
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, ErrUnexpectedAlg
}
kid, _ := t.Header["kid"].(string)
return doc.findRSAPublicKey(kid)
},
jwt.WithValidMethods([]string{"RS256"}),
jwt.WithIssuer(issuer),
jwt.WithAudience(audience),
jwt.WithExpirationRequired(),
)
// ...
}
JWKS (JSON Web Key Set, RFC 7517) — это просто набор публичных ключей, опубликованных authorization server’ом по стандартному адресу (jwks_uri из discovery, см. ниже): каждый ключ — kid (идентификатор), kty (тип, здесь "RSA"), n/e (модуль и экспонента RSA-ключа в base64url). Ничего специфичного для описания API или контрактов запросов — только материал для проверки подписей, публикуемый именно потому, что публичный ключ можно раздавать открыто: он ничего не компрометирует, в отличие от приватного. mockidp.jwks публикует ровно такой документ:
func (h *Handler) jwks(w http.ResponseWriter, r *http.Request) {
pub := h.signingKey.PublicKey
writeJSON(w, http.StatusOK, map[string]any{
"keys": []map[string]any{{
"kty": "RSA", "use": "sig", "alg": "RS256", "kid": h.kid,
"n": base64.RawURLEncoding.EncodeToString(pub.N.Bytes()),
"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pub.E)).Bytes()),
}},
})
}
ValidateIDToken принимает только RS256 — любой другой алгоритм в заголовке токена, включая alg=none и alg=HS256, отклоняется ещё на этапе выбора ключа, до какой-либо проверки подписи (ErrUnexpectedAlg). Это тот же класс защиты от alg confusion, что разбирала третья статья, только зеркально: там jwtclaims.Parse отклоняет всё, кроме HMAC, здесь ValidateIDToken отклоняет всё, кроме RSA. Конкретная атака, которую это закрывает: если бы проверка метода шла после выбора ключа (или если бы вызывающий код просто доверял alg из заголовка), атакующий мог бы взять настоящий id_token, переподписать его тем же payload’ом алгоритмом HS256, используя публичный RSA-модуль как HMAC-секрет (публичный ключ по определению известен всем, кто ходил на jwks_uri) — и такой поддельный токен прошёл бы проверку, если бы код наивно смотрел на alg=HS256 в заголовке и брал первый попавшийся «секрет». Тест стенда (TestValidateIDTokenAcceptsValidRejectsForged) воспроизводит эту атаку буквально — берёт claims настоящего токена, переподписывает HS256 с произвольным атакующим секретом, и проверяет, что ValidateIDToken отказывает.
После выбора метода и ключа по kid (findRSAPublicKey, ErrKeyNotFound если такого kid нет в JWKS — например, ключ уже ротировали) идёт обычная проверка claims: iss должен совпасть с ожидаемым issuer, aud — содержать ожидаемую audience (в стенде это client_id, с которым клиент инициировал флоу), exp — обязателен и не истёк (jwt.WithExpirationRequired() — без этого флага JWT без поля exp вообще прошёл бы как «бессрочный», что почти всегда не то, что имелось в виду).
Здесь важна честная оговорка о доверии каналу: JWKS раздаётся по HTTP(S), и вся конструкция «публичный ключ можно публиковать открыто» держится на том, что клиент действительно получил ключи от настоящего IdP, а не от того, кто подменил ответ по пути — это зона ответственности TLS, а не самого OIDC-протокола. fetchJWKS в стенде тянет ключи заново при каждом вызове — для demo этого достаточно, но в проде JWKS кэшируют с уважением к Cache-Control и обновляют по незнакомому kid (о ротации ключей и её отсутствии в mockidp — ниже).
Что выбрать. Introspection даёт мгновенную свежесть — отозванный на IdP токен тут же вернёт active: false, но платит сетевым round-trip и нагрузкой на IdP на каждый запрос. Локальная JWKS-валидация не ходит в сеть вовсе, но токен остаётся «валидным» для проверяющего до своего exp, даже если IdP уже считает его отозванным — тот же компромисс stateless-токенов, который разобран в ст.3 про цену JWT. Этот стенд показывает механику обеих проверок на живом коде, но не измеряет их относительную цену — сравнивать искусственно замедленный httptest.Server из multi-service/bench с introspection было бы нечестно. Реальный замер обоих режимов на настоящем IdP — отдельный стенд security/keycloak: 40 000 запросов на /me, p50 локальной JWKS-валидации — 1.18 мс при 0 обращений к Keycloak, p50 introspection — 4.19 мс при ровно 1 обращении на запрос (разбор в статье о валидации токенов Keycloak). Порядок цифр — тот же, что и в бенче ст.3 (локальная проверка на порядок дешевле похода к IdP), только здесь это не эмуляция, а измерение на реальном провайдере.
OIDC discovery: .well-known/openid-configuration
Последний кусок механики — как клиент вообще узнаёт, где у провайдера /authorize, /token, jwks_uri и что он вообще умеет, не хардкодя URL-ы для каждого IdP отдельно. OIDC Discovery 1.0 стандартизирует один адрес — /.well-known/openid-configuration, документ по которому описывает всю топологию провайдера:
func (h *Handler) discovery(w http.ResponseWriter, r *http.Request) {
base := baseURL(r)
writeJSON(w, http.StatusOK, map[string]any{
"issuer": base,
"authorization_endpoint": base + "/authorize",
"token_endpoint": base + "/token",
"userinfo_endpoint": base + "/userinfo",
"jwks_uri": base + "/.well-known/jwks.json",
"introspection_endpoint": base + "/introspect",
"device_authorization_endpoint": base + "/device_authorization",
"grant_types_supported": []string{
"authorization_code", "client_credentials",
"urn:ietf:params:oauth:grant-type:device_code",
},
"code_challenge_methods_supported": []string{"S256"},
"id_token_signing_alg_values_supported": []string{"RS256"},
})
}
Практический смысл: клиентская библиотека делает один GET на /.well-known/openid-configuration, получает jwks_uri/token_endpoint/authorization_endpoint и дальше работает с ними, не зная заранее ничего специфичного про конкретного провайдера. grant_types_supported и code_challenge_methods_supported в ответе mockidp буквально перечисляют то, что разобрано выше — этим документом провайдер сам заявляет, что implicit и ROPC он не поддерживает, а PKCE (S256) — обязателен. id_token_signing_alg_values_supported: ["RS256"] — важный, но обманчивый по надёжности пункт: он говорит клиенту, чего ожидать, но не заменяет собой проверку. ValidateIDToken не читает этот список из discovery и не доверяет ему вслепую — она жёстко проверяет фактический алгоритм в заголовке каждого конкретного токена (jwt.WithValidMethods([]string{"RS256"})), потому что discovery-документ описывает намерение провайдера, а не гарантию для каждого отдельного байта, который однажды придёт по сети. Один заявляет, другой проверяет — и заявление без проверки было бы ровно той же дырой, что и alg confusion, только на уровень выше.
Честная оговорка из README стенда: mockidp — учебный OIDC-провайдер, «не полноценный OIDC» — discovery упрощён (нет полного набора обязательных для спеки полей вроде response_modes_supported, subject_types_supported и т.д.), не проверяются все обязательные claims, а главное — нет ротации ключей: RSA-ключ подписи генерируется один раз при старте процесса (rsa.GenerateKey в New()) и живёт, пока жив процесс. При рестарте login-methods ключ меняется целиком, и все ранее выданные id_token вместе с их kid в JWKS мгновенно становятся невалидными — для учебного провайдера это ожидаемо (демо не обязано переживать рестарт с валидными старыми токенами), но в проде управляемая ротация ключей держит старый и новый kid активными параллельно на переходный период, чтобы токены, подписанные до ротации, не отваливались разом.
Куда дальше по серии
Эта статья — первое из трёх углублений поверх ядра серии, разбирающее OAuth2/OIDC не как «подключить провайдера», а как протокол:
- Ст.1 — модели сессий: opaque vs JWT, система координат, на которую опирается весь разговор про introspection vs JWKS выше.
- Ст.3 — сложный путь: HS256 vs RS256, цена stateless-валидации — эта статья продолжает именно ту врезку на живом RS256/JWKS-коде.
- Ст.4 — вход и регистрация: тот же
mockidp, но с фокусом на то, как OAuth-профиль превращается в сессию сайта.
Дальше по углублениям:
- Ст.7 — MFA и TOTP: второй фактор поверх любого из разобранных здесь способов входа.
- Ст.8 — Passkeys и WebAuthn: passwordless-альтернатива всей схеме authorization code + пароль.
Если нужен реальный, не эмулированный замер введения introspection против JWKS-валидации на настоящем IdP — это серия про Keycloak, в частности интеграция бэкенда с валидацией токенов.
Источники
- RFC 6749 — The OAuth 2.0 Authorization Framework
- RFC 7636 — Proof Key for Code Exchange (PKCE)
- RFC 8628 — OAuth 2.0 Device Authorization Grant
- RFC 7517 — JSON Web Key (JWK)
- RFC 7662 — OAuth 2.0 Token Introspection
- RFC 9700 — Best Current Practice for OAuth 2.0 Security
- OpenID Connect Core 1.0
- OpenID Connect Discovery 1.0
- Живой стенд статьи:
security/auth/oidc(клиент: JWKS-валидация, device flow) +security/auth/login-methods/mockidp(провайдер)
Комментарии