Аутентификация на основе токенов в ядре 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

Надеюсь, что это поможет,

Бест,

Алекс