ASP.NET Core 8 Identity: Ошибка внешней аутентификации на разных доменах

Вопрос или проблема

Я работаю над приложением, которое должно аутентифицировать пользователей с использованием внешних поставщиков (например, Google) через мой ASP.NET Core 8 Web API, использующий Identity.

Мой код работает прекрасно, когда веб-приложение и API находятся в одном домене: Angular-приложение вызывает конечную точку challenge, пользователь перенаправляется на Google, выполняет свои действия, Google перенаправляет на URL возврата, указанный в Angular-приложении, с установленным cookie Identity.External, и наконец, Angular-приложение вызывает /external-login-callback с { withCredentials: true } и получает токен носителя.

Но мне нужно обработать случай, когда это не так, когда фронтенд и API находятся в разных доменах, например, мобильное приложение или просто веб-приложение на другом домене.

Когда это происходит на другом домене, я всегда получаю 401 Unauthorized, потому что GetExternalLoginInfoAsync всегда равен null. Действительно, cookie устанавливается только на домене API, но не на домене Angular-приложения, вызывающего API.

        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseInMemoryDatabase("AppDb"));

            builder.Services.AddIdentityApiEndpoints<IdentityUser>()
                .AddEntityFrameworkStores<ApplicationDbContext>();

            builder.Services.AddAuthentication()
                .AddGoogle(x =>
                {
                    x.SignInScheme = IdentityConstants.ExternalScheme;
                    x.ClientId = builder.Configuration["Authentication:Google:ClientId"]!;
                    x.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]!;
                });

            builder.Services.AddAuthorization();
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            builder.Services.AddCors(x =>
            {
                x.AddDefaultPolicy(y =>
                {
                    y.AllowAnyHeader()
                    .AllowAnyMethod()
                    .AllowCredentials()
                    .SetIsOriginAllowed(_ => true);
                });
            });

            var app = builder.Build();

            app.UseCors();

            app.MapGet("/external-login", (SignInManager<IdentityUser> signInManager, [FromQuery] string redirectUrl, [FromQuery] string externalProvider = "Google") =>
            {
                return Results.Challenge(
                    authenticationSchemes: [externalProvider.ToString()],
                    properties: signInManager.ConfigureExternalAuthenticationProperties(
                        externalProvider.ToString(),
                        redirectUrl
                    )
                );
            });

            app.MapGet("/external-login-callback", async (UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager) =>
            {
                signInManager.AuthenticationScheme = IdentityConstants.BearerScheme;

                var info = await signInManager.GetExternalLoginInfoAsync();

                if (info == null) // Всегда null, когда API и фронтенд находятся в разных доменах
                {
                    return Results.Unauthorized();
                }

                var externalSignInResult = await signInManager.ExternalLoginSignInAsync(
                    info.LoginProvider,
                    info.ProviderKey,
                    isPersistent: false);

                // ... логика аутентификации ...
                return Results.Empty;
            });

            app.Run();
        }
    }

У вас есть идеи, как я могу с этим справиться? Или решение обходной проблемы?

Ответ или решение

Ваша проблема связана с механизмом аутентификации в ASP.NET Core при работе с внешними поставщиками и различными доменами. Когда клиент (ваш Angular-приложение) и сервер (ваш API) находятся на разных доменах, браузер не отправляет куки (в данном случае Identity.External) из-за политики CORS и ограничений безопасности, таких как SameSite.

Вот несколько решений и обходных путей, которые помогут вам обработать аутентификацию среди разных доменов:

1. Использование SameSite=None

Убедитесь, что куки аутентификации настроены корректно, чтобы они могли быть отправлены между доменами. Вам нужно установить атрибут SameSite куки на None, чтобы обеспечить их отправку в междоменных запросах. Пример настройки вашей аутентификации:

builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.SameSite = SameSiteMode.None;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // Используйте HTTPS
});

2. Настройки CORS

Ваши настройки CORS выглядят хорошо, но стоит убедиться, что вы явно разрешаете нужные origin. Вместо вызова SetIsOriginAllowed(_ => true) можно указать конкретные источники:

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowSpecificOrigin", builder =>
    {
        builder.WithOrigins("https://your-angular-app-domain.com")
               .AllowAnyHeader()
               .AllowAnyMethod()
               .AllowCredentials();
    });
});

Затем убедитесь, что вы используете эту политику:

app.UseCors("AllowSpecificOrigin");

3. Переход на полный OAuth2

Вместо использования простого вызова на /external-login, вы можете реализовать полноценный OAuth2 процесс с помощью промежуточного сервера для аутентификации. Ваш Angular-приложение может инициировать процесс аутентификации, перенаправляя пользователя на Google и получать Authorization Code. Затем этот код отправляется на API для обмена на токен доступа.

4. Использование токена доступа

Для мобильных приложений и других клиентов, возможно, имеет смысл использовать более простой подход с получением токена доступа после аутентификации через соответствующую библиотеку OAuth2. После успешной аутентификации клиент может сохранить этот токен для дальнейшего обращения к API.

5. Обработка ошибок

Убедитесь, что ваша логика обработки ошибок правильно информирует о проблемах. Например, проверяя, когда info равно null, вы можете дополнительно логировать детали запроса, чтобы выяснить, что пошло не так.

Пример улучшенной логики

app.MapGet("/external-login-callback", async (UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager) =>
{
    signInManager.AuthenticationScheme = IdentityConstants.BearerScheme;

    var info = await signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        // Логгирование ошибки
        Console.WriteLine("External login info is null.");
        return Results.Unauthorized();
    }

    var externalSignInResult = await signInManager.ExternalLoginSignInAsync(
        info.LoginProvider,
        info.ProviderKey,
        isPersistent: false);

    if (externalSignInResult.Succeeded)
    {
        // Подготовка и возврат токена
        var user = await userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
        var token = GenerateJwtToken(user); // Имплементируйте метод генерации токена
        return Results.Ok(new { Token = token });
    }

    // Логика создания нового пользователя, если не удалось
    return Results.BadRequest();
});

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

Оцените материал
Добавить комментарий

Капча загружается...