Свой OIDC-провайдер: OpenIddict, Duende IdentityServer и когда Keycloak

↑ 3.4 Аутентификация и авторизация · ← Предыдущая

Обязательно~11 мин чтенияУровень: senior

Зачем это на собесе

«Нужно выдавать токены нашим приложениям, писать ли свой сервер авторизации?» Правильный первый ответ: «Использовать готовый провайдер (Keycloak, Entra ID, Auth0)». Но знать, что такое OpenIddict и IdentityServer, и когда свой провайдер оправдан, ждут от senior-разработчика .NET.

Подтемы

  • Роли в OAuth 2.0 и OpenID Connect
  • Потоки: code + PKCE, client credentials, refresh
  • OpenIddict
  • Duende IdentityServer
  • Keycloak и облачные провайдеры
  • Когда писать свой

Объяснение

Роли

  • Authorization server (провайдер): аутентифицирует пользователя и выдаёт токены.
  • Client: приложение, которому нужен доступ (SPA, мобильное приложение, сервис).
  • Resource server: API, которое проверяет токены.
  • OpenID Connect (OIDC) добавляет к OAuth 2.0 идентификацию пользователя: id_token, endpoint userinfo, стандартные claims. Основы: OAuth 2.0, OIDC и Keycloak.

Какие потоки использовать

ПотокДля чегоЗаметки
Authorization Code + PKCEпользователь входит через SPA, мобильное или серверное приложениерекомендуемый для интерактивного входа
Client Credentialsсервис обращается к API от своего именинет пользователя
Refresh Tokenобновление access-токена без повторного входахраните безопасно, ротируйте
Implicit, Resource Owner Passwordустаревшиене используйте
sequenceDiagram
    participant U as Пользователь
    participant SPA as Приложение (SPA)
    participant AS as Провайдер (OIDC)
    participant API as API
    SPA->>SPA: создаёт code_verifier и code_challenge (PKCE)
    SPA->>AS: /authorize (code_challenge, scope)
    AS->>U: форма входа
    U->>AS: логин и пароль
    AS->>SPA: redirect с code
    SPA->>AS: /token (code + code_verifier)
    AS->>SPA: access_token, id_token, refresh_token
    SPA->>API: запрос с Bearer access_token
    API->>API: проверка подписи и claims токена

PKCE защищает код авторизации от перехвата: клиент доказывает, что именно он начал поток (через code_verifier), поэтому секрет клиента в SPA не нужен.

OpenIddict

Фреймворк для создания OIDC-сервера на ASP.NET Core (лицензия Apache 2.0). Реализует протоколы OAuth 2.0 и OIDC, токены, хранилища приложений и токенов (например, EF Core). Интерфейса входа у него нет: страницы логина, согласия, регистрации вы пишете сами (часто на ASP.NET Core Identity). Подходит, когда провайдер должен быть частью вашего приложения и тесно интегрирован с вашей моделью пользователей.

Duende IdentityServer

Коммерческий продукт (наследник IdentityServer4), тоже фреймворк для OIDC на ASP.NET Core: много готовых возможностей, поддержка. Лицензия платная для организаций выше определённого порога, для разработки и небольших проектов условия отличаются, проверяйте актуальную лицензию.

Keycloak и облачные провайдеры

ВариантОсобенности
Keycloakготовый сервер с интерфейсом, пользователи, роли, SSO, федерация, админ-консоль (Keycloak в инфраструктуре)
Microsoft Entra ID, Auth0, Oktaуправляемые сервисы, не нужно поддерживать
Authentik, Ory (Hydra, Kratos)альтернативы с открытым кодом
OpenIddict, Duendeбиблиотеки для встраивания в .NET-приложение

Когда писать свой сервер

flowchart TD
    A["Нужна аутентификация и выдача токенов"] --> B{"Подходит готовый провайдер<br/>(Keycloak, Entra ID, Auth0)?"}
    B -- да --> C["Используйте готовый: меньше кода и рисков"]
    B -- нет --> D{"Причина?"}
    D -- "особая логика входа,<br/>тесная связь с продуктом" --> E["OpenIddict"]
    D -- "нужна коммерческая поддержка<br/>и готовый набор функций" --> F["Duende IdentityServer"]

Аутентификация это зона высоких рисков: ошибки в своём сервере приводят к уязвимостям. Свой провайдер оправдан только при реальной необходимости и с командой, понимающей протоколы.

Примеры

Сервер: client credentials на OpenIddict

using System.Security.Claims;
using Microsoft.AspNetCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
 
var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddDbContext<AuthDb>(o =>
{
    o.UseNpgsql(builder.Configuration.GetConnectionString("Auth"));
    o.UseOpenIddict();                                 // таблицы приложений, токенов, авторизаций
});
 
builder.Services.AddOpenIddict()
    .AddCore(o => o.UseEntityFrameworkCore().UseDbContext<AuthDb>())
    .AddServer(o =>
    {
        o.SetTokenEndpointUris("/connect/token");
        o.AllowClientCredentialsFlow();                // машина-машина
        o.AddDevelopmentEncryptionCertificate().AddDevelopmentSigningCertificate();   // только для разработки
        o.UseAspNetCore().EnableTokenEndpointPassthrough();
    })
    .AddValidation(o =>
    {
        o.UseLocalServer();
        o.UseAspNetCore();
    });
 
builder.Services.AddAuthentication(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
builder.Services.AddAuthorization();
 
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
 
app.MapPost("/connect/token", (HttpContext ctx) =>
{
    var request = ctx.GetOpenIddictServerRequest() ?? throw new InvalidOperationException();
    if (!request.IsClientCredentialsGrantType()) return Results.BadRequest();
 
    var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, Claims.Name, Claims.Role);
    identity.SetClaim(Claims.Subject, request.ClientId);
    identity.SetScopes(request.GetScopes());
    return Results.SignIn(new ClaimsPrincipal(identity), properties: null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
});
 
app.MapGet("/api/data", () => "secret").RequireAuthorization();
app.Run();
 
public class AuthDb(DbContextOptions<AuthDb> o) : DbContext(o);

Пакеты OpenIddict.AspNetCore и OpenIddict.EntityFrameworkCore. Клиента (приложение) нужно зарегистрировать в хранилище (IOpenIddictApplicationManager) с идентификатором, секретом и разрешёнными потоками и scope. Сертификаты для подписи и шифрования в продакшене должны быть настоящими, а не «development».

Получение токена клиентом

curl -X POST https://auth.example.com/connect/token \
  -d grant_type=client_credentials \
  -d client_id=billing -d client_secret=*** -d scope=api

Нюансы и подводные камни

  • Не пишите криптографию и протокол сами. Используйте проверенную библиотеку и следуйте рекомендациям (PKCE, короткие access-токены).
  • Ключи подписи. Хранить, ротировать и публиковать через JWKS; «development»-сертификаты в продакшене недопустимы.
  • Срок жизни токенов. Короткий access-токен (минуты), refresh-токен с ротацией и отзывом.
  • Redirect URI. Регистрируйте точные адреса, без подстановочных знаков, иначе возможна кража кода.
  • Безопасное хранение токенов в SPA. Избегайте localStorage для долгоживущих токенов, рассмотрите BFF-схему с cookie.
  • Проверка в API. Проверяйте подпись, издателя (iss), аудиторию (aud), срок и scope, см. JWT Bearer.
  • Лицензия Duende и условия поддержки меняются, проверяйте актуальные.

Вопросы с ответами

Связанные темы