Аутентифікація на основі токена в 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 для роботи з аутентифікацією на основі лексем?


Я з тим же питанням і я стругання робити все , що я, FYI є ще одне питання stackoverflow.com/questions/29055477 / ... , але ще не anserw, давайте подивимося , що станеться
Son_of_Sam


Я також стикаюся з тією ж проблемою, але поки не можу знайти рішення ... Мені потрібно написати власну автентифікацію за допомогою іншої служби, яка підтверджує мій знак.
Mayank Gupta

Відповіді:


137

Оновлення для .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);
    }

    Тут я припускаю, що ви вже маєте принципал. Якщо ви використовуєте Identity, ви можете використовувати IUserClaimsPrincipalFactory<>для перетворення вашого Userв ClaimsPrincipal.

  6. Щоб перевірити це : Отримати маркер, помістіть його в форму на jwt.io . Наведені вище інструкції також дозволяють використовувати секрет від вашого конфігурації для перевірки підпису!

  7. Якщо ви відображали це в частковому перегляді на своїй HTML-сторінці в поєднанні з аутентифікацією, що надається тільки на пред'явника ViewComponent. Це здебільшого те саме, що описаний вище код дій контролера.


1
Вам потрібно буде фактично вводити IOptions<OAuthBearerAuthenticationOptions>для введення Параметри; використання об'єкта Options безпосередньо не підтримується через іменовану конфігурацію, яка підтримується рамкою Model Options.
Метт Декре

2
Оновлено те, що я використовую, хоча зараз відповідь має переписати. Дякуємо, що тикали мене!
Метт Декрей

5
№5 було змінено на наступне в Microsoft.AspNet.Authentication.OAuthBearer - бета-версія 5 - 6 і, можливо, більш ранні бета-версії, але їх не підтвердили. auth.AddPolicy ("Носій", новий AuthorizationPolicyBuilder () .AddAuthenticationSchemes (OAuthBearerAuthenticationDefaults.AuthenticationScheme) .RequireAuthenticationUser (). build ());
dinamilynk

5
@MattDeKrey Я використовував цей відповідь в якості відправної точки для прикладу простих маркерів на основі AUTH, і оновив його на роботу проти бети 7 - см github.com/mrsheepuk/ASPNETSelfCreatedTokenAuthExample - також включає в себе кілька покажчиках з цих зауважень.
Марк Хьюз

2
Оновлено знову для RC1 - старі версії для Beta7 та Beta8 доступні у відділеннях на GitHub.
Марк Хьюз

83

Працюючи з казкової відповіді Метта Декрея , я створив повноцінний приклад аутентифікації на основі лексем, що працює проти ASP.NET Core (1.0.1). Ви можете знайти повний код в цьому репозиторії на GitHub (альтернативні гілки для 1.0.0-rc1 , beta8 , beta7 ), але коротко, важливі кроки:

Створіть ключ для вашої програми

У моєму прикладі я генерую випадковий ключ щоразу, коли програма запускається, вам потрібно буде генерувати та зберігати його десь і надавати вашій програмі. Дивіться цей файл, як я генерую випадковий ключ і як ви можете імпортувати його з .json-файлу . Як було запропоновано у коментарях @kspearrin, API захисту даних здається ідеальним кандидатом для керування ключами "правильно", але я ще не розробив, чи це можливо ще. Будь ласка, надішліть запит на витяг, якщо ви його відпрацюєте!

Startup.cs - Налаштування сервісів

Тут нам потрібно завантажити приватний ключ для наших підписаних жетонів, який ми також використаємо для перевірки маркерів під час їх представлення. Ми зберігаємо ключ у змінній рівня класу, keyяку ми повторно використовуватимемо в методі Налаштування нижче. 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 в запуску, тому нам потрібно ввести це в конструктор 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 замість beta8, все ще може знайти цей приклад в історії github
nickspoon

1
Радий, що це допомогло @nickspoon - якщо у вас з цим взагалі виникнуть якісь проблеми, повідомте мене або перейдіть на запит на виклик на github, і я оновлю!
Марк Хьюз

2
Дякую за це, однак я не зовсім розумію, чому щось, що вийшло з коробки в веб-API ASP.Net 4, вимагає зовсім небагато конфігурації в ASP.Net 5. Схоже, крок назад.
JMK

1
Я думаю, що вони дійсно підштовхують "соціальний аут" для ASP.NET 5, що, мабуть, має певний сенс, але є додатки, які не підходять, тому я не впевнений, що я згоден з їхнім напрямком @JMK
Марк Х'юз

1
@YuriyP Мені потрібно оновити цю відповідь для RC2 - я ще не оновив наш внутрішній додаток, який використовує це для RC2, тому я не впевнений, що стосується цього. Я оновлю, як тільки я розробив різницю ...
Марк Хьюз

3

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

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

Якщо ви подивитесь на проект «Корк 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...;
        });

EDIT: Я реалізував додаток на одній сторінці з реалізацією автентифікації на основі лексем, використовуючи фронтальну основу Aurelia та ядро ​​ASP.NET. Існує також сигнал R стійкого з'єднання. Однак я не робив жодної реалізації БД. Код можна побачити тут: https://github.com/alexandre-spieser/AureliaAspNetCoreAuth

Сподіваюся, це допомагає,

Найкраще,

Олексій


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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.