Auth

OAuth 2.0 та зовнішні провайдери

Підключення «Увійти через Google/GitHub» в Minimal API: OAuth 2.0 Authorization Code Flow, OpenID Connect, Claims Transformation, зв'язка з локальною БД.

OAuth 2.0 та зовнішні провайдери

Кнопка «Увійти через Google» — це не магія. Це чітко визначений протокол OAuth 2.0, який ASP.NET Core підтримує «з коробки». У цій статті ми розберемо, що відбувається під капотом, і підключимо Google та GitHub як зовнішні провайдери аутентифікації.

1. Навіщо зовнішні провайдери?

Для користувачів

Не потрібно запам'ятовувати ще один пароль. Один клік — і вони в системі. Конверсія реєстрації зростає на 20-40%.

Для розробників

Не потрібно зберігати паролі! Google/GitHub відповідають за безпеку облікових записів, 2FA, захист від brute-force.

Для бізнесу

Менше покинутих форм реєстрації, достовірні email-адреси (підтверджені Google), менше витрат на підтримку «забув пароль».

2. OAuth 2.0 — як це працює?

Ключові поняття

ТермінХто цеАналогія
Resource OwnerКористувач (Іван)Господар квартири
ClientВаш додаток (MyApi)Друг, який просить ключ
Authorization ServerGoogle/GitHubКонсьєрж будинку
Resource ServerGoogle API (profile, email)Квартира з даними

Authorization Code Flow

Це найбезпечніший flow для серверних додатків. Ось повна послідовність:

Loading diagram...
sequenceDiagram
    participant U as 👤 Користувач
    participant A as 🌐 Ваш додаток
    participant G as 🔐 Google

    U->>A: 1. Натискає "Увійти через Google"
    A->>G: 2. Redirect на Google<br/>client_id, redirect_uri, scope

    Note over U,G: Користувач бачить<br/>екран Google "Дозволити?"

    U->>G: 3. Натискає "Дозволити"
    G->>A: 4. Redirect назад з authorization code<br/>GET /callback?code=abc123

    A->>G: 5. Server-to-server: обмін code на tokens<br/>POST: code + client_secret
    G-->>A: 6. access_token + id_token

    A->>G: 7. Запит даних: GET /userinfo<br/>Authorization: Bearer access_token
    G-->>A: 8. {name, email, picture}

    A-->>U: 9. Створює сесію/JWT<br/>Користувач залогінений! ✅

Користувач натискає «Увійти через Google»

Ваш додаток перенаправляє браузер на Google з параметрами:

https://accounts.google.com/o/oauth2/v2/auth?
  client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/signin-google
  &response_type=code
  &scope=openid email profile

Google показує екран дозволу

Користувач бачить: «Додаток MyApi хоче отримати доступ до вашого email та профілю. Дозволити?»

Після дозволу — redirect назад

Google перенаправляє браузер на ваш redirect_uri з authorization code:

https://yourapp.com/signin-google?code=4/0AXxyz...

Ваш сервер обмінює code на токени

Це серверний запит (невидимий для користувача). client_secret ніколи не потрапляє в браузер:

POST https://oauth2.googleapis.com/token
  code=4/0AXxyz...
  &client_id=YOUR_CLIENT_ID
  &client_secret=YOUR_SECRET
  &redirect_uri=https://yourapp.com/signin-google
  &grant_type=authorization_code

Google повертає access_token

З цим токеном ви можете запитати дані користувача (email, ім'я, аватар).

OAuth 2.0 vs OpenID Connect

OAuth 2.0OpenID Connect (OIDC)
МетаАвторизація — дозвіл на діїАутентифікація — хто користувач
Що повертаєaccess_token (доступ до ресурсів)id_token (інформація про користувача)
Юзкейс«Дозволити MyApi читати ваші файли на Google Drive»«Увійти через Google»

OpenID Connect = OAuth 2.0 + Identity Layer. Коли ми кажемо «Увійти через Google» — ми використовуємо OIDC, який побудований поверх OAuth 2.0.


3. Підключення Google Auth

Крок 1: Створення OAuth Client у Google

Перейдіть до Google Cloud Console

Відкрийте console.cloud.google.com → APIs & Services → Credentials

Створіть OAuth 2.0 Client ID

  • Type: Web application
  • Authorized redirect URI: https://localhost:5001/signin-google

Збережіть Client ID та Client Secret

Ці дані потрібні для налаштування в ASP.NET Core.

Крок 2: Налаштування в ASP.NET Core

# Зберігаємо секрети безпечно
dotnet user-secrets set "Google:ClientId" "YOUR_CLIENT_ID"
dotnet user-secrets set "Google:ClientSecret" "YOUR_SECRET"
Program.cs — Google Auth
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlite("Data Source=app.db"));

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "Google";
    })
    .AddCookie("Cookies")
    .AddGoogle("Google", options =>
    {
        options.ClientId = builder.Configuration[
            "Google:ClientId"]!;
        options.ClientSecret = builder.Configuration[
            "Google:ClientSecret"]!;

        // Додаткові scopes (за замовчуванням:
        // openid, profile, email)
        options.Scope.Add("email");
        options.Scope.Add("profile");

        // Mapping claims
        options.ClaimActions.MapJsonKey(
            "picture", "picture");
    });

builder.Services.AddAuthorization();

Крок 3: Ендпоінти

Auth ендпоінти для OAuth
// Ініціювати логін через Google
app.MapGet("/auth/google-login", () =>
    Results.Challenge(
        new AuthenticationProperties
        {
            RedirectUri = "/auth/google-callback"
        },
        ["Google"]))
    .AllowAnonymous();

// Callback — після Google redirect
app.MapGet("/auth/google-callback",
    async (HttpContext ctx) =>
{
    // Claims від Google вже заповнені
    // через Cookie middleware
    var user = ctx.User;

    var email = user.FindFirst(
        ClaimTypes.Email)?.Value;
    var name = user.FindFirst(
        ClaimTypes.Name)?.Value;
    var picture = user.FindFirst("picture")?.Value;
    var googleId = user.FindFirst(
        ClaimTypes.NameIdentifier)?.Value;

    return Results.Ok(new
    {
        message = "Logged in via Google!",
        email,
        name,
        picture,
        googleId
    });
}).RequireAuthorization();

4. Підключення GitHub Auth

Створення OAuth App у GitHub

Перейдіть до GitHub Settings

Settings → Developer settings → OAuth Apps → New OAuth App

Заповніть форму

  • Application name: MyApi
  • Homepage URL: https://localhost:5001
  • Authorization callback URL: https://localhost:5001/signin-github

Збережіть Client ID та Secret

dotnet user-secrets set "GitHub:ClientId" "YOUR_CLIENT_ID"
dotnet user-secrets set "GitHub:ClientSecret" "YOUR_SECRET"

Налаштування в коді

dotnet add package AspNet.Security.OAuth.GitHub
Додаємо GitHub поряд з Google
builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
    })
    .AddCookie("Cookies")
    .AddGoogle("Google", options =>
    {
        // ... конфігурація Google
    })
    .AddGitHub("GitHub", options =>
    {
        options.ClientId = builder.Configuration[
            "GitHub:ClientId"]!;
        options.ClientSecret = builder.Configuration[
            "GitHub:ClientSecret"]!;
        options.Scope.Add("user:email");
    });
Login через GitHub
app.MapGet("/auth/github-login", () =>
    Results.Challenge(
        new AuthenticationProperties
        {
            RedirectUri = "/auth/callback"
        },
        ["GitHub"]))
    .AllowAnonymous();

5. Зв'язка з локальною БД

Проблема

Google/GitHub повертають email та ім'я, але ваш додаток має власну базу користувачів з ролями, підписками, налаштуваннями. Потрібно зв'язати зовнішній акаунт з локальним.

Стратегія: Find or Create

Зв'язка зовнішнього акаунту з локальним
app.MapGet("/auth/callback",
    async (HttpContext ctx,
           UserManager<AppUser> userManager,
           SignInManager<AppUser> signInManager) =>
{
    // 1. Отримуємо дані від зовнішнього провайдера
    var info = await signInManager
        .GetExternalLoginInfoAsync();

    if (info is null)
        return Results.Json(
            new { error = "External login failed" },
            statusCode: 401);

    var email = info.Principal.FindFirst(
        ClaimTypes.Email)?.Value;
    var name = info.Principal.FindFirst(
        ClaimTypes.Name)?.Value;

    // 2. Шукаємо існуючого користувача
    var user = await userManager
        .FindByEmailAsync(email!);

    if (user is null)
    {
        // 3a. Створюємо нового користувача
        user = new AppUser
        {
            UserName = email,
            Email = email,
            FullName = name ?? "Unknown",
            EmailConfirmed = true // Google вже підтвердив
        };

        var createResult = await userManager
            .CreateAsync(user);

        if (!createResult.Succeeded)
            return Results.Json(
                new { error = "User creation failed" },
                statusCode: 500);

        await userManager
            .AddToRoleAsync(user, "User");
    }

    // 4. Зв'язуємо зовнішній логін з користувачем
    //    (якщо ще не зв'язаний)
    var logins = await userManager
        .GetLoginsAsync(user);

    if (!logins.Any(l =>
            l.LoginProvider == info.LoginProvider))
    {
        await userManager
            .AddLoginAsync(user, info);
    }

    // 5. Генеруємо JWT (або cookie)
    var roles = await userManager
        .GetRolesAsync(user);

    var token = tokenService.GenerateAccessToken(
        user.Id, user.FullName,
        user.Email!, roles);

    return Results.Ok(new
    {
        access_token = token,
        is_new_user = logins.Count == 0
    });
}).AllowAnonymous();

Ця стратегія:

  • Якщо email існує → логінить існуючого користувача
  • Якщо email новий → створює нового користувача
  • Зберігає зв'язок у таблиці AspNetUserLogins (LoginProvider + ProviderKey)

6. Claims Transformation

Іноді claims від зовнішнього провайдера не відповідають вашим потребам. Можна їх трансформувати:

Claims Transformation
builder.Services
    .AddTransient<IClaimsTransformation,
        AppClaimsTransformation>();

public class AppClaimsTransformation
    : IClaimsTransformation
{
    private readonly UserManager<AppUser> _userManager;

    public AppClaimsTransformation(
        UserManager<AppUser> userManager)
    {
        _userManager = userManager;
    }

    public async Task<ClaimsPrincipal> TransformAsync(
        ClaimsPrincipal principal)
    {
        var email = principal.FindFirst(
            ClaimTypes.Email)?.Value;

        if (email is null)
            return principal;

        var user = await _userManager
            .FindByEmailAsync(email);

        if (user is null)
            return principal;

        // Додаємо кастомні claims з нашої БД
        var identity = principal.Identity
            as ClaimsIdentity;

        var roles = await _userManager
            .GetRolesAsync(user);

        foreach (var role in roles)
        {
            identity?.AddClaim(
                new Claim(ClaimTypes.Role, role));
        }

        identity?.AddClaim(
            new Claim("user_id", user.Id));
        identity?.AddClaim(
            new Claim("full_name", user.FullName));

        return principal;
    }
}

Тепер Claims від Google/GitHub автоматично доповнюються ролями та іншими даними з вашої БД.


7. Практичні завдання

Рівень 1: Базовий

Рівень 2: Проєктування

Рівень 3: Архітектура


8. Резюме

OAuth 2.0 = Authorization Code Flow

Redirect → Google → Code → Exchange for Token → Userinfo. Client Secret ніколи не потрапляє в браузер.

OpenID Connect = Auth + Identity

OAuth 2.0 дає access_token (для ресурсів). OIDC додає id_token (хто користувач). ASP.NET Core використовує OIDC.

Find or Create

Зв'язуйте зовнішній акаунт з локальним через email. Нові — створюйте, існуючі — логіньте. AspNetUserLogins зберігає зв'язок.

Claims Transformation

IClaimsTransformation додає ролі та кастомні Claims з вашої БД до Claims від зовнішнього провайдера.

Далі: у фінальній статті модуля ми зібрімо все разом і розглянемо безпеку на практиці — CORS, HTTPS, CSRF, brute-force захист, заголовки безпеки та production чеклист.

Copyright © 2026