Вопрос или проблема
Я работаю над приложением, которое должно аутентифицировать пользователей с использованием внешних поставщиков (например, 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();
});
С помощью этих шагов вы сможете управлять аутентификацией пользователей через внешние сервисы в приложениях с несколькими доменами. Надеюсь, это поможет вам в решении вашей проблемы.