Аутентификация на основе токенов в ядре ASP.NET(обновлена)
Я работаю с приложением ASP.NET Core. Я пытаюсь выполнить аутентификацию на основе токенов, но не могу понять, как использовать новую систему безопасности.
Мой сценарий:
Клиент запрашивает токен. Мой сервер должен авторизовать пользователя и возвращать access_token, который будет использоваться клиентом в следующих запросах.
Вот две замечательные статьи о том, как реализовать именно то, что мне нужно:
Проблема в том, что для ASP.NET Core не очевидно, как сделать то же самое.
Мой вопрос: как настроить приложение ASP.NET Core Web Api для работы с аутентификацией на токене? Какое направление следует преследовать? Вы писали какие-либо статьи о новейшей версии или знаете, где я могу их найти?
Спасибо!
Ответы
Ответ 1
Работа от сказочного ответа Matt Dekrey, я создал полностью рабочий пример аутентификации на токенах, работая с ASP.NET Core (1.0.1). Вы можете найти полный код в этом репозитории на GitHub (альтернативные ветки для 1.0.0-rc1, beta8, beta7), но вкратце, важными шагами являются:
Создать ключ для вашего приложения
В моем примере я генерирую случайный ключ каждый раз, когда приложение запускается, вам нужно сгенерировать его и сохранить его где-нибудь и предоставить его вашему приложению. Посмотрите этот файл, как я генерирую случайный ключ и как вы можете импортировать его из файла .json. Как было предложено в комментариях @kspearrin, API защиты данных кажется идеальным кандидатом для управления ключами "правильно", но я не если это возможно, выработали. Пожалуйста, отправьте запрос на вытягивание, если вы его разработали!
Startup.cs - ConfigureServices
Здесь нам нужно загрузить закрытый ключ для наших токенов, которые будут подписаны, что мы также будем использовать для проверки токенов по мере их представления. Мы сохраняем ключ в переменной уровня key
класса, которую мы будем повторно использовать в методе Configure ниже. TokenAuthOptions - это простой класс, который содержит идентификатор подписки, аудиторию и эмитента, которые нам понадобятся в TokenController для создания наших ключей.
// Replace this with some sort of loading from config / file.
RSAParameters keyParams = RSAKeyUtils.GetRandomKey();
// Create the key, and a set of token options to record signing credentials
// using that key, along with the other parameters we will need in the
// token controlller.
key = new RsaSecurityKey(keyParams);
tokenOptions = new TokenAuthOptions()
{
Audience = TokenAudience,
Issuer = TokenIssuer,
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.Sha256Digest)
};
// Save the token options into an instance so they're accessible to the
// controller.
services.AddSingleton<TokenAuthOptions>(tokenOptions);
// Enable the use of an [Authorize("Bearer")] attribute on methods and
// classes to protect.
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
});
Мы также установили политику авторизации, чтобы мы могли использовать [Authorize("Bearer")]
для конечных точек и классов, которые мы хотим защитить.
Startup.cs - Настроить
Здесь нам нужно настроить JwtBearerAuthentication:
app.UseJwtBearerAuthentication(new JwtBearerOptions {
TokenValidationParameters = new TokenValidationParameters {
IssuerSigningKey = key,
ValidAudience = tokenOptions.Audience,
ValidIssuer = tokenOptions.Issuer,
// When receiving a token, check that it is still valid.
ValidateLifetime = true,
// This defines the maximum allowable clock skew - i.e.
// provides a tolerance on the token expiry time
// when validating the lifetime. As we're creating the tokens
// locally and validating them on the same machines which
// should have synchronised time, this can be set to zero.
// Where external tokens are used, some leeway here could be
// useful.
ClockSkew = TimeSpan.FromMinutes(0)
}
});
TokenController
В контроллере маркера вам необходимо иметь способ генерации подписанных ключей с помощью ключа, который был загружен в Startup.cs. Мы зарегистрировали экземпляр TokenAuthOptions в Startup, поэтому нам нужно добавить это в конструктор для TokenController:
[Route("api/[controller]")]
public class TokenController : Controller
{
private readonly TokenAuthOptions tokenOptions;
public TokenController(TokenAuthOptions tokenOptions)
{
this.tokenOptions = tokenOptions;
}
...
Затем вам нужно будет создать токен в обработчике для конечной точки входа, в моем примере я беру имя пользователя и пароль и проверяю те, которые используют оператор if, но главное, что вам нужно сделать, это создать или загрузить идентификатор на основе утверждений и создать для него токен:
public class AuthRequest
{
public string username { get; set; }
public string password { get; set; }
}
/// <summary>
/// Request a new token for a given username/password pair.
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
[HttpPost]
public dynamic Post([FromBody] AuthRequest req)
{
// Obviously, at this point you need to validate the username and password against whatever system you wish.
if ((req.username == "TEST" && req.password == "TEST") || (req.username == "TEST2" && req.password == "TEST"))
{
DateTime? expires = DateTime.UtcNow.AddMinutes(2);
var token = GetToken(req.username, expires);
return new { authenticated = true, entityId = 1, token = token, tokenExpires = expires };
}
return new { authenticated = false };
}
private string GetToken(string user, DateTime? expires)
{
var handler = new JwtSecurityTokenHandler();
// Here, you should create or look up an identity for the user which is being authenticated.
// For now, just creating a simple generic identity.
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user, "TokenAuth"), new[] { new Claim("EntityID", "1", ClaimValueTypes.Integer) });
var securityToken = handler.CreateToken(new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor() {
Issuer = tokenOptions.Issuer,
Audience = tokenOptions.Audience,
SigningCredentials = tokenOptions.SigningCredentials,
Subject = identity,
Expires = expires
});
return handler.WriteToken(securityToken);
}
И это должно быть так. Просто добавьте [Authorize("Bearer")]
к любому методу или классу, который вы хотите защитить, и вы должны получить сообщение об ошибке, если попытаетесь получить к нему доступ без присутствующего токена. Если вы хотите вернуть ошибку 401 вместо ошибки 500, вам нужно зарегистрировать собственный обработчик исключений как это было в моем примере здесь.
Ответ 2
-
Создайте ключ RSA только для своего приложения. Ниже приведен очень простой пример, но есть много информации о том, как ключи безопасности обрабатываются в .Net Framework; Я настоятельно рекомендую вам прочитать некоторые из них, по крайней мере.
private static string GenerateRsaKeys()
{
RSACryptoServiceProvider myRSA = new RSACryptoServiceProvider(2048);
RSAParameters publicKey = myRSA.ExportParameters(true);
return myRSA.ToXmlString(includePrivateParameters: true);
}
Сохраните это в .xml файле и включите его в ваше приложение; Я встроил его в свою DLL, потому что это небольшой персональный проект, который я решил, что никто не должен получить доступ к моей сборке в любом случае, но есть много причин, почему это не очень хорошая идея, и поэтому я не предоставляю этот пример здесь. В конечном счете, вам нужно решить, что лучше всего подходит для вашего проекта.
Примечание. Было указано, что ToXmlString
и FromXmlString
недоступны в .NET Core. Вместо этого вы можете сохранять/загружать значения самостоятельно, используя RSAParameters ExportParameters(bool includePrivateParameters)
и void ImportParameters(RSAParameters parameters)
с помощью Core-совместимого способа, например, с помощью JSON.
-
Создайте несколько констант, которые мы будем использовать позже; вот что я сделал:
const string TokenAudience = "Myself";
const string TokenIssuer = "MyProject";
-
Добавьте это в свой Startup.cs ConfigureServices
. Позднее мы будем использовать инъекцию зависимостей для доступа к этим настройкам. Я оставляю доступ к XML-потоку RSA; но я предполагаю, что у вас есть доступ к нему в переменной stream
.
RsaSecurityKey key;
using (var textReader = new System.IO.StreamReader(stream))
{
RSACryptoServiceProvider publicAndPrivate = new RSACryptoServiceProvider();
publicAndPrivate.FromXmlString(textReader.ReadToEnd());
key = new RsaSecurityKey(publicAndPrivate.ExportParameters(true));
}
services.AddInstance(new SigningCredentials(key,
SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest));
services.Configure<OAuthBearerAuthenticationOptions>(bearer =>
{
bearer.TokenValidationParameters.IssuerSigningKey = key;
bearer.TokenValidationParameters.ValidAudience = TokenAudience;
bearer.TokenValidationParameters.ValidIssuer = TokenIssuer;
});
-
Настроить идентификацию носителя. Если вы используете Identity, сделайте это до строки UseIdentity
. Обратите внимание, что любые сторонние строки аутентификации, такие как UseGoogleAuthentication
, должны идти до строки UseIdentity
. Вам не нужен UseCookieAuthentication
, если вы используете Identity.
app.UseOAuthBearerAuthentication();
-
Вы можете указать AuthorizationPolicy
. Это позволит вам указать контроллеры и действия, которые разрешают токены-носители в качестве аутентификации, используя [Authorize("Bearer")]
.
services.ConfigureAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationTypes(OAuthBearerAuthenticationDefaults.AuthenticationType)
.RequireAuthenticatedUser().Build());
});
-
Вот сложная часть: создание токена. Я не буду предоставлять весь свой код здесь, но его должно быть достаточно, чтобы воспроизвести. (У меня есть несколько несвязанных проприетарных вещей прямо вокруг этого кода в моей собственной базе кода.)
Этот бит вводится из конструктора; поэтому мы настроили параметры выше, а не просто передали их в UseOAuthBearerAuthentication()
private readonly OAuthBearerAuthenticationOptions bearerOptions;
private readonly SigningCredentials signingCredentials;
Затем в вашем действии /Token
...
// add to using clauses:
// using System.IdentityModel.Tokens.Jwt;
var handler = bearerOptions.SecurityTokenValidators.OfType<JwtSecurityTokenHandler>()
.First();
// The identity here is the ClaimsIdentity you want to authenticate the user as.
// You can add your own custom claims to it if you like.
// You can get this using the SignInManager if you're using Identity.
var securityToken = handler.CreateToken(
issuer: bearerOptions.TokenValidationParameters.ValidIssuer,
audience: bearerOptions.TokenValidationParameters.ValidAudience,
signingCredentials: signingCredentials,
subject: identity);
var token = handler.WriteToken(securityToken);
var token
- ваш токен-носитель - вы можете вернуть это как строку, которую должен пройти пользователь, как вы ожидали бы для идентификации на предъявителя.
-
Если вы частично отринули это на своей HTML-странице в сочетании с аутентификацией только на уровне носителя в .Net 4.5, вы можете теперь использовать ViewComponent
. Он в основном такой же, как и код действия контроллера выше.
Ответ 3
Чтобы добиться того, что вы описали, вам понадобится сервер авторизации OAuth2/OpenID Connect и промежуточное программное обеспечение, проверяющее токены доступа для вашего API.
Катана обычно предлагала OAuthAuthorizationServerMiddleware
, но она больше не существует в ASP.NET Core.
Я предлагаю взглянуть на AspNet.Security.OpenIdConnect.Server экспериментальную версию промежуточного программного обеспечения сервера авторизации OAuth2, которое используется в упомянутом вами учебнике: есть версия OWIN/Katana 3, и базовую версию ASP.NET, поддерживающую как net451
(.NET Desktop), так и netstandard1.4
(совместимую с .NET Core).
https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server
Не пропустите образец MVC Core, который показывает, как настроить сервер авторизации OpenID Connect с помощью AspNet.Security.OpenIdConnect.Server и как проверить зашифрованные токены доступа, выпущенные промежуточным программным обеспечением сервера: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/blob/dev/samples/Mvc/Mvc.Server/Startup.cs
Вы также можете прочитать это сообщение в блоге, в котором объясняется, как реализовать грант пароля владельца ресурса, который эквивалентен базовой аутентификации OAuth2: http://kevinchalet.com/2016/07/13/creating-your-own-openid-connect-server-with-asos-implementing-the-resource-owner-password-credentials-grant/
Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication();
}
public void Configure(IApplicationBuilder app)
{
// Add a new middleware validating the encrypted
// access tokens issued by the OIDC server.
app.UseOAuthValidation();
// Add a new middleware issuing tokens.
app.UseOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/connect/token";
// Override OnValidateTokenRequest to skip client authentication.
options.Provider.OnValidateTokenRequest = context =>
{
// Reject the token requests that don't use
// grant_type=password or grant_type=refresh_token.
if (!context.Request.IsPasswordGrantType() &&
!context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this
return Task.FromResult(0);
}
// Since there only one application and since it a public client
// (i.e a client that cannot keep its credentials private),
// call Skip() to inform the server the request should be
// accepted without enforcing client authentication.
context.Skip();
return Task.FromResult(0);
};
// Override OnHandleTokenRequest to support
// grant_type=password token requests.
options.Provider.OnHandleTokenRequest = context =>
{
// Only handle grant_type=password token requests and let the
// OpenID Connect server middleware handle the other grant types.
if (context.Request.IsPasswordGrantType())
{
// Do your credentials validation here.
// Note: you can call Reject() with a message
// to indicate that authentication failed.
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique id]");
// By default, claims are not serialized
// in the access and identity tokens.
// Use the overload taking a "destinations"
// parameter to make sure your claims
// are correctly inserted in the appropriate tokens.
identity.AddClaim("urn:customclaim", "value",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes("profile", "offline_access");
context.Validate(ticket);
}
return Task.FromResult(0);
};
});
}
}
project.json
{
"dependencies": {
"AspNet.Security.OAuth.Validation": "1.0.0",
"AspNet.Security.OpenIdConnect.Server": "1.0.0"
}
}
Удачи!
Ответ 4
Вы можете использовать OpenIddict для обслуживания токенов (вход в систему), а затем использовать UseJwtBearerAuthentication
для проверки их, когда API/контроллер доступен.
Это, по сути, вся необходимая конфигурация в Startup.cs
:
ConfigureServices:
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
// this line is added for OpenIddict to plug in
.AddOpenIddictCore<Application>(config => config.UseEntityFramework());
Настройка
app.UseOpenIddictCore(builder =>
{
// here you tell openiddict you're wanting to use jwt tokens
builder.Options.UseJwtTokens();
// NOTE: for dev consumption only! for live, this is not encouraged!
builder.Options.AllowInsecureHttp = true;
builder.Options.ApplicationCanDisplayErrors = true;
});
// use jwt bearer authentication to validate the tokens
app.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.RequireHttpsMetadata = false;
// must match the resource on your token request
options.Audience = "http://localhost:58292/";
options.Authority = "http://localhost:58292/";
});
Есть одна или две другие незначительные вещи, например, ваш DbContext должен быть получен из OpenIddictContext<ApplicationUser, Application, ApplicationRole, string>
.
Вы можете увидеть полное объяснение (включая функционирование github repo) в этом блоге:
http://capesean.co.za/blog/asp-net-5-jwt-tokens/
Ответ 5
Вы можете посмотреть образцы подключения OpenId, которые иллюстрируют, как обращаться с различными механизмами аутентификации, включая токены JWT:
https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Samples
Если вы посмотрите проект Cordova Backend, конфигурация для API будет такой:
app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")),
branch => {
branch.UseJwtBearerAuthentication(options => {
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.RequireHttpsMetadata = false;
options.Audience = "localhost:54540";
options.Authority = "localhost:54540";
});
});
Логика в /Providers/AuthorizationProvider.cs и RessourceController этого проекта также стоит взглянуть на;).
Кроме того, я реализовал одностраничное приложение с реализацией аутентификации на основе токенов, используя фреймворк Aurelia и ядро ASP.NET. Существует также постоянное соединение R-сигнала. Однако я не выполнил никакой реализации БД.
Код можно увидеть здесь:
https://github.com/alexandre-spieser/AureliaAspNetCoreAuth
Надеюсь, что это поможет,
Бест,
Алекс