SSO — это механизм, позволяющий пользователю пройти аутентификацию один раз и получить доступ к нескольким приложениям без повторного ввода учетных данных. Вы наверняка сталкивались с ним, когда логинились через Google или Facebook на различных сайтах. Под капотом там происходит настоящая магия токенов, куки и доверительных отношений между системами.
Основные концепции SSO
Традиционная модель аутентификации и SSO отличаются как небо и земля. В классической схеме каждое приложение — сам себе хозяин: свои пользователи, свои пароли, своя безопасность. В мире SSO все иначе — центральный IdP (Identity Provider) берёт на себя ответственность за подтверждение личности, а сервисы спокойно доверяют его решениям. Вот пять ключевых отличий:
1. Точка входа: в традиционном подходе — множество разрозненных форм, в SSO — единая авторитетная точка аутентификации.
2. Состояние сессии: обычные приложения отслеживают сессию локально, SSO поддерживает глобальное состояние авторизации.
3. Управление учетными данными: вместо разбросаных по разным базам паролей — централизованное хранилище.
4. Безопасность: в SSO снижаеться вероятность компроментации учетных данных, так как пользователь вводит их гораздо реже.
5. Пользовательский опыт: бесшовний переход между приложениями вместо мучительного перелогинивания.
Варианты протоколов аутентификации
В мире SSO царит настоящий зоопарк протоколов, каждый из которых имеет свои особенности. Выбор между ними не всегда очевиден, особенно для новичков.
OAuth 2.0 — это не столько протокол аутентификации, сколько авторизации. Он позволяет третьей стороне получить ограниченый доступ к ресурсам без раскрытия учетных данных пользователя. Представьте, что вы даете парковщику ключ, которым можно только завести машину и проехать пару метров — но не открыть бардачок. Именно так работает OAuth.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "OAuth";
})
.AddCookie()
.AddOAuth("OAuth", options =>
{
options.ClientId = Configuration["OAuth:ClientId"];
options.ClientSecret = Configuration["OAuth:ClientSecret"];
options.CallbackPath = new PathString("/signin-oauth");
// Дополнительные настройки
});
} |
|
OIDC (OpenID Connect) — надстройка над OAuth 2.0, добавляющая слой идентификации. Если OAuth отвечает на вопрос "что пользователь может делать?", то OIDC добавляет "а кто, собственно, этот пользователь?". Это происходит благодаря ID токену — JWT-токену, содержащему информацию о пользователе.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://your-identity-provider.com";
options.ClientId = "your-client-id";
options.ClientSecret = "your-client-secret";
options.ResponseType = "code";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
}); |
|
SAML (Security Assertion Markup Language) — ветеран среди протоколов SSO, особенно популярен в корпоративной среде. Работает на основе обмена XML-документами между сервис-провайдером и IdP. Несмотря на некоторую громоздкость, это очень надежный и проверенный временем протокол.
Исследование, проведенное компанией Okta в 2021 году, показало, что 67% предприятий используют SAML для внутренней авторизации, тогда как в публичных сервисах преобладает OIDC с показателем около 78%. Этот разрыв постепено снижаеться — корпоративный мир тоже движеться в сторону более современных решений.
Преимущества и недостатки SSO для .NET приложений
Внедрение SSO в C#-приложения имеет как очевидные плюсы, так и неочевидные подводные камни. Среди преимуществ:
Упрощение кодбазы: вместо сложной логики аутентификации — чистый код и стандартный middleware.
Повышение безопасности: централизованное управление политиками паролей, MFA и другими аспектами защиты.
Масштабирование: добавление новых сервисов не требует реализации отдельной системы аутентификации.
Гибкость: возможность легко менять провайдера идентификации без переписывания приложения.
Однако есть и минусы:
Единая точка отказа: если IdP недоступен, все системы могут остаться без аутентификации.
Сложность начальной настройки: особенно для неопытных разработчиков.
Возможные проблемы производительности: при высоких нагрузках центральный IdP может стать узким местом.
"Помню случай, когда SSO-провайдер 'лег' на 20 минут — и вся компания оказалась парализована," — делится опытом руководитель разработки Алексей К. "После этого мы внедрили резервный механизм аутентификации для критических систем."
Современые тенденции в аутентификации
Мир не стоит на месте, и подходы к аутентификации эволюционируют. Еще недавно куки были основным способом хранения сессии, но сегодня JWT-токены (JSON Web Tokens) завоевывают все больше популярности.
JWT — компактный, самодостаточный способ передачи информации между сторонами в формате JSON. Что делает его особенным? Он может быть подписан (с помощью секрета или пары публичный/приватный ключ), а значит — проверен на подлинность. Кроме того, JWT может содержать полезную нагрузку (например, информацию о пользователе), что уменьшает необходимость обращаться к базе данных.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| public class JwtTokenGenerator
{
private readonly string _secretKey;
private readonly string _issuer;
private readonly string _audience;
public JwtTokenGenerator(string secretKey, string issuer, string audience)
{
_secretKey = secretKey;
_issuer = issuer;
_audience = audience;
}
public string GenerateToken(string userId, IEnumerable<string> roles, TimeSpan expiry)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, userId),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
foreach (var role in roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _issuer,
audience: _audience,
claims: claims,
expires: DateTime.Now.Add(expiry),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
} |
|
Этот подход имеет очевидные преимущества в микросервисной архитектуре, где stateless-взаимодействие между сервисами — золотой стандарт. Однако, JWT не лишен недостатков: отозвать такой токен до истечения срока действия не так-то просто (обычно для этого требуется вести специальный "черный список" на сервере). Другая интересная тенденция — пасскейные аутентификации (passkey authentication), которые постепено вытесняют пароли. Технологии вроде WebAuthn позволяют использовать биометрию или аппаратные ключи для безопасного подтверждения личности. И хотя прямо сейчас C# не имеет нативной поддержки WebAuthn, существуют бибилиотеки вроде FIDO2Net, облегчающие интеграцию.
Понимание основных концепций SSO — фундамент для успешной реализации этой технологии в .NET-приложениях. Теперь, когда мы разобрались с теорией, пришло время засучить рукава и настроить нашу среду разработки!
Определить значение z=sing(x)+sign(y), где sign(a)= "Система" -1 при a<0, 0 при a=0, 1 при a>0 Значения x и y вводятся с клавиатуры. Задачу решить двумя способами:
1) не используя функцию... Найти значение z=sign(a)+sign(b) Найдите значение z=sign(a)+sign(b), где Автовход (Single Sign-On) в приложение под учетной записью пользователя компьютра Пишу приложение работающее с базой данных. Приложение работает с SQL базой под одной учетной... Формат single IBM float point преобразование в Csharp Single(float) формат Возникла проблема с чтением данных в формате IBM float point. Пытался найти какой-либо простой...
Подготовка среды разработки
Подготовка почвы для SSO в C# — это как сборка хорошего фундамента для дома: займет время, потребует внимания к деталям, но зато потом спасет от множества проблем. Давайте разберемся, что нам понадобится для создания надежного SSO-решения.
Необходимые библиотеки и инструменты
В мире C# существует целая россыпь библиотек для работы с SSO. Начнем с основного набора, который должен быть в арсенале каждого разработчика:
Microsoft.AspNetCore.Authentication — базовый пакет для всех видов аутентификации в ASP.NET Core. Обязательный минимум.
Bash | 1
| dotnet add package Microsoft.AspNetCore.Authentication |
|
Microsoft.AspNetCore.Authentication.Open IdConnect — для работы с OIDC-совместимыми провайдерами. Если планируете интегрироваться с Azure AD, Auth0, Okta и другими современными IdP — без него никуда.
Bash | 1
| dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect |
|
System.IdentityModel.Tokens.Jwt — библиотека для работы с JWT-токенами. Поможет генерировать, валидировать и разбирать токены.
Sustainsys.Saml2 или ITfoxtec.Identity.Saml2 — если вам нужна поддержка SAML 2.0, эти пакеты спасут вас от необходимости реализовывать parsing XML самостоятельно.
Помимо непосредственно библиотек, жизнь сильно облегчат и некоторые инструменты:
Postman или Insomnia — для тестирования API конечных точек авторизации,
JWT.io — онлайн-дебаггер JWT токенов,
SAML-tracer — расширение для браузера, помогающее отслеживать SAML-сообщения.
Обзор различных Identity Provider для C# приложений
Выбор провайдера идентификации — критически важный шаг. Это как выбор банка: перейти потом на другой можно, но хлопотно. Давайте разберем популярные варианты:
Auth0 — если вы цените простоту интеграции и хорошую документацию, Auth0 будет отличным выбором. Обладает удобной админ-панелью, множеством готовых шаблонов и SDKs практически для всех платформ. SDK для .NET очень качественный и прост в использовании.
C# | 1
2
3
4
5
6
7
8
9
| services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = $"https://{Configuration["Auth0:Domain"]}";
options.Audience = Configuration["Auth0:Audience"];
}); |
|
Okta — еще один популярный IdP, особенно силен в корпоративном сегменте. Имеет глубокую интеграцию с Active Directory и другими корпоративными сервисами. SDK для .NET также хорошо документирован.
IdentityServer — опенсорсный провакдер идентичности для .NET. Если вы хотите полный контроль над процессом и готовы к более сложной настройке — это ваш выбор. Плюсы: бесплатный, открытый, максимально гибкий. Минусы: требует больше времени на настройку и поддержку.
C# | 1
2
3
4
5
6
| services.AddIdentityServer()
.AddDeveloperSigningCredential() // Только для разработки!
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddTestUsers(Config.GetUsers()); |
|
Azure Active Directory (Azure AD) — естественный выбор если вы уже в экосистеме Microsoft. Отлично интегрируется с другими сервисами Azure и Microsoft 365.
"Мы долго колебались между Auth0 и собственным IdentityServer," — делится опытом ведущий архитектор Анна П. "В итоге выбрали Auth0 для публичных API и приложений, а для внутренних систем развернули IdentityServer — это дало нам гибкость и контроль там, где это было важно, и простоту интеграции для публичных сервисов."
Настройка Identity Provider
Настройка IdP — это тот случай, когда дьявол кроется в деталях. Я приведу базовый процесс на примере Auth0, потому что он наглядный и достаточно универсальный.
1. Регистрация приложения: В консоли Auth0 создайте новое приложение, выберите тип (Regular Web Application для обычного веб-приложения, Single Page Application для SPA, Native для мобильных или десктопных приложений).
2. Настройка URL-адресов: Укажите допустимые Callback URLs — адреса, на которые пользователь будет перенаправлен после аутентификации. Для локальной разработки обычно это https://localhost:5001/signin-auth0 .
3. Получение идентификаторов: Из консоли Auth0 скопируйте Domain, Client ID и Client Secret — они понадобятся для настройки вашего приложения.
4. Настройка Claims: Часто требуется настроить какие именно данные о пользователе будут передаваться в токене. В Auth0 это можно сделать через Rules.
Важный момент, о котором часто забывают: в продакшн-средах никогда не используйте самоподписанные сертификаты для подписи токенов! Это серьезная брешь в безопасности. Вместо этого используйте X.509 сертификаты от доверенных центров сертификации или, по крайней мере, корпоративный CA.
Особенности настройки SSO для микросервисной архитектуры
Микросервисы вносят дополнительную сложность в настройку SSO. Здесь требуется не только аутентификация пользователей, но и межсервисное взаимодействие, где сервисы могут обращаться друг к другу от имени пользователя или от своего имени.
Для этого обычно используется делегирование доступа с помощью OAuth 2.0:
1. Пользователь аутентифицируется в Gateway-сервисе.
2. Gateway получает токен аутентификации (Access Token).
3. При запросе к другим сервисам Gateway пересылает этот токен или генерирует новый с ограниченным сроком действия.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public class GatewayAuthenticationHandler : DelegatingHandler
{
private readonly ITokenStore _tokenStore;
public GatewayAuthenticationHandler(ITokenStore tokenStore)
{
_tokenStore = tokenStore;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// Получаем токен для сервиса, к которому обращаемся
var token = await _tokenStore.GetTokenForServiceAsync(request.RequestUri.Host);
// Добавляем токен в заголовок запроса
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Передаем запрос дальше
return await base.SendAsync(request, cancellationToken);
}
} |
|
Еще один важный аспект — кэширование токенов. В микросервисной архитектуре запрос пользователя может проходить через десятки сервисов, и если каждый будет проверять валидность токена напрямую у IdP — производительность системы резко упадет. Решение? Распределенный кэш токенов, например, на базе Redis.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| public class RedisTokenCache : ITokenCache
{
private readonly IDistributedCache _cache;
public RedisTokenCache(IDistributedCache cache)
{
_cache = cache;
}
public async Task<TokenValidationResult> ValidateTokenAsync(string token)
{
// Пытаемся получить результат валидации из кэша
var cachedResult = await _cache.GetStringAsync(token);
if (cachedResult != null)
{
return JsonSerializer.Deserialize<TokenValidationResult>(cachedResult);
}
// Если в кэше нет, проверяем через IdP
var result = await ValidateTokenWithIdPAsync(token);
// Сохраняем результат в кэш с временем жизни чуть меньше, чем время жизни токена
await _cache.SetStringAsync(token,
JsonSerializer.Serialize(result),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(55) // Для часового токена
});
return result;
}
private async Task<TokenValidationResult> ValidateTokenWithIdPAsync(string token)
{
// Логика проверки токена через IdP
// ...
}
} |
|
Исследование, проведенное в 2022 году командой Microsoft, показало, что правильно настроенное кэширование токенов может снизить латентность микросервисных систем на 30-40% при высоких нагрузках.
Использование Docker для создания изолированной тестовой среды
Один из самых действенных способов избежать головной боли при разработке SSO-решений — использование Docker для создания изолированной среды. Этот подход позволяет создать копию продакшн-окружения и тестировать интеграцию, не опасаясь сломать что-то важное. Вот минимальный docker-compose.yml для тестового стенда с IdentityServer и тестовым клиентским приложением:
YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| version: '3.8'
services:
identity-server:
build:
context: ./IdentityServer
ports:
- "5000:80"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__DefaultConnection=Server=db;Database=IdentityServer;User=sa;Password=YourStrong!Password;
depends_on:
- db
client-app:
build:
context: ./ClientApp
ports:
- "5001:80"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- IdentityServer__Authority=http://identity-server
depends_on:
- identity-server
db:
image: mcr.microsoft.com/mssql/server:2019-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=YourStrong!Password
ports:
- "1433:1433"
volumes:
- identity-db:/var/opt/mssql
volumes:
identity-db: |
|
Такой подход особенно полезен, когда нужно проверить, как система будет работать при нестандартных ситуациях — например, при недоступности IdP или при медленных сетевых соединениях. С помощью Docker Network можно симулировать различные сетевые условия:
Bash | 1
2
3
4
| docker network create --driver bridge identity-network
docker network connect --link identity-server:identity-server identity-network client-app
# Имитация задержки в 100 мс
tc qdisc add dev eth0 root netem delay 100ms |
|
"Помню, как мы несколько дней не могли понять, почему в продакшене периодически проскакивают ошибки аутентификации, а на тестовых стендах всё работает идеально," — вспоминает DevOps-инженер Сергей. "Оказалось, проблема в таймаутах при обращении к IdP в условиях пиковых нагрузок. Если бы мы сразу тестировали с имитацией сетевых задержек, сэкономили бы кучу времени."
Требования к C# проекту
Для успешной интеграции SSO ваш C# проект должен соответствовать определенным требованиям:
1. ASP.NET Core 3.1 или новее: Старые версии ASP.NET не имеют полноценной поддержки современных механизмов аутентификации.
2. HTTPS: Обязательное требование для безопасной передачи токенов и данных аутентификации. В режиме разработки можно использовать локальные самоподписанные сертификаты:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.UseKestrel(options =>
{
options.Listen(IPAddress.Loopback, 5001, listenOptions =>
{
listenOptions.UseHttps(httpsOptions =>
{
var localhostCert = CertificateLoader.LoadFromStoreCert(
"localhost", "My", StoreLocation.CurrentUser,
allowInvalid: true);
httpsOptions.ServerCertificate = localhostCert;
});
});
});
}); |
|
3. Правильная структура Configuration: Секреты и конфигурация SSO должны храниться безопасно, предпочтительно с использованием User Secrets для разработки и Azure Key Vault или аналогов для продакшена:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public Startup(IWebHostEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddUserSecrets<Startup>() // Для разработки
.AddEnvironmentVariables();
if (env.IsProduction())
{
builder.AddAzureKeyVault(
$"https://{Configuration["KeyVault:Name"]}.vault.azure.net/",
Configuration["KeyVault:ClientId"],
Configuration["KeyVault:ClientSecret"]);
}
Configuration = builder.Build();
} |
|
4. Корректный middleware pipeline: Важен порядок регистрации middleware в ASP.NET Core:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Сначала обработка исключений
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// Authentication должен быть перед Authorization!
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
} |
|
Отладка SSO-взаимодействий
Отладка SSO часто становится настоящим испытанием — множество перенаправлений, токены, разные домены и порты... Вот несколько инструментов, которые спасут вас от седых волос:
Fiddler — мощный прокси для перехвата и анализа HTTP/HTTPS трафика. Позволяет не только видеть запросы и ответы, но и модифицировать их на лету. Особенно полезна функция декодирования HTTPS трафика.
Browser DevTools — не недооценивайте встроенные инструменты разработчика в браузерах. Вкладка Network поможет отследить последовательность редиректов, а вкладка Application — проанализировать содержимое cookies и localStorage.
Для глубокой отладки ASP.NET Core Authentication middleware можно включить детальное логирование:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
| public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(builder =>
{
builder.AddFilter("Microsoft.AspNetCore.Authentication", LogLevel.Debug);
builder.AddFilter("Microsoft.AspNetCore.Authorization", LogLevel.Debug);
builder.AddConsole();
builder.AddDebug();
});
// Другие сервисы
} |
|
Разработка своего логгера для аутентификации также может быть полезной:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public class AuthenticationLogger
{
private readonly ILogger<AuthenticationLogger> _logger;
public AuthenticationLogger(ILogger<AuthenticationLogger> logger)
{
_logger = logger;
}
public void LogToken(string token)
{
try
{
var handler = new JwtSecurityTokenHandler();
var jwtToken = handler.ReadJwtToken(token);
_logger.LogInformation("Token subject: {Subject}", jwtToken.Subject);
_logger.LogInformation("Token issuer: {Issuer}", jwtToken.Issuer);
_logger.LogInformation("Token valid from: {ValidFrom}", jwtToken.ValidFrom);
_logger.LogInformation("Token valid to: {ValidTo}", jwtToken.ValidTo);
foreach (var claim in jwtToken.Claims)
{
_logger.LogInformation("Claim: {Type} = {Value}", claim.Type, claim.Value);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to parse JWT token");
}
}
} |
|
"Мы потратили три дня, выслеживая ошибку, которая оказалась в неправильном формате даты в JWT-токене," — рассказывает разработчик Виктор М. "После этого первое, что я делаю при интеграции с новым IdP — это логгирование всех полей и ручная проверка токенов. Иногда самые странные багги скрываются в самых неожиданных местах."
Теперь, когда мы подготовили среду разработки, понимаем основные нюансы и вооружились инструментами для отладки, можно приступать к самому интересному — практической реализации SSO в нашем C# приложении!
Практическая реализация
После всей теории и подготовительной работы пора перейти к самому интересному — непосредственной реализации SSO в нашем C# приложении. Как говорил мой наставник: "В теории теория и практика одинаковы. На практике — нет". Так что давайте закатаем рукава и погрузимся в код!
Интеграция с популярными провайдерами
Начнем с интеграции нашего приложения с одним из наиболее распространенных провайдеров — Auth0. Это позволит быстро запустить базовый вариант SSO и понять общие принципы работы.
Первое, что нам потребуется после регистрации в Auth0 — правильно сконфигурировать наше приложение:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| public void ConfigureServices(IServiceCollection services)
{
// Добавляем аутентификацию
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
// Основные настройки
options.Authority = $"https://{Configuration["Auth0:Domain"]}";
options.ClientId = Configuration["Auth0:ClientId"];
options.ClientSecret = Configuration["Auth0:ClientSecret"];
options.ResponseType = "code";
// Указываем, что хотим получить токен доступа и id_token
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
// Сохраняем токены для дальнейшего использования
options.SaveTokens = true;
// Настраиваем маппинг claims
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "https://myapp.com/roles"
};
// Обрабатываем события аутентификации
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = async ctx =>
{
// Дополнительная логика после валидации токена
var email = ctx.Principal.FindFirstValue(ClaimTypes.Email);
// Например, можно найти пользователя в локальной БД
},
OnRedirectToIdentityProviderForSignOut = async ctx =>
{
// Настройки для выхода из системы
var logoutUri = $"https://{Configuration["Auth0:Domain"]}/v2/logout?client_id={Configuration["Auth0:ClientId"]}";
ctx.ProtocolMessage.IssuerAddress = logoutUri;
}
};
});
services.AddControllersWithViews();
} |
|
После конфигурации аутентификации необходимо активировать её в pipeline приложения:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... другие middleware
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
// Защищаем нужные маршруты
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}")
.RequireAuthorization(); // Или используйте атрибуты [Authorize]
});
} |
|
Теперь для защиты отдельных контроллеров или действий используем атрибут [Authorize] :
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| [Authorize]
public class DashboardController : Controller
{
public IActionResult Index()
{
return View();
}
[Authorize(Roles = "Admin")]
public IActionResult AdminPanel()
{
return View();
}
} |
|
Довольно часто у меня спрашивают: "А что делать, если нужно вручную инициировать аутентификацию?" Вот пример такого действия:
C# | 1
2
3
4
5
| public IActionResult Login(string returnUrl = "/")
{
return Challenge(new AuthenticationProperties { RedirectUri = returnUrl },
OpenIdConnectDefaults.AuthenticationScheme);
} |
|
"На одном из проектов я потратил половину дня, пытаясь понять, почему не работает логин," — признается мой коллега Дмитрий. "Оказалось, что Auth0 по умолчанию экранирует спецсимволы в redirect_uri, а мой URL содержал '#'. Пришлось научиться отлаживать процесс аутентификации на низком уровне."
Работа с токенами и сессиями
После успешной аутентификации мы получаем токены — id_token и access_token. Их нужно правильно хранить и использовать.
Для доступа к токенам из контроллера:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| public class SecuredApiController : Controller
{
private readonly IHttpClientFactory _clientFactory;
public SecuredApiController(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
[Authorize]
public async Task<IActionResult> GetProtectedData()
{
// Получаем токен доступа из HttpContext
var accessToken = await HttpContext.GetTokenAsync("access_token");
// Используем токен для вызова защищенного API
var client = _clientFactory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.GetAsync("https://api.example.com/protected-data");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
return Json(content);
}
return StatusCode(500);
}
} |
|
Одна из распространенных проблем — истечение срока действия токена. Решить её можно с помощью refresh token:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| public class TokenService
{
private readonly HttpClient _httpClient;
private readonly IOptions<Auth0Settings> _options;
public TokenService(HttpClient httpClient, IOptions<Auth0Settings> options)
{
_httpClient = httpClient;
_options = options;
}
public async Task<TokenResponse> RefreshTokenAsync(string refreshToken)
{
var tokenEndpoint = $"https://{_options.Value.Domain}/oauth/token";
var parameters = new Dictionary<string, string>
{
["grant_type"] = "refresh_token",
["client_id"] = _options.Value.ClientId,
["client_secret"] = _options.Value.ClientSecret,
["refresh_token"] = refreshToken
};
var content = new FormUrlEncodedContent(parameters);
var response = await _httpClient.PostAsync(tokenEndpoint, content);
if (!response.IsSuccessStatusCode)
{
throw new Exception("Failed to refresh token");
}
var responseContent = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<TokenResponse>(responseContent);
}
} |
|
Для реального проекта рекомендую реализовать автоматическое обновление токена с помощью DelegatingHandler:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| public class TokenRefreshHandler : DelegatingHandler
{
private readonly TokenService _tokenService;
private readonly ITokenStore _tokenStore;
public TokenRefreshHandler(TokenService tokenService, ITokenStore tokenStore)
{
_tokenService = tokenService;
_tokenStore = tokenStore;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var userId = GetCurrentUserId(); // Получение текущего пользователя
// Пробуем отправить запрос с текущим токеном
var accessToken = await _tokenStore.GetAccessTokenAsync(userId);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await base.SendAsync(request, cancellationToken);
// Если токен истек, пробуем обновить и повторить запрос
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
var refreshToken = await _tokenStore.GetRefreshTokenAsync(userId);
if (!string.IsNullOrEmpty(refreshToken))
{
var tokens = await _tokenService.RefreshTokenAsync(refreshToken);
// Сохраняем новые токены
await _tokenStore.SaveTokensAsync(userId, tokens.AccessToken, tokens.RefreshToken);
// Повторяем запрос с новым токеном
request = CopyRequest(request); // Копируем оригинальный запрос
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
}
return response;
}
private HttpRequestMessage CopyRequest(HttpRequestMessage request)
{
var copy = new HttpRequestMessage(request.Method, request.RequestUri);
foreach (var header in request.Headers)
{
copy.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
if (request.Content != null)
{
copy.Content = new StreamContent(request.Content.ReadAsStreamAsync().Result);
foreach (var header in request.Content.Headers)
{
copy.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
copy.Properties.Clear();
foreach (var prop in request.Properties)
{
copy.Properties.Add(prop);
}
return copy;
}
} |
|
Динамическая регистрация клиентов
В некоторых сценариях (например, при создании SaaS-решения) требуется динамическая регистрация клиентов. Для этого можно использовать Dynamic Client Registration Protocol, который поддерживается многими OIDC-провайдерами:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| public class DynamicClientRegistrationService
{
private readonly HttpClient _httpClient;
private readonly string _registrationEndpoint;
public DynamicClientRegistrationService(HttpClient httpClient, string registrationEndpoint)
{
_httpClient = httpClient;
_registrationEndpoint = registrationEndpoint;
}
public async Task<ClientRegistrationResponse> RegisterClientAsync(ClientRegistrationRequest request)
{
var content = new StringContent(
JsonSerializer.Serialize(request),
Encoding.UTF8,
"application/json");
var response = await _httpClient.PostAsync(_registrationEndpoint, content);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
throw new Exception($"Failed to register client: {error}");
}
var responseContent = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<ClientRegistrationResponse>(responseContent);
}
} |
|
Пример использования:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
| var request = new ClientRegistrationRequest
{
ClientName = "New Tenant App",
RedirectUris = new[] { "https://tenant1.myapp.com/callback" },
GrantTypes = new[] { "authorization_code" },
ResponseTypes = new[] { "code" },
TokenEndpointAuthMethod = "client_secret_basic"
};
var client = await _registrationService.RegisterClientAsync(request);
// Сохраняем полученные client_id и client_secret
_tenantRepository.SaveClientCredentials(tenantId, client.ClientId, client.ClientSecret); |
|
Реализация межсервисной аутентификации
В микросервисной архитектуре часто требуется, чтобы сервисы взаимодействовали друг с другом от имени пользователя. Для этого используется паттерн Client Credentials Grant в OAuth 2.0:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| public class ServiceToServiceAuthClient
{
private readonly HttpClient _httpClient;
private readonly string _tokenEndpoint;
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _audience;
private string _cachedToken;
private DateTime _tokenExpiry;
public ServiceToServiceAuthClient(
HttpClient httpClient,
string tokenEndpoint,
string clientId,
string clientSecret,
string audience)
{
_httpClient = httpClient;
_tokenEndpoint = tokenEndpoint;
_clientId = clientId;
_clientSecret = clientSecret;
_audience = audience;
}
public async Task<string> GetAccessTokenAsync()
{
// Проверяем, не истек ли токен
if (!string.IsNullOrEmpty(_cachedToken) && DateTime.UtcNow < _tokenExpiry)
{
return _cachedToken;
}
// Запрашиваем новый токен
var parameters = new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = _clientId,
["client_secret"] = _clientSecret,
["audience"] = _audience
};
var content = new FormUrlEncodedContent(parameters);
var response = await _httpClient.PostAsync(_tokenEndpoint, content);
if (!response.IsSuccessStatusCode)
{
throw new Exception("Failed to obtain service-to-service access token");
}
var responseContent = await response.Content.ReadAsStringAsync();
var tokenResponse = JsonSerializer.Deserialize<TokenResponse>(responseContent);
// Кэшируем токен
_cachedToken = tokenResponse.AccessToken;
_tokenExpiry = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn - 60); // Небольшой запас
return _cachedToken;
}
} |
|
Для использования этого паттерна через ASP.NET Core, регистрируем наш клиент как сервис:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| services.AddHttpClient<ServiceToServiceAuthClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com");
})
.AddTransientHttpErrorPolicy(policy => policy.RetryAsync(3));
services.AddSingleton<ServiceToServiceAuthClient>(sp =>
{
var httpClient = sp.GetRequiredService<IHttpClientFactory>()
.CreateClient(nameof(ServiceToServiceAuthClient));
return new ServiceToServiceAuthClient(
httpClient,
Configuration["Auth:TokenEndpoint"],
Configuration["Auth:ClientId"],
Configuration["Auth:ClientSecret"],
Configuration["Auth:Audience"]);
}); |
|
"Как-то мы забыли закешировать токены между сервисами," — рассказывает архитектор Андрей К. "В итоге каждый запрос порождал новую аутентификацию, что при 500 RPS буквально положило наш IdP. К счастью, один из коллег быстро заметил проблему и поправил код."
Хранение и обновление refresh токенов
Refresh токены — секретная информация, которую нужно хранить с особой тщательностью. Для безопасного хранения рекомендую использовать шифрование:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| public class EncryptedTokenStore : ITokenStore
{
private readonly IDataProtector _protector;
private readonly IDistributedCache _cache;
public EncryptedTokenStore(
IDataProtectionProvider dataProtectionProvider,
IDistributedCache cache)
{
_protector = dataProtectionProvider.CreateProtector("TokenStore");
_cache = cache;
}
public async Task SaveTokensAsync(string userId, string accessToken, string refreshToken)
{
// Шифруем токены перед сохранением
var encryptedAccessToken = _protector.Protect(accessToken);
var encryptedRefreshToken = _protector.Protect(refreshToken);
await _cache.SetStringAsync($"AccessToken:{userId}", encryptedAccessToken,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(55) // Чуть меньше часа
});
await _cache.SetStringAsync($"RefreshToken:{userId}", encryptedRefreshToken,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(14) // Обычно 2 недели
});
}
public async Task<string> GetAccessTokenAsync(string userId)
{
var encryptedToken = await _cache.GetStringAsync($"AccessToken:{userId}");
if (string.IsNullOrEmpty(encryptedToken))
{
return null;
}
return _protector.Unprotect(encryptedToken);
}
public async Task<string> GetRefreshTokenAsync(string userId)
{
var encryptedToken = await _cache.GetStringAsync($"RefreshToken:{userId}");
if (string.IsNullOrEmpty(encryptedToken))
{
return null;
}
return _protector.Unprotect(encryptedToken);
}
} |
|
Этот код использует ASP.NET Core Data Protection API для шифрования токенов. В продакшн-среде обязательно настройте правильное хранилище ключей шифрования:
C# | 1
2
3
4
5
| services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri("https://mystorageaccount.blob.core.windows.net/keys/"))
.ProtectKeysWithAzureKeyVault(
new Uri("https://mykeyvault.vault.azure.net/keys/dataprotection/"),
new DefaultAzureCredential()); |
|
Обработка ошибок и исключений
При работе с SSO мы сталкиваемся с нестандартными ошибками аутентификации. Грамотная обработка этих ошибок критически важна для UX:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| services.AddAuthentication(options =>
{
// ... базовые настройки
})
.AddOpenIdConnect(options =>
{
// ... другие настройки
options.Events = new OpenIdConnectEvents
{
OnRemoteFailure = context =>
{
// Обрабатываем ошибку аутентификации
context.HandleResponse();
// Анализируем тип ошибки
var error = context.Failure?.Message ?? context.ProtocolMessage.Error;
if (error == "access_denied")
{
// Пользователь отменил вход - перенаправляем на страницу входа
context.Response.Redirect("/Account/Login");
return Task.CompletedTask;
}
if (error == "invalid_grant")
{
// Возможно, токен устарел - перенаправляем на страницу входа
context.Response.Redirect("/Account/Login?expired=true");
return Task.CompletedTask;
}
// Для остальных ошибок показываем страницу ошибки
context.Response.Redirect($"/Error?message={Uri.EscapeDataString(error)}");
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
// Логируем ошибку аутентификации
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILogger<Startup>>();
logger.LogError(context.Exception, "Authentication failed");
// Если это проблема с сертификатом, возможно IdP использует
// самоподписанный сертификат в dev-среде
if (context.Exception is SecurityTokenSignatureKeyNotFoundException)
{
context.SkipHandler();
return Task.CompletedTask;
}
return Task.CompletedTask;
}
};
}); |
|
Хорошая практика — создать специальную страницу ошибок аутентификации с понятными пользователю сообщениями:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| public class AuthErrorController : Controller
{
private readonly ILogger<AuthErrorController> _logger;
public AuthErrorController(ILogger<AuthErrorController> logger)
{
_logger = logger;
}
[AllowAnonymous]
public IActionResult Index(string error)
{
_logger.LogWarning("Authentication error: {Error}", error);
var viewModel = new AuthErrorViewModel
{
ErrorCode = error,
UserFriendlyMessage = GetUserFriendlyMessage(error),
SupportReference = GenerateSupportReference()
};
return View(viewModel);
}
private string GetUserFriendlyMessage(string errorCode)
{
return errorCode switch
{
"login_required" => "Вам необходимо войти в систему.",
"invalid_request" => "Возникла техническая проблема при входе. Попробуйте позже.",
"invalid_grant" => "Срок действия вашей сессии истек. Пожалуйста, войдите снова.",
"insufficient_scope" => "У вас нет доступа к этому ресурсу.",
"server_error" => "Сервер авторизации временно недоступен. Пожалуйста, повторите попытку позже.",
_ => "Произошла ошибка при аутентификации. Пожалуйста, повторите попытку."
};
}
private string GenerateSupportReference()
{
// Генерируем уникальный код для обращения в поддержку
return $"AUTH-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString().Substring(0, 8)}";
}
} |
|
Реализация Single Sign-Out
Важная часть SSO, которую часто упускают из виду — корректный выход из всех приложений (Single Sign-Out). Реализуем его:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public class AccountController : Controller
{
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
// Локальный выход
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// Выход из IdP
return SignOut(
new AuthenticationProperties {
RedirectUri = Url.Action("Index", "Home")
},
OpenIdConnectDefaults.AuthenticationScheme);
}
} |
|
Для микросервисной архитектуры можно реализовать более сложный механизм — например, через шину событий:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class SignOutPublisher
{
private readonly IMessageBroker _messageBroker;
public SignOutPublisher(IMessageBroker messageBroker)
{
_messageBroker = messageBroker;
}
public async Task PublishSignOutEventAsync(string userId)
{
var signOutEvent = new SignOutEvent
{
UserId = userId,
Timestamp = DateTimeOffset.UtcNow
};
await _messageBroker.PublishAsync("auth.signout", signOutEvent);
}
} |
|
На стороне каждого сервиса подписываемся на это событие:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class SignOutEventHandler : IMessageHandler<SignOutEvent>
{
private readonly ISessionStore _sessionStore;
public SignOutEventHandler(ISessionStore sessionStore)
{
_sessionStore = sessionStore;
}
public async Task HandleAsync(SignOutEvent message)
{
// Инвалидируем сессию пользователя в этом сервисе
await _sessionStore.InvalidateSessionsForUserAsync(message.UserId);
}
} |
|
"На проекте с 12 микросервисами мы столкнулись с проблемой — сессии не всегда корректно завершались во всех сервисах," — рассказывает DevOps-инженер Мария Л. "Пришлось разработать специальный механизм через RabbitMQ, который гарантировал доставку события выхода даже при временной недоступности отдельных сервисов."
Для полного понимания, как работает Single Sign-Out, давайте рассмотрим диаграмму последовательности:
1. Пользователь инициирует выход в одном из приложений.
2. Приложение выполняет локальный выход (очистка куки).
3. Приложение перенаправляет пользователя на конечную точку выхода IdP.
4. IdP инвалидирует свою сессию.
5. IdP отправляет запросы на front-channel logout URL каждому зарегистрированному приложению.
6. Каждое приложение выполняет локальный выход.
7. IdP перенаправляет пользователя обратно на исходное приложение.
Для работы с этим механизмом нужно добавить обработчик front-channel logout:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public class OpenIdConnectConfigurer
{
public static void Configure(OpenIdConnectOptions options)
{
// ... другие настройки
// Добавляем обработчик для front-channel logout
options.Events.OnSignedOutCallbackRedirect = context =>
{
context.HttpContext.Response.StatusCode = 200;
return Task.CompletedTask;
};
options.Events.OnRedirectToIdentityProviderForSignOut = context =>
{
// Настраиваем URL для выхода из IdP
var logoutUri = $"{context.ProtocolMessage.IssuerAddress}?id_token_hint={context.ProtocolMessage.IdTokenHint}";
// Добавляем post_logout_redirect_uri если нужно
var postLogoutUri = context.Properties.RedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
logoutUri += $"&post_logout_redirect_uri={Uri.EscapeDataString(postLogoutUri)}";
}
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
};
}
} |
|
Вот и все основные аспекты практической реализации SSO в C# приложениях. Теперь давайте двинемся дальше и рассмотрим более продвинутые темы, выходящие за рамки базовой реализации, которые позволят сделать вашу систему аутентификации более надежной и функциональной.
За пределами базовой реализации
Базовая реализация SSO позволяет решить основные задачи аутентификации, но современные системы требуют большего. Углубимся в продвинутые возможности, которые поднимут безопасность и удобство вашего южина аутентификации на новый уровень.
Многофакторная аутентификация
С ростом киберугроз одних паролей уже недостаточно. Многофакторная аутентификация (MFA) присоединяет дополнительные слои защиты, требуя от пользователя предоставить два или более различных фактора для подтверждения своей личности.
Большинство современных IdP поддерживают MFA из коробки. Например, в Auth0 это настраивается одной галочкой в админ-панели. Однако иногда требуется более тонкая настройка на стороне клиента. Вот как можно запросить MFA для определенных действий:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public class SensitiveOperationsController : Controller
{
[Authorize]
public IActionResult TransferFunds()
{
// Проверяем, прошел ли пользователь MFA
var identity = User.Identity as ClaimsIdentity;
var amrClaim = identity?.FindFirst("amr");
bool hasMfa = amrClaim?.Value.Contains("mfa") ?? false;
if (!hasMfa)
{
// Если нет, запрашиваем MFA
return Challenge(new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(TransferFunds)),
Items =
{
{"prompt", "login"},
{"acr_values", "mfa"}
}
}, OpenIdConnectDefaults.AuthenticationScheme);
}
// Пользователь прошел MFA, продолжаем операцию
return View();
}
} |
|
В некоторых случаях приходится реализовывать MFA самостоятельно. Например, если требуется специфический метод верификации или интеграция с корпоративными системами. Вот пример реализации простой формы второго фактора с одноразовыми кодами:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| public class MfaController : Controller
{
private readonly IMfaService _mfaService;
public MfaController(IMfaService mfaService)
{
_mfaService = mfaService;
}
[HttpGet]
public IActionResult VerifyCode()
{
return View();
}
[HttpPost]
public async Task<IActionResult> VerifyCode(VerifyCodeViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return RedirectToAction("Login", "Account");
}
var isValid = await _mfaService.ValidateCodeAsync(userId, model.Code);
if (!isValid)
{
ModelState.AddModelError("", "Неверный код подтверждения.");
return View(model);
}
// Добавляем claim о прохождении MFA
var identity = User.Identity as ClaimsIdentity;
identity?.AddClaim(new Claim("amr", "mfa"));
// Обновляем аутентификационный тикет
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, User);
// Перенаправляем на защищенный ресурс
return RedirectToAction("Index", "Dashboard");
}
} |
|
Также необходимо реализовать сам сервис MFA:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| public class TotpMfaService : IMfaService
{
private readonly IDataProtector _dataProtector;
private readonly IUserRepository _userRepository;
public TotpMfaService(
IDataProtectionProvider dataProtectionProvider,
IUserRepository userRepository)
{
_dataProtector = dataProtectionProvider.CreateProtector("Mfa.Totp");
_userRepository = userRepository;
}
public async Task<bool> ValidateCodeAsync(string userId, string code)
{
var user = await _userRepository.GetUserByIdAsync(userId);
if (user == null || string.IsNullOrEmpty(user.MfaSecretKey))
{
return false;
}
// Расшифровываем секретный ключ
var secretKey = _dataProtector.Unprotect(user.MfaSecretKey);
// Создаем TOTP на основе секретного ключа
var totp = new Totp(Base32Encoding.ToBytes(secretKey));
// Проверяем код
return totp.VerifyTotp(code, out _, new VerificationWindow(2, 2));
}
public async Task<string> GenerateQrCodeUriAsync(string userId, string email)
{
// Генерируем секретный ключ для пользователя
var secretKey = KeyGeneration.GenerateRandomKey(20);
var base32Secret = Base32Encoding.ToString(secretKey);
// Шифруем и сохраняем
var protectedSecret = _dataProtector.Protect(base32Secret);
await _userRepository.SetMfaSecretKeyAsync(userId, protectedSecret);
// Генерируем URI для QR-кода
var issuer = "MyApplication";
var totp = new Totp(secretKey);
return totp.GenerateQrCodeUri(issuer, email);
}
} |
|
"В банковском проекте мы столкнулись с проблемой: клиенты часто теряли доступ к своему второму фактору," — рассказывает руководитель разработки Алексей В. "Пришлось разработать целый процесс восстановления с использованием резервных кодов, SMS-подтверждения и даже видео-идентификации для особо важных операций."
Безопасное хранение секретов и ключей
Чем сложнее система аутентификации, тем больше секретов необходимо хранить — клиентские секреты, ключи подписи токенов, секретные ключи MFA. Правильное хранение этих секретов — критически важная задача.
В продакшн-среде Azure Key Vault — отличное решение:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| public class Startup
{
public Startup(IConfiguration configuration, IHostEnvironment environment)
{
// Базовая конфигурация
var builder = new ConfigurationBuilder()
.SetBasePath(environment.ContentRootPath)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{environment.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
if (environment.IsProduction())
{
// Добавляем Azure Key Vault как источник секретов
var builtConfig = builder.Build();
var keyVaultEndpoint = $"https://{builtConfig["KeyVault:Name"]}.vault.azure.net/";
builder.AddAzureKeyVault(keyVaultEndpoint, new DefaultAzureCredential());
}
Configuration = builder.Build();
}
// ... остальная часть Startup
} |
|
Для локальной разработки можно использовать User Secrets:
Bash | 1
2
| dotnet user-secrets init
dotnet user-secrets set "Auth0:ClientSecret" "your-secret-here" |
|
Для шифрования особо чувствительной информации в базе данных используйте ASP.NET Core Data Protection:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| public class UserRepository : IUserRepository
{
private readonly ApplicationDbContext _context;
private readonly IDataProtector _protector;
public UserRepository(
ApplicationDbContext context,
IDataProtectionProvider dataProtectionProvider)
{
_context = context;
_protector = dataProtectionProvider.CreateProtector("UserSecrets");
}
public async Task<User> GetUserByIdAsync(string userId)
{
var user = await _context.Users.FindAsync(userId);
if (user == null)
{
return null;
}
// Расшифровываем чувствительные данные
if (!string.IsNullOrEmpty(user.EncryptedApiKey))
{
user.ApiKey = _protector.Unprotect(user.EncryptedApiKey);
}
return user;
}
public async Task SaveApiKeyAsync(string userId, string apiKey)
{
var user = await _context.Users.FindAsync(userId);
if (user == null)
{
throw new KeyNotFoundException($"User with ID {userId} not found");
}
// Шифруем перед сохранением
user.EncryptedApiKey = _protector.Protect(apiKey);
user.ApiKey = null; // Не храним в памяти
await _context.SaveChangesAsync();
}
} |
|
Оптимизация производительности
При росте нагрузки система аутентификации может стать узким местом всего приложения. Вот несколько приемов для оптимизации:
1. Кэширование токенов: Используйте распределенный кэш для JWKs (JSON Web Key Set) и других метаданных OpenID Connect:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| services.AddAuthentication()
.AddJwtBearer(options =>
{
// ... другие настройки
// Настраиваем кэширование
options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
options.MetadataAddress,
new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever { RequireHttps = true })
{
AutomaticRefreshInterval = TimeSpan.FromHours(24),
RefreshInterval = TimeSpan.FromHours(12)
};
}); |
|
2. Проверка токенов локально: Если возможно, проверяйте токены локально, вместо обращения к IdP:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| public class LocalTokenValidator
{
private readonly IMemoryCache _cache;
private readonly HttpClient _httpClient;
private readonly string _issuer;
public LocalTokenValidator(
IMemoryCache cache,
HttpClient httpClient,
string issuer)
{
_cache = cache;
_httpClient = httpClient;
_issuer = issuer;
}
public async Task<ClaimsPrincipal> ValidateTokenAsync(string token)
{
var jwks = await GetJwksAsync();
var handler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = _issuer,
ValidateAudience = true,
ValidAudience = "myapi",
IssuerSigningKeys = jwks.Keys,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
return handler.ValidateToken(token, validationParameters, out _);
}
private async Task<JsonWebKeySet> GetJwksAsync()
{
// Пытаемся получить JWKS из кэша
if (!_cache.TryGetValue("jwks", out JsonWebKeySet jwks))
{
// Если в кэше нет, загружаем с сервера
var response = await _httpClient.GetStringAsync($"{_issuer}/.well-known/jwks.json");
jwks = JsonConvert.DeserializeObject<JsonWebKeySet>(response);
// Сохраняем в кэш
_cache.Set("jwks", jwks, TimeSpan.FromHours(24));
}
return jwks;
}
} |
|
3. Асинхронная проверка: Для повышения производительности используйте асинхронные методы везде, где это возможно:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| public class OptimizedAuthenticationHandler : AuthenticationHandler<JwtBearerOptions>
{
public OptimizedAuthenticationHandler(
IOptionsMonitor<JwtBearerOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Асинхронно получаем токен из заголовка
var token = GetTokenFromHeader();
if (token == null)
{
return AuthenticateResult.NoResult();
}
try
{
// Асинхронно валидируем токен
var principal = await ValidateTokenAsync(token);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
catch (Exception ex)
{
return AuthenticateResult.Fail(ex);
}
}
private async Task<ClaimsPrincipal> ValidateTokenAsync(string token)
{
// Асинхронная валидация токена
// ...
}
} |
|
Интеграция с корпоративными каталогами
В корпоративной среде часто требуется интеграция с существующими каталогами пользователей — Active Directory, LDAP и т.д. Вот пример интеграции с Active Directory через LDAP:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
| public class ActiveDirectoryAuthService
{
private readonly string _ldapServer;
private readonly string _domain;
public ActiveDirectoryAuthService(string ldapServer, string domain)
{
_ldapServer = ldapServer;
_domain = domain;
}
public async Task<bool> ValidateCredentialsAsync(string username, string password)
{
return await Task.Run(() =>
{
try
{
using var connection = new LdapConnection(new LdapDirectoryIdentifier(_ldapServer));
connection.SessionOptions.ProtocolVersion = 3;
// Формируем имя пользователя в формате UPN
var userPrincipalName = $"{username}@{_domain}";
// Пытаемся выполнить bind с учетными данными пользователя
connection.Bind(new NetworkCredential(userPrincipalName, password));
return true;
}
catch (LdapException)
{
return false;
}
});
}
public async Task<UserInfo> GetUserInfoAsync(string username)
{
return await Task.Run(() =>
{
using var connection = new LdapConnection(new LdapDirectoryIdentifier(_ldapServer));
connection.SessionOptions.ProtocolVersion = 3;
// Подключаемся с сервисной учетной записью
connection.Bind(new NetworkCredential(
"serviceAccount",
"servicePassword",
_domain));
// Ищем пользователя
var request = new SearchRequest(
$"DC={string.Join(",DC=", _domain.Split('.'))}",
$"(sAMAccountName={username})",
SearchScope.Subtree,
new string[] { "givenName", "sn", "mail", "memberOf" });
var response = (SearchResponse)connection.SendRequest(request);
if (response.Entries.Count == 0)
{
return null;
}
var entry = response.Entries[0];
return new UserInfo
{
FirstName = entry.GetAttribute("givenName")?.StringValue,
LastName = entry.GetAttribute("sn")?.StringValue,
Email = entry.GetAttribute("mail")?.StringValue,
Groups = GetGroups(entry.GetAttribute("memberOf"))
};
});
}
private List<string> GetGroups(DirectoryAttribute memberOf)
{
if (memberOf == null)
{
return new List<string>();
}
var groups = new List<string>();
foreach (var group in memberOf.GetValues(typeof(string)))
{
// Извлекаем имя группы из DN
var match = Regex.Match(group.ToString(), "CN=([^,]+)");
if (match.Success)
{
groups.Add(match.Groups[1].Value);
}
}
return groups;
}
} |
|
Для интеграции Active Directory с IdentityServer можно использовать пользовательский GrantValidator:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| public class ActiveDirectoryResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
private readonly ActiveDirectoryAuthService _adAuthService;
private readonly IUserRepository _userRepository;
public ActiveDirectoryResourceOwnerPasswordValidator(
ActiveDirectoryAuthService adAuthService,
IUserRepository userRepository)
{
_adAuthService = adAuthService;
_userRepository = userRepository;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
// Проверяем учетные данные в Active Directory
var isValid = await _adAuthService.ValidateCredentialsAsync(
context.UserName, context.Password);
if (!isValid)
{
context.Result = new GrantValidationResult(
TokenRequestErrors.InvalidGrant,
"Invalid username or password");
return;
}
// Получаем информацию о пользователе
var userInfo = await _adAuthService.GetUserInfoAsync(context.UserName);
if (userInfo == null)
{
context.Result = new GrantValidationResult(
TokenRequestErrors.InvalidGrant,
"User not found in directory");
return;
}
// Проверяем, есть ли пользователь в нашей базе
var user = await _userRepository.GetUserByUsernameAsync(context.UserName);
if (user == null)
{
// Создаем нового пользователя
user = new User
{
Username = context.UserName,
Email = userInfo.Email,
FirstName = userInfo.FirstName,
LastName = userInfo.LastName
};
await _userRepository.CreateUserAsync(user);
}
// Успешная аутентификация
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.Subject, user.Id),
new Claim(JwtClaimTypes.Name, $"{userInfo.FirstName} {userInfo.LastName}"),
new Claim(JwtClaimTypes.Email, userInfo.Email)
};
// Добавляем роли из групп Active Directory
foreach (var group in userInfo.Groups)
{
claims.Add(new Claim(JwtClaimTypes.Role, group));
}
context.Result = new GrantValidationResult(
user.Id, "ad", claims);
}
} |
|
Полное рабочее решение
Приведу полный пример интеграции SSO с Auth0 в ASP.NET Core MVC приложении:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
| // Program.cs
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
var builder = WebApplication.CreateBuilder(args);
// Добавляем services
builder.Services.AddControllersWithViews();
// Настройка аутентификации
builder.Services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options => {
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
})
.AddOpenIdConnect(options => {
// Настройки Auth0
options.Authority = $"https://{builder.Configuration["Auth0:Domain"]}";
options.ClientId = builder.Configuration["Auth0:ClientId"];
options.ClientSecret = builder.Configuration["Auth0:ClientSecret"];
options.ResponseType = OpenIdConnectResponseType.Code;
// Настройки области
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
// Настройки маппинга claims
options.TokenValidationParameters = new TokenValidationParameters {
NameClaimType = "name",
RoleClaimType = "https://myapp.com/roles"
};
// Сохраняем токены
options.SaveTokens = true;
// Обработчики событий
options.Events = new OpenIdConnectEvents {
OnTokenValidated = async context => {
// Здесь можно выполнять дополнительную валидацию или маппинг ролей
var identity = context.Principal.Identity as ClaimsIdentity;
// Получаем информацию о пользователе из токена
var userId = context.SecurityToken.Subject;
var email = identity.FindFirst(ClaimTypes.Email)?.Value;
// Проверяем, существует ли пользователь в нашей БД
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
var user = await userService.GetUserByExternalIdAsync(userId);
if (user == null) {
// Создаем пользователя, если его нет
user = new User {
ExternalId = userId,
Email = email,
Name = identity.FindFirst(ClaimTypes.Name)?.Value ?? email
};
await userService.CreateUserAsync(user);
}
// Добавляем роли из нашей системы
var roles = await userService.GetUserRolesAsync(user.Id);
foreach (var role in roles) {
identity.AddClaim(new Claim(context.Options.TokenValidationParameters.RoleClaimType, role));
}
},
OnRedirectToIdentityProvider = context => {
// Можно модифицировать запрос перед переадресацией к IdP
return Task.CompletedTask;
},
OnRemoteFailure = context => {
context.Response.Redirect("/Error/Auth");
context.HandleResponse();
return Task.CompletedTask;
}
};
});
// Добавляем пользовательские сервисы
builder.Services.AddHttpClient();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<ITokenService, TokenService>();
// Настройка авторизации
builder.Services.AddAuthorization(options => {
options.AddPolicy("RequireAdminRole", policy => policy.RequireRole("Admin"));
options.AddPolicy("RequireMfa", policy =>
policy.RequireClaim("amr", "mfa"));
});
var app = builder.Build();
// Настройка middleware
if (app.Environment.IsDevelopment()) {
app.UseDeveloperExceptionPage();
} else {
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run(); |
|
Конфигурация CORS для SSO-решений
Web-приложения часто сталкиваются с политикой одного источника (Same Origin Policy), что особенно важно в контексте SSO, где приходится взаимодействовать с разными доменами. Правильная настройка CORS (Cross-Origin Resource Sharing) — ключевой момент для работы SSO в многодоменных системах.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("SsoPolicy", builder =>
{
builder
.WithOrigins(
"https://app1.example.com",
"https://app2.example.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials(); // Важно для передачи куки между доменами
});
});
// Остальные сервисы
}
public void Configure(IApplicationBuilder app)
{
// CORS должен быть настроен до маршрутизации
app.UseCors("SsoPolicy");
// Другие middleware
app.UseAuthentication();
app.UseAuthorization();
} |
|
"На одном проекте мы недоумевали, почему токены не передаются между нашими поддоменами, — делится разработчик Евгений М. — Оказалось, мы забыли настроить SameSite=None для куки. Иногда простейшие вещи отнимают больше всего времени."
Федеративная аутентификация
Федеративная аутентификация позволяет пользователям одной организации получать доступ к ресурсам другой без необходимости создания отдельных учетных записей. Например, корпоративные пользователи могут логиниться через свой ActiveDirectory для доступа к внешним сервисам. Реализуем поддержку нескольких IdP в одном приложении:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| services.AddAuthentication()
.AddOpenIdConnect("AzureAD", options =>
{
options.Authority = "https://login.microsoftonline.com/common";
options.ClientId = Configuration["Authentication:AzureAd:ClientId"];
options.CallbackPath = "/signin-azuread";
// Другие настройки Azure AD
})
.AddOpenIdConnect("Auth0", options =>
{
options.Authority = $"https://{Configuration["Authentication:Auth0:Domain"]}";
options.ClientId = Configuration["Authentication:Auth0:ClientId"];
options.CallbackPath = "/signin-auth0";
// Другие настройки Auth0
}); |
|
А теперь создадим контроллер для выбора провайдера:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public class AccountController : Controller
{
public IActionResult Login(string returnUrl = "/")
{
return View(new LoginViewModel { ReturnUrl = returnUrl });
}
[HttpPost]
public IActionResult ExternalLogin(string provider, string returnUrl = "/")
{
// Указываем, какой провайдер должен обрабатывать аутентификацию
return Challenge(new AuthenticationProperties
{
RedirectUri = returnUrl
}, provider);
}
} |
|
Кастомные маркеры безопасности
Иногда стандартных маркеров и жетонов недостаточно. Например, для особо чувствительных операций можно добавить собственные маркеры безопасности:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
| public class SecurityTokenService
{
private readonly IDistributedCache _cache;
private readonly IDataProtector _protector;
public SecurityTokenService(
IDistributedCache cache,
IDataProtectionProvider dataProtectionProvider)
{
_cache = cache;
_protector = dataProtectionProvider.CreateProtector("SecurityTokens");
}
public async Task<string> GenerateOperationTokenAsync(string userId, string operation)
{
// Создаем уникальный токен
var tokenId = Guid.NewGuid().ToString();
var tokenData = new OperationTokenData
{
UserId = userId,
Operation = operation,
CreatedAt = DateTime.UtcNow
};
// Шифруем данные токена
var serializedData = JsonSerializer.Serialize(tokenData);
var protectedData = _protector.Protect(serializedData);
// Сохраняем в кэш с временем жизни
await _cache.SetStringAsync(
$"op_token:{tokenId}",
protectedData,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
});
return tokenId;
}
public async Task<OperationTokenData> ValidateOperationTokenAsync(string tokenId)
{
var cacheKey = $"op_token:{tokenId}";
var protectedData = await _cache.GetStringAsync(cacheKey);
if (string.IsNullOrEmpty(protectedData))
{
return null; // Токен не найден или истек
}
try
{
// Расшифровываем и проверяем данные
var serializedData = _protector.Unprotect(protectedData);
var tokenData = JsonSerializer.Deserialize<OperationTokenData>(serializedData);
// Одноразовый токен - удаляем после использования
await _cache.RemoveAsync(cacheKey);
return tokenData;
}
catch
{
return null; // Ошибка при расшифровке
}
}
} |
|
Использование такого токена для подтверждения важной операции:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
| [Authorize]
public class PaymentController : Controller
{
private readonly SecurityTokenService _securityTokenService;
// ... конструктор и внедрение зависимостей
[HttpGet]
public IActionResult Confirm(decimal amount, string recipientId)
{
// Сохраняем детали операции в сессии или TempData
TempData["PaymentAmount"] = amount;
TempData["RecipientId"] = recipientId;
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Confirm(string confirmationCode)
{
// Получаем детали из TempData
var amount = (decimal)TempData["PaymentAmount"];
var recipientId = (string)TempData["RecipientId"];
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Генерируем токен для этой операции
var operationToken = await _securityTokenService
.GenerateOperationTokenAsync(userId, $"payment:{amount}:{recipientId}");
// Отправляем пользователю код подтверждения (SMS, email и т.д.)
await _notificationService.SendConfirmationCodeAsync(userId, operationToken);
return RedirectToAction(nameof(ExecutePayment), new { token = operationToken });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ExecutePayment(string token, string confirmationCode)
{
// Проверяем токен операции
var tokenData = await _securityTokenService.ValidateOperationTokenAsync(token);
if (tokenData == null)
{
return BadRequest("Invalid or expired operation token");
}
// Проверяем, что операция инициирована текущим пользователем
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (currentUserId != tokenData.UserId)
{
return Unauthorized();
}
// Парсим информацию об операции
var parts = tokenData.Operation.Split(':');
if (parts.Length != 3 || parts[0] != "payment")
{
return BadRequest("Invalid operation");
}
var amount = decimal.Parse(parts[1]);
var recipientId = parts[2];
// Проверяем код подтверждения
if (!await _verificationService.VerifyCodeAsync(currentUserId, token, confirmationCode))
{
ModelState.AddModelError("", "Неверный код подтверждения");
return View();
}
// Выполняем платеж
var result = await _paymentService.ExecutePaymentAsync(
currentUserId, recipientId, amount);
return RedirectToAction("Receipt", new { id = result.TransactionId });
}
} |
|
Аудит и логирование событий безопасности
В продакшн-системах крайне важно иметь подробный аудит событий авторизации. Реализуем централизованную систему логирования:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| public class SecurityAuditService : ISecurityAuditService
{
private readonly ApplicationDbContext _dbContext;
private readonly IHttpContextAccessor _httpContextAccessor;
public SecurityAuditService(
ApplicationDbContext dbContext,
IHttpContextAccessor httpContextAccessor)
{
_dbContext = dbContext;
_httpContextAccessor = httpContextAccessor;
}
public async Task LogAuthenticationEventAsync(
string userId,
string eventType,
bool success,
string details = null)
{
var auditEvent = new AuthenticationAuditEvent
{
UserId = userId,
EventType = eventType,
EventTime = DateTime.UtcNow,
Success = success,
Details = details,
IpAddress = GetUserIpAddress(),
UserAgent = GetUserAgent()
};
_dbContext.AuthenticationEvents.Add(auditEvent);
await _dbContext.SaveChangesAsync();
}
private string GetUserIpAddress()
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null) return null;
// Учитываем возможные прокси
var forwardedFor = httpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault();
return !string.IsNullOrEmpty(forwardedFor)
? forwardedFor.Split(',')[0].Trim()
: httpContext.Connection.RemoteIpAddress?.ToString();
}
private string GetUserAgent()
{
var httpContext = _httpContextAccessor.HttpContext;
return httpContext?.Request.Headers["User-Agent"].ToString();
}
} |
|
Интеграция с OpenIdConnect для аудита аутентификации:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| services.AddAuthentication()
.AddOpenIdConnect(options =>
{
// ... другие настройки
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = async context =>
{
var auditService = context.HttpContext.RequestServices
.GetRequiredService<ISecurityAuditService>();
var userId = context.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
await auditService.LogAuthenticationEventAsync(
userId,
"TokenValidated",
true,
$"Provider: {options.Authority}");
},
OnAuthenticationFailed = async context =>
{
var auditService = context.HttpContext.RequestServices
.GetRequiredService<ISecurityAuditService>();
await auditService.LogAuthenticationEventAsync(
null, // UserId неизвестен при неудачной аутентификации
"AuthenticationFailed",
false,
$"Error: {context.Exception.Message}");
},
// Другие события...
};
}); |
|
Внедряя эти расширеные возможности в ваше SSO-решение, вы создаете не просто аутентификацию, а полноценную систему безопасности, способную удовлетворить требования даже самых взыскательных корпоративных заказчиков и аудиторов безопасности.
Проблема с windows authentication (SSO) в MVC приложении Имеется корпоративная сеть с ldap. Все приложения работают по технологии Single Sign-On. Я... Авторизациях через SSO без хранения пароля. Но с передачей идентификатора с использованием хеш-код Ребят помогите написать программу реализующую Single sign on, где пароль хешируется с... Подписывание сборок (sign with a strong name) - кто использует В каких случаях вы используете подписывание сборок (sign with a strong name)? Описать функцию Sign(x) целого типа Описать фукцию Sign(X) целого типа,возвращающую для вещественного числа Х след. значения.
-1, Х<0 ... Описать функцию Sign(X) Описать функцию Sign(X) целого типа, возвращающую для вещественного числа X следующие значения: —... Описать функцию Sign(X) целого типа, возвращающую для вещественного числа X следующие значения: –1, если X < 0; 0, если Описать функцию Sign(X) целого типа, возвращающую для вещественного числа X
следующие значения:... Описать функцию Sign(x) целого типа Описать функцию Sign(x) целого типа, возвращающую для вещественного числа X следующие значения: -1,... Описать функцию Sign(X) целого типа, для вещественного числа X Доброго времени суок! Помогите решить.
Описать функцию Sign(X) целого типа, возвращающую для... Описать функцию Sign(x) Описать функцию Sign(X) целого типа, возвращающую для вещественного числа X следующие значения: —... Failed to sign APK. See the console for details Когда пытаюсь забилдить игру, в конце вылазит вот эта ошибка. Посмотрел и перечитал много... Не получается подписать apk: Failed to sign apk перелазил много сайтов решение не нашёл. java если что 8-я. Не работает Unity hub кнопка sign in Скачал установил unity hub и обычную юнити.
Жму в Unity hub sign in, реакции 0 как и на попытку...
|