Волга
Главная
Agent Skill
API Docs
GitHub
  • English
  • Русский
Главная
Agent Skill
API Docs
GitHub
  • English
  • Русский
  • Главная
  • Старт

    • Быстрый старт
    • Обработчики
    • Параметры маршрута
    • Параметры запроса
    • Группировка маршрутов
  • Запросы и ответы

    • Заголовки (Headers)
    • Работа с JSON
    • Работа с Form Data
    • Работа с файлами
    • Multipart-ответы
    • Сырое тело запроса
    • Cookies
    • Валидация запросов
  • Middleware и инфраструктура

    • Основы Middleware
    • Пользовательские Middleware
    • Параметризованные Middleware
    • Сжатие ответов
    • Распаковка запросов
    • CORS (Cross-Origin Resource Sharing)
    • Статические файлы
    • Rate Limiting
    • Файлы конфигурации
    • OpenAPI
  • Безопасность и доступ

    • Аутентификация и авторизация
    • OAuth 2.1 и OpenID Connect
    • OAuth 2.1 Клиент
    • Гранты «сервис-сервис»
    • DPoP — токены, привязанные к отправителю
  • Надежность и наблюдаемость

    • Центральный обработчик ошибок
    • Логгирование и Трассировка
    • Отмена запросов
    • Плавное завершение работы
  • Протоколы и realtime

    • Версии и методы HTTP
    • HTTPS
    • WebSockets
    • Server-Sent Events (SSE)
  • Продвинутые паттерны

    • Внедрение Зависимостей
    • Пользовательская обработка методов HEAD, OPTIONS и TRACE
  • Agent Skill

OAuth 2.1 Клиент

volga-oauth-client — это клиент OAuth 2.1 / OpenID Connect, построенный на общих типах протокола из volga-oauth-core. Он не зависит от серверного крейта volga — его можно использовать в любом Tokio-приложении (CLI, фоновом воркере или веб-приложении на Волге, реализующем флоу входа).

Он предоставляет три клиента, разделяющих транспортную политику ClientConfig и модель ошибок ClientError:

  • DiscoveryClient — загружает Authorization Server Metadata (RFC 8414), Protected Resource Metadata (RFC 9728) и конфигурацию провайдера OpenID Connect.
  • OAuthClient — флоу Authorization Code с обязательным PKCE, refresh-токенами и индикаторами ресурсов, плюс сохранение токенов. С v0.9.8 он также реализует гранты, аутентифицирующие сам клиент — см. Гранты «сервис-сервис».
  • RegistrationClient — динамическую регистрацию клиентов (RFC 7591).

С v0.9.8 полученные токены можно привязать к ключу клиента через Dpop (RFC 9449) — см. DPoP.

Зависимости

[dependencies]
volga-oauth-client = { version = "..." }

Feature-флаги

ФлагЧто включает
http1 (по-умолчанию)HTTP/1.1 через hyper
http2HTTP/2 через hyper; согласуется через TLS ALPN при совместном использовании с http1, либо используется эксклюзивно (prior knowledge поверх plaintext) без него
private-key-jwtаутентификацию клиента private_key_jwt (RFC 7523 §2.2) — клиентское утверждение, подписанное собственным ключом клиента
dpopтокены, привязанные к отправителю, по DPoP (RFC 9449)

Как минимум один из http1 / http2 должен быть включён.

Инфо

private-key-jwt и dpop выключены по-умолчанию, потому что это единственные части крейта, которым нужен бэкенд подписи JWS (jsonwebtoken поверх aws-lc-rs). Все гранты, методы аутентификации по секрету и публичные клиенты работают без них.

Discovery

DiscoveryClient разрешает well-known discovery-URL, загружает документы по HTTPS и проверяет каждый на соответствие идентификатору, для которого он был запрошен (RFC 8414 §3.3 / RFC 9728 §3.3):

use volga_oauth_client::{ClientError, DiscoveryClient};

async fn discover() -> Result<(), ClientError> {
    let client = DiscoveryClient::new();

    // напрямую по идентификатору эмитента (RFC 8414 или путь OIDC):
    let server = client.fetch_server_metadata("https://auth.example.com").await?;

    // или начиная с ресурса и следуя к его серверу авторизации:
    let resource = client.fetch_resource_metadata("https://api.example.com").await?;
    let server = client.discover_authorization_server(&resource).await?;

    assert!(server.token_endpoint.is_some());
    Ok(())
}
  • fetch_server_metadata / fetch_oidc_metadata — один и тот же вид документа по путям RFC 8414 и OIDC Discovery.
  • fetch_resource_metadata / fetch_resource_metadata_from_url — второй берёт URL resource_metadata прямо из заголовка WWW-Authenticate.
  • discover_authorization_server — берёт первый объявленный сервер авторизации и загружает его метаданные, автоматически откатываясь с пути RFC 8414 на путь OIDC.
  • fetch_jwks / fetch_jwks_from_url — набор ключей JSON Web Key Set эмитента в виде сырого JSON.

Совет

Подключите MetadataCache через with_cache(...), чтобы переиспользовать имеющееся хранилище; discovery-документы меняются редко. Загрузки JWKS намеренно обходят кэш в обе стороны — ключи подписи ротируются, поэтому политика свежести остаётся за вами.

Authorization Code + PKCE

OAuthClient реализует флоу OAuth 2.1 Authorization Code. PKCE (S256) генерируется и применяется автоматически — это защита, которую OAuth 2.1 предписывает для публичных клиентов.

use std::sync::Arc;
use volga_oauth_client::{ClientError, DiscoveryClient, InMemoryTokenStore, OAuthClient};

async fn authorize() -> Result<(), ClientError> {
    let metadata = DiscoveryClient::new()
        .fetch_server_metadata("https://auth.example.com")
        .await?;

    let client = OAuthClient::new("my-client")
        .with_redirect_uri("https://app.example.com/callback")
        .with_token_store(Arc::new(InMemoryTokenStore::new()));

    // 1. строим запрос авторизации (state и PKCE генерируются)
    let auth = client
        .authorization_request(&metadata)
        .with_scopes(["read"])
        .with_resource("https://api.example.com")
        .build()?;

    // 2. отправляем пользователя на `auth.url`. Провайдер редиректит обратно
    //    на ваш callback с настоящими query-параметрами `code` и `state` —
    //    их и читаем там. (В этом примере переиспользуется сгенерированный
    //    `auth.state`, чтобы проверка ниже проходила на «счастливом пути».)
    let (code, state) = ("the-authorization-code", auth.state.as_str());

    // всегда проверяйте state из callback до обмена — защита от CSRF
    if !auth.matches_state(state) {
        return Ok(()); // отклоняем — возможен CSRF
    }

    // 3. обмениваем код на токены (verifier PKCE идёт вместе с запросом)
    let tokens = client.exchange_code(&metadata, code, &auth).await?;
    client.store_tokens("alice", &tokens);

    // 4. позже — отдаётся из хранилища, прозрачно обновляется при устаревании:
    let tokens = client.token("alice", &metadata).await?;
    Ok(())
}

AuthorizationRequest, возвращаемый методом build(), содержит url для редиректа, state для проверки в callback и пару PKCE. Он реализует Serialize/Deserialize, так что веб-приложение может сохранить его в сессии между редиректом и callback.

Построитель запроса принимает with_scopes, with_resource (RFC 8707, можно повторять), with_state (переопределить сгенерированное значение) и with_param (например, OIDC nonce или prompt).

Примечание

Всегда проверяйте state из callback через matches_state до обмена кода — это ваша защита от CSRF.

Валидация callback

Начиная с v0.9.6, метод validate_callback проверяет callback целиком — и state, и параметр iss из RFC 9207 — и рекомендуется вместо голого matches_state:

use volga_oauth_client::{AuthorizationRequest, AuthorizationServerMetadata, ClientError};

fn check(
    request: &AuthorizationRequest,
    metadata: &AuthorizationServerMetadata,
    state: &str,
    iss: Option<&str>,
) -> Result<(), ClientError> {
    request.validate_callback(metadata, state, iss)?;
    Ok(())
}

iss — это параметр запроса iss из callback или None, если ответ его не содержал. Он обязан совпадать с издателем, когда присутствует, и становится обязательным, как только сервер объявляет authorization_response_iss_parameter_supported.

Примечание

Без проверки iss callback можно воспроизвести от другого сервера авторизации — это и есть атака подмены (mix-up), ради которой существует RFC 9207. Если провайдер объявляет этот параметр, используйте validate_callback; matches_state остаётся для проверки только state.

Прозрачное обновление

token(key, &metadata) читает сохранённые токены и незаметно обновляет устаревший access-токен. Он возвращает Ok(None), когда требуется интерактивная авторизация — ничего не сохранено, у записи нет refresh-токена или сервер отклонил refresh-токен (invalid_grant); в последних случаях «мёртвая» запись удаляется из хранилища. Можно также обновить явно через refresh.

Аутентификация клиента

Без учётных данных клиент работает как публичный (защитой служит PKCE, как и предписывает OAuth 2.1). Конфиденциальный клиент аутентифицируется на token-эндпоинте одним из трёх способов, и выбранный способ применяется ко всем грантам, которые он отправляет.

Общий секрет

use volga_oauth_client::{ClientAuthMethod, OAuthClient};

let client = OAuthClient::new("my-client")
    .with_secret("s3cret")
    // `client_secret_basic` (по-умолчанию) или `client_secret_post`
    .with_auth_method(ClientAuthMethod::Post);

private_key_jwt

Начиная с v0.9.8 (флаг private-key-jwt), клиент может аутентифицироваться утверждением, подписанным его собственным ключом (RFC 7523 §2.2) — при этом общий секрет никогда не покидает клиент:

use volga_oauth_client::{ClientError, JwsAlgorithm, OAuthClient, PrivateKeyJwt};

fn build() -> Result<OAuthClient, ClientError> {
    let key = PrivateKeyJwt::from_pem_file("/etc/secrets/client.pem", JwsAlgorithm::RS256)?
        .with_key_id("2026-08");

    Ok(OAuthClient::new("my-client").with_private_key_jwt(key))
}

PrivateKeyJwt загружает ключ (from_pem, from_pem_file, from_der) и несёт политику claims — with_key_id, with_lifetime (по-умолчанию 60 секунд) и with_audiences. На каждый запрос токена выпускается новое утверждение со случайным jti, поэтому перехваченное живёт недолго. Подключение ключа отменяет любой with_secret — учётными данными становится утверждение.

Примечание

Симметричные алгоритмы отклоняются: HMAC-секрет, который сервер и так знает, ничего не доказывает о том, кто подписал. Алгоритм также проверяется по token_endpoint_auth_signing_alg_values_supported, если сервер его объявляет.

Публикация открытого ключа

Сервер авторизации проверяет утверждения открытой половиной ключа, которую он либо забирает по jwks_uri, либо получил при регистрации. with_public_jwk прикрепляет её, а jwks() формирует документ для публикации:

use volga_oauth_client::{ClientError, JwsAlgorithm, PrivateKeyJwt, PublicJwk};

fn publish(key: PrivateKeyJwt, public: PublicJwk) -> Result<(), ClientError> {
    let key = key.with_public_jwk(public)?;

    // отдавайте это по вашему `jwks_uri` либо отправьте как поле `jwks`
    // в запросе динамической регистрации клиента
    let document = key.jwks();
    Ok(())
}

PublicJwk (RFC 7517, из volga-oauth-core) моделирует исключительно открытый материал подписи — приватные поля в нём выразить нельзя, а десериализация документа, который их несёт, завершается ошибкой, а не молчаливым отбрасыванием. Он также отклоняет сочетания, с которыми не сможет работать ни один проверяющий: RSA-ключ, объявляющий ES256, ключ P-384 с ES256, открытый ключ с HMAC-алгоритмом или кривую, не принадлежащую типу ключа. Поля kid и alg заполняются из конфигурации подписи, поэтому опубликованный документ всегда согласован с тем, что реально несут утверждения.

Инфо

Открытый ключ передаётся явно — крейт подписывает, но не выводит открытые ключи из закрытых. Именно это делает невозможной случайную публикацию ключа подписи.

Что проверяется до отправки запроса

Начиная с v0.9.8, настроенный способ аутентификации проверяется по token_endpoint_auth_methods_supported до отправки запроса токена: способ, который сервер не объявлял, принёс бы по сети лишь invalid_client. Метаданные, не перечисляющие ни одного способа, под сомнение не ставятся — именно так выглядит AuthorizationServerMetadata, собранный вручную — но обнаруженный через discovery документ всегда что-то перечисляет, поскольку по RFC 8414 отсутствие поля означает client_secret_basic. Публичный клиент не предъявляет учётных данных и не проверяется.

Инфо

Зарегистрированные идентификаторы протокола, о которых договариваются обе стороны, с v0.9.8 живут в одном месте — volga_oauth_core::protocol, в виде констант grant, client_auth, token_type и auth_scheme, реэкспортируемых и из volga::auth::oauth, и из volga_oauth_client. Сервер объявляет их в своих метаданных, а клиент по ним сопоставляет — так они не могут разойтись.

Хранилище токенов

Сохранение выполняется через трейт TokenStore. InMemoryTokenStore — встроенная реализация в памяти процесса, подходящая для CLI, тестов и одноинстансовых сервисов; всё долговременное (БД, зашифрованный файл, связка ключей ОС) реализуется одной реализацией трейта.

use volga_oauth_client::{TokenSet, TokenStore};

struct MyStore;

impl TokenStore for MyStore {
    fn get(&self, key: &str) -> Option<TokenSet> { /* ... */ None }
    fn put(&self, key: &str, tokens: &TokenSet) { /* ... */ }
    fn remove(&self, key: &str) { /* ... */ }
}

Ключ выбирает приложение — обычно идентификатор пользователя или сессии, при необходимости в сочетании с ресурсом, когда один клиент обслуживает несколько аудиторий. TokenSet содержит access-токен, опциональный refresh-токен, выданный scope, OIDC id_token (передаётся как есть, без валидации) и абсолютное время expires_at; в выводе Debug токены скрыты.

Динамическая регистрация клиентов

RegistrationClient отправляет ClientMetadata на registration-эндпоинт сервера (RFC 7591) и возвращает выданные учётные данные. OAuthClient::from_registration принимает их и создаёт готовый к использованию клиент:

use volga_oauth_client::{
    ClientError, ClientMetadata, DiscoveryClient, OAuthClient, RegistrationClient,
};

async fn register() -> Result<(), ClientError> {
    let metadata = DiscoveryClient::new()
        .fetch_server_metadata("https://auth.example.com")
        .await?;

    let registered = RegistrationClient::new()
        .register(
            &metadata,
            &ClientMetadata::new()
                .with_redirect_uris(["https://app.example.com/callback"])
                .with_client_name("My App"),
        )
        .await?;

    // готовый к использованию клиент с выданными учётными данными
    let client = OAuthClient::from_registration(&registered)?;
    Ok(())
}

Для серверов, не разрешающих открытую регистрацию, добавьте начальный токен доступа через with_initial_access_token.

Два поля ClientMetadata стали полноценными в v0.9.6 и v0.9.8 соответственно:

  • with_application_type — "web" или "native". Десктопные и CLI-клиенты регистрируются как "native" — именно это делает loopback-адреса перенаправления (http://127.0.0.1:{port}/...) приемлемыми для серверов авторизации.
  • with_token_endpoint_auth_signing_alg — алгоритм, которым именно этот клиент подписывает свои утверждения, для регистрации с private_key_jwt.

Если регистрация использует private_key_jwt, принимайте её через from_registration_with_key — он также отклонит ключ, который регистрация не приняла бы: подписывающий алгоритмом, отличным от зарегистрированного token_endpoint_auth_signing_alg, или с kid, который не разрешается встроенным jwks:

use volga_oauth_client::{ClientError, ClientRegistrationResponse, OAuthClient, PrivateKeyJwt};

fn adopt(
    registered: &ClientRegistrationResponse,
    key: PrivateKeyJwt,
) -> Result<OAuthClient, ClientError> {
    OAuthClient::from_registration_with_key(registered, key)
}

Примечание

Начиная с v0.9.8, клиент, созданный через from_registration, отклоняет грант, который не был одобрен его регистрацией — до обращения к сети, а не в виде unauthorized_client от token-эндпоинта. Отсутствующий grant_types означает только authorization_code (RFC 7591 §2), а не полную свободу; неограничен лишь клиент, который вообще не проходил регистрацию. refresh_token не отклоняется никогда, поскольку по RFC 6749 §6 это продолжение уже полученного гранта.

Инфо

Протокол управления RFC 7592 (чтение, обновление и удаление регистрации) не реализован, но пара registration_access_token / registration_client_uri из ответа доступна для приложений, которым она нужна.

Транспортная политика и ошибки

ClientConfig несёт политику, общую для всех операций клиента — принудительный HTTPS, таймауты на запрос и лимиты редиректов. Значения по-умолчанию безопасны для продакшена; чаще всего переопределяют отключение HTTPS для локального сервера разработки:

use std::time::Duration;
use volga_oauth_client::{ClientConfig, OAuthClient};

let config = ClientConfig::new()
    .require_https(false)              // только для локальной разработки
    .with_timeout(Duration::from_secs(5))
    .with_max_redirects(0);

let client = OAuthClient::new("my-client").with_config(config);

ClientError отделяет разобранный ответ об ошибке OAuth (Protocol, несущий OAuthError из RFC 6749 §5.2) от ошибок транспорта, декодирования, небезопасного URL и валидации — так вы можете отличить «сервер ответил invalid_grant» от «соединение оборвалось». С v0.9.8 к ним добавился Signing — конфигурация подписи не может создать JWS, нужный запросу: утверждение private_key_jwt или доказательство DPoP.

Проброс ошибок из обработчика

Начиная с v0.9.8, при включённом на volga флаге oauth-client, ClientError преобразуется в volga::Error, поэтому обработчик, общающийся с сервером авторизации, может пробрасывать ошибку через ?:

use volga::{HttpResult, ok};
use volga_oauth_client::{DiscoveryClient};

async fn metadata() -> HttpResult {
    let metadata = DiscoveryClient::new()
        .fetch_server_metadata("https://auth.example.com")
        .await?; // ClientError -> volga::Error

    ok!(metadata.issuer)
}

Начиная с 0.12.0 это преобразование — реализация IntoError, поэтому ClientError может быть и Err обработчика как есть и попадает в map_err, как любая другая ошибка:

use volga_oauth_client::{ClientError, DiscoveryClient};

async fn issuer() -> Result<String, ClientError> {
    let metadata = DiscoveryClient::new()
        .fetch_server_metadata("https://auth.example.com")
        .await?;

    Ok(metadata.issuer)
}

Статус описывает, где произошёл сбой, а не повторяет ответ сервера авторизации — ведь в этом вызове клиентом было ваше приложение:

СбойСтатус
до сервера не удалось достучаться (Transport)503 Service Unavailable
сервер ответил непригодно — ошибка протокола, неожиданный статус, неразбираемое тело502 Bad Gateway
собственная конфигурация приложения — небезопасный URL, метаданные, не прошедшие валидацию, ключ, которым нельзя подписать500 Internal Server Error

Чтобы показать вызывающей стороне код ошибки самого сервера авторизации, сопоставляйте ClientError::Protocol вместо того, чтобы полагаться на это преобразование.

Что дальше

  • Гранты «сервис-сервис» — client_credentials, JWT bearer и обмен токенов, для сценариев без участия пользователя.
  • DPoP — привязка токенов к ключу клиента, чтобы украденный токен ничего не стоил.

Примеры

  • OAuth Flow — полный флоу discovery → авторизация → обмен кода → вызов защищённого маршрута, реализованный через volga-oauth-client.
Последнее обновление: 26.09.2026, 12:18
Prev
OAuth 2.1 и OpenID Connect
Next
Гранты «сервис-сервис»