Проверка подлинности на основе токенов в ASP.NET Core

161

Я работаю с приложением ASP.NET Core. Я пытаюсь реализовать аутентификацию на основе токенов, но не могу понять, как использовать новую систему безопасности для моего случая. Я просмотрел примеры, но они мне мало чем помогли, они используют либо аутентификацию по cookie, либо внешнюю аутентификацию (GitHub, Microsoft, Twitter).

Мой сценарий таков: приложение angularjs должно запрашивать /tokenURL, передавая имя пользователя и пароль. WebApi должен авторизовать пользователя и access_tokenвыполнить возврат, который будет использоваться приложением angularjs в следующих запросах.

Я нашел отличную статью о реализации именно того, что мне нужно в текущей версии ASP.NET - аутентификация на основе токенов с использованием ASP.NET Web API 2, Owin и Identity . Но для меня не очевидно, как сделать то же самое в ASP.NET Core.

У меня вопрос: как настроить приложение ASP.NET Core WebApi для работы с аутентификацией на основе токенов?

Грант
источник
У меня возникла та же проблема, и я планировал сделать все это сам, к вашему сведению, есть еще один вопрос stackoverflow.com/questions/29055477/… но пока нет ответа, посмотрим, что произойдет
Son_of_Sam
stackoverflow.com/a/29144031/1659846
Андрей Некулай
Я также сталкиваюсь с той же проблемой, но пока не могу найти решение ... Мне нужно написать собственную аутентификацию, используя другой сервис, который аутентифицирует мой токен.
Маянк Гупта

Ответы:

138

Обновление для .Net Core 3.1:

Дэвид Фаулер (архитектор группы ASP .NET Core) собрал невероятно простой набор приложений для задач, включая простое приложение, демонстрирующее JWT . Я скоро включу его обновления и упрощенный стиль в этот пост.

Обновлено для .Net Core 2:

Предыдущие версии этого ответа использовали RSA; это действительно не нужно, если ваш код, который генерирует токены, также проверяет токены. Однако, если вы распределяете ответственность, вы, вероятно, все еще хотите сделать это, используя экземпляр Microsoft.IdentityModel.Tokens.RsaSecurityKey.

  1. Создайте несколько констант, которые мы будем использовать позже; вот что я сделал:

    const string TokenAudience = "Myself";
    const string TokenIssuer = "MyProject";
  2. Добавьте это к вашему Startup.cs ConfigureServices. Мы будем использовать внедрение зависимостей позже для доступа к этим настройкам. Я предполагаю, что вы authenticationConfiguration- объект ConfigurationSectionили Configurationобъект, такой, что у вас может быть другая конфигурация для отладки и производства. Убедитесь, что вы храните свой ключ надежно! Это может быть любая строка.

    var keySecret = authenticationConfiguration["JwtSigningKey"];
    var symmetricKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(keySecret));
    
    services.AddTransient(_ => new JwtSignInHandler(symmetricKey));
    
    services.AddAuthentication(options =>
    {
        // This causes the default authentication scheme to be JWT.
        // Without this, the Authorization header is not checked and
        // you'll get no results. However, this also means that if
        // you're already using cookies in your app, they won't be 
        // checked by default.
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    })
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters.ValidateIssuerSigningKey = true;
            options.TokenValidationParameters.IssuerSigningKey = symmetricKey;
            options.TokenValidationParameters.ValidAudience = JwtSignInHandler.TokenAudience;
            options.TokenValidationParameters.ValidIssuer = JwtSignInHandler.TokenIssuer;
        });

    Я видел, как другие ответы меняли другие настройки, такие как ClockSkew; Значения по умолчанию установлены так, что они должны работать для распределенных сред, часы которых не синхронизированы. Это единственные настройки, которые вам нужно изменить.

  3. Настройте аутентификацию. Вы должны иметь эту строку перед любым промежуточным ПО, которое требует вашей Userинформации, например app.UseMvc().

    app.UseAuthentication();

    Обратите внимание, что это не приведет к тому, что ваш токен будет выдан вместе с SignInManagerчем-либо еще. Вам нужно будет предоставить собственный механизм вывода вашего JWT - см. Ниже.

  4. Вы можете указать AuthorizationPolicy. Это позволит вам указать контроллеры и действия, которые разрешают использование только токенов Bearer в качестве аутентификации [Authorize("Bearer")].

    services.AddAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationTypes(JwtBearerDefaults.AuthenticationType)
            .RequireAuthenticatedUser().Build());
    });
  5. Здесь начинается сложная часть: создание токена.

    class JwtSignInHandler
    {
        public const string TokenAudience = "Myself";
        public const string TokenIssuer = "MyProject";
        private readonly SymmetricSecurityKey key;
    
        public JwtSignInHandler(SymmetricSecurityKey symmetricKey)
        {
            this.key = symmetricKey;
        }
    
        public string BuildJwt(ClaimsPrincipal principal)
        {
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    
            var token = new JwtSecurityToken(
                issuer: TokenIssuer,
                audience: TokenAudience,
                claims: principal.Claims,
                expires: DateTime.Now.AddMinutes(20),
                signingCredentials: creds
            );
    
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }

    Затем в вашем контроллере, где вы хотите свой токен, что-то вроде следующего:

    [HttpPost]
    public string AnonymousSignIn([FromServices] JwtSignInHandler tokenFactory)
    {
        var principal = new System.Security.Claims.ClaimsPrincipal(new[]
        {
            new System.Security.Claims.ClaimsIdentity(new[]
            {
                new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, "Demo User")
            })
        });
        return tokenFactory.BuildJwt(principal);
    }

    Здесь, я полагаю, у вас уже есть директор. Если вы используете личность, вы можете использовать IUserClaimsPrincipalFactory<>для преобразования Userв ClaimsPrincipal.

  6. Чтобы проверить это : Получить маркер, поместите его в форму на jwt.io . Инструкции, которые я предоставил выше, также позволяют вам использовать секрет из вашей конфигурации для проверки подписи!

  7. Если вы представляли это в виде частичного представления на своей HTML-странице в сочетании с аутентификацией только на предъявителя в .Net 4.5, теперь вы можете использовать a, ViewComponentчтобы сделать то же самое. Это в основном так же, как код действия контроллера выше.

Мэтт ДеКрей
источник
1
Вы должны будете фактически ввести, IOptions<OAuthBearerAuthenticationOptions>чтобы использовать Опции; прямое использование объекта параметров не поддерживается из-за именованной конфигурации, поддерживаемой платформой модели параметров.
Мэтт ДеКрей
2
Обновлено до того, что я использую, хотя теперь ответ должен получить переписать. Спасибо, что тыкаешь в меня!
Мэтт ДеКрей
5
С тех пор # 5 было изменено на следующее в Microsoft.AspNet.Authentication.OAuthBearer - бета 5 - 6 и, возможно, более ранние бета-версии, но не подтвердили их. auth.AddPolicy («Носитель», новый AuthorizationPolicyBuilder () .AddAuthenticationSchemes (OAuthBearerAuthenticationDefaults.AuthenticationScheme) .RequireAuthenticatedUser (). Build ());
dynamiclynk
5
@MattDeKrey Я использовал этот ответ в качестве отправной точки для примера простой аутентификации на основе токенов и обновил его для работы с бета-версией 7 - см. Github.com/mrsheepuk/ASPNETSelfCreatedTokenAuthExample - также включает в себя несколько указателей из этих комментариев.
Марк Хьюз,
2
Обновлен снова для RC1 - старые версии для Beta7 и Beta8 доступны в ветках на GitHub.
Марк Хьюз
83

Работая сказочного ответ Matt Dekrey в , я создал полностью рабочий пример аутентификации на основе маркеров, работающий на ASP.NET Ядро (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 - настроить

Здесь нам нужно настроить аутентификацию JwtBearer:

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 в автозагрузке, поэтому нам нужно добавить его в конструктор 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, вам нужно зарегистрировать пользовательский обработчик исключений, как в моем примере здесь .

Марк Хьюз
источник
1
Это действительно отличный пример, и он включает в себя все недостающие части, которые мне нужны, чтобы пример @ MattDeKrey работал, большое спасибо! Обратите внимание, что любой, кто все еще нацеливается на бета7 вместо бета8, может найти этот пример в истории github
nickspoon
1
Рад, что это помогло @nickspoon - если у вас есть какие-либо проблемы с этим, дайте мне знать или вставьте запрос на github, и я обновлю!
Марк Хьюз
2
Спасибо за это, однако я не совсем понимаю, почему то, что работало из коробки в ASP.Net 4 Web API, теперь требует немало настроек в ASP.Net 5. Это похоже на шаг назад.
JMK
1
Я думаю , что они на самом деле толкают «социальные» AUTH для ASP.NET 5, что делает какой - то смысл , я полагаю, но есть приложения , которые не подходят для так что я не уверен , что я согласен с их направлением @JMK
Марк Хьюз
1
@YuriyP Мне нужно обновить этот ответ для RC2 - я еще не обновил наше внутреннее приложение, которое использует это для RC2, поэтому я не уверен, что в этом замешано. Я обновлю, как только разберусь с разницей ...
Марк Хьюз
3

Вы можете взглянуть на примеры подключения OpenId, которые иллюстрируют, как работать с различными механизмами аутентификации, включая токены JWT:

https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Samples

Если вы посмотрите на проект Cordova Backend, конфигурация для API выглядит следующим образом:

           // Create a new branch where the registered middleware will be executed only for non API calls.
        app.UseWhen(context => !context.Request.Path.StartsWithSegments(new PathString("/api")), branch => {
            // Insert a new cookies middleware in the pipeline to store
            // the user identity returned by the external identity provider.
            branch.UseCookieAuthentication(new CookieAuthenticationOptions {
                AutomaticAuthenticate = true,
                AutomaticChallenge = true,
                AuthenticationScheme = "ServerCookie",
                CookieName = CookieAuthenticationDefaults.CookiePrefix + "ServerCookie",
                ExpireTimeSpan = TimeSpan.FromMinutes(5),
                LoginPath = new PathString("/signin"),
                LogoutPath = new PathString("/signout")
            });

            branch.UseGoogleAuthentication(new GoogleOptions {
                ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com",
                ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f"
            });

            branch.UseTwitterAuthentication(new TwitterOptions {
                ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g",
                ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI"
            });
        });

Также стоит взглянуть на логику в /Providers/AuthorizationProvider.cs и RessourceController этого проекта;).

В качестве альтернативы вы также можете использовать следующий код для проверки токенов (также есть фрагмент, чтобы заставить его работать с signalR):

        // Add a new middleware validating access tokens.
        app.UseOAuthValidation(options =>
        {
            // Automatic authentication must be enabled
            // for SignalR to receive the access token.
            options.AutomaticAuthenticate = true;

            options.Events = new OAuthValidationEvents
            {
                // Note: for SignalR connections, the default Authorization header does not work,
                // because the WebSockets JS API doesn't allow setting custom parameters.
                // To work around this limitation, the access token is retrieved from the query string.
                OnRetrieveToken = context =>
                {
                    // Note: when the token is missing from the query string,
                    // context.Token is null and the JWT bearer middleware will
                    // automatically try to retrieve it from the Authorization header.
                    context.Token = context.Request.Query["access_token"];

                    return Task.FromResult(0);
                }
            };
        });

Для выдачи токена вы можете использовать пакеты сервера openId Connect следующим образом:

        // Add a new middleware issuing access tokens.
        app.UseOpenIdConnectServer(options =>
        {
            options.Provider = new AuthenticationProvider();
            // Enable the authorization, logout, token and userinfo endpoints.
            //options.AuthorizationEndpointPath = "/connect/authorize";
            //options.LogoutEndpointPath = "/connect/logout";
            options.TokenEndpointPath = "/connect/token";
            //options.UserinfoEndpointPath = "/connect/userinfo";

            // Note: if you don't explicitly register a signing key, one is automatically generated and
            // persisted on the disk. If the key cannot be persisted, an exception is thrown.
            // 
            // On production, using a X.509 certificate stored in the machine store is recommended.
            // You can generate a self-signed certificate using Pluralsight's self-cert utility:
            // https://s3.amazonaws.com/pluralsight-free/keith-brown/samples/SelfCert.zip
            // 
            // options.SigningCredentials.AddCertificate("7D2A741FE34CC2C7369237A5F2078988E17A6A75");
            // 
            // Alternatively, you can also store the certificate as an embedded .pfx resource
            // directly in this assembly or in a file published alongside this project:
            // 
            // options.SigningCredentials.AddCertificate(
            //     assembly: typeof(Startup).GetTypeInfo().Assembly,
            //     resource: "Nancy.Server.Certificate.pfx",
            //     password: "Owin.Security.OpenIdConnect.Server");

            // Note: see AuthorizationController.cs for more
            // information concerning ApplicationCanDisplayErrors.
            options.ApplicationCanDisplayErrors = true // in dev only ...;
            options.AllowInsecureHttp = true // in dev only...;
        });

РЕДАКТИРОВАТЬ: я реализовал одностраничное приложение с аутентификацией на основе токенов, используя интерфейсную среду Aurelia и ядро ​​ASP.NET. Есть также сигнал R постоянного соединения. Однако я не сделал никакой реализации БД. Код можно посмотреть здесь: https://github.com/alexandre-spieser/AureliaAspNetCoreAuth

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

Лучший,

Alex

Darxtar
источник
1

Взгляните на OpenIddict - это новый проект (на момент написания), который позволяет легко настроить создание токенов JWT и обновить токены в ASP.NET 5. Проверка токенов выполняется другим программным обеспечением.

Предполагая, что вы используете Identityс Entity Framework, последняя строка - это то, что вы добавили бы в свой ConfigureServicesметод:

services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    .AddOpenIddictCore<Application>(config => config.UseEntityFramework());

В Configure, вы настроили OpenIddict для обслуживания токенов JWT:

app.UseOpenIddictCore(builder =>
{
    // 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;
});

Вы также настраиваете проверку токенов в Configure:

// use jwt bearer authentication
app.UseJwtBearerAuthentication(options =>
{
    options.AutomaticAuthenticate = true;
    options.AutomaticChallenge = true;
    options.RequireHttpsMetadata = false;
    options.Audience = "http://localhost:58292/";
    options.Authority = "http://localhost:58292/";
});

Есть одна или две второстепенные вещи, например, ваш DbContext должен быть получен из OpenIddictContext.

Вы можете увидеть подробное объяснение в этом блоге: http://capesean.co.za/blog/asp-net-5-jwt-tokens/

Функциональная демонстрация доступна по адресу: https://github.com/capesean/openiddict-test

Шон
источник