Xác thực dựa trên mã thông báo trong ASP.NET Core


161

Tôi đang làm việc với ứng dụng ASP.NET Core. Tôi đang cố gắng triển khai Xác thực dựa trên mã thông báo nhưng không thể tìm ra cách sử dụng Hệ thống bảo mật mới cho trường hợp của tôi. Tôi đã xem qua các ví dụ nhưng họ không giúp tôi nhiều, họ đang sử dụng xác thực cookie hoặc xác thực bên ngoài (GitHub, Microsoft, Twitter).

Kịch bản của tôi là gì: ứng dụng angularjs nên yêu cầu /tokenurl chuyển tên người dùng và mật khẩu. WebApi nên ủy quyền cho người dùng và trả lại access_tokensẽ được ứng dụng angularjs sử dụng trong các yêu cầu sau.

Tôi đã tìm thấy bài viết tuyệt vời về việc triển khai chính xác những gì tôi cần trong phiên bản hiện tại của ASP.NET - Xác thực dựa trên mã thông báo bằng cách sử dụng ASP.NET Web API 2, Owin và Nhận dạng . Nhưng nó không rõ ràng đối với tôi làm thế nào để làm điều tương tự trong ASP.NET Core.

Câu hỏi của tôi là: làm cách nào để định cấu hình ứng dụng ASP.NET Core WebApi để hoạt động với xác thực dựa trên mã thông báo?


Tôi có cùng một vấn đề và tôi đã lên kế hoạch tự mình làm tất cả, FYI có một câu hỏi khác stackoverflow.com/questions/29055477/ Khăn nhưng chưa có câu trả lời nào, hãy xem điều gì xảy ra
Son_of_Sam


Tôi cũng đang đối mặt với vấn đề tương tự nhưng chưa thể tìm ra giải pháp ... Tôi cần viết một xác thực tùy chỉnh bằng một dịch vụ khác xác thực Mã thông báo của tôi.
Mayank Gupta

Câu trả lời:


137

Cập nhật cho .Net Core 3.1:

David Fowler (kiến trúc sư cho nhóm ASP .NET Core) đã tập hợp một bộ ứng dụng tác vụ cực kỳ đơn giản, bao gồm một ứng dụng đơn giản thể hiện JWT . Tôi sẽ sớm kết hợp các cập nhật và phong cách đơn giản của anh ấy vào bài đăng này.

Đã cập nhật cho .Net Core 2:

Các phiên bản trước của câu trả lời này đã sử dụng RSA; thực sự không cần thiết nếu cùng mã của bạn đang tạo mã thông báo cũng đang xác minh mã thông báo. Tuy nhiên, nếu bạn đang phân phối trách nhiệm, có lẽ bạn vẫn muốn thực hiện việc này bằng cách sử dụng một thể hiện của Microsoft.IdentityModel.Tokens.RsaSecurityKey.

  1. Tạo một vài hằng số mà chúng ta sẽ sử dụng sau này; đây là những gì tôi đã làm:

    const string TokenAudience = "Myself";
    const string TokenIssuer = "MyProject";
    
  2. Thêm phần này vào Startup.cs của bạn ConfigureServices. Chúng tôi sẽ sử dụng nội xạ phụ thuộc sau để truy cập các cài đặt này. Tôi giả định rằng bạn authenticationConfigurationlà một ConfigurationSectionhoặc một Configurationđối tượng để bạn có thể có một cấu hình khác để gỡ lỗi và sản xuất. Hãy chắc chắn rằng bạn lưu trữ chìa khóa của bạn một cách an toàn! Nó có thể là bất kỳ chuỗi.

    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;
        });
    

    Tôi đã thấy các câu trả lời khác thay đổi các cài đặt khác, như ClockSkew; mặc định được đặt sao cho phù hợp với môi trường phân tán có đồng hồ không chính xác. Đây là những cài đặt duy nhất bạn cần thay đổi.

  3. Thiết lập Xác thực. Bạn nên có dòng này trước bất kỳ phần mềm trung gian nào yêu cầu Userthông tin của bạn , chẳng hạn như app.UseMvc().

    app.UseAuthentication();

    Lưu ý rằng điều này sẽ không làm cho mã thông báo của bạn được phát ra cùng với SignInManagerhoặc bất cứ thứ gì khác. Bạn sẽ cần cung cấp cơ chế của riêng mình để xuất JWT của bạn - xem bên dưới.

  4. Bạn có thể muốn chỉ định một AuthorizationPolicy. Điều này sẽ cho phép bạn chỉ định các bộ điều khiển và hành động chỉ cho phép mã thông báo Bearer làm xác thực bằng cách sử dụng [Authorize("Bearer")].

    services.AddAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationTypes(JwtBearerDefaults.AuthenticationType)
            .RequireAuthenticatedUser().Build());
    });
  5. Ở đây có phần khó khăn: xây dựng mã thông báo.

    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);
        }
    }

    Sau đó, trong bộ điều khiển của bạn nơi bạn muốn mã thông báo của mình, đại loại như sau:

    [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);
    }

    Ở đây, tôi cho rằng bạn đã có một hiệu trưởng. Nếu bạn đang sử dụng Danh tính, bạn có thể sử dụng IUserClaimsPrincipalFactory<>để chuyển đổi Userthành một ClaimsPrincipal.

  6. Để kiểm tra nó : Nhận một mã thông báo, đặt nó vào biểu mẫu ở jwt.io . Các hướng dẫn tôi cung cấp ở trên cũng cho phép bạn sử dụng bí mật từ cấu hình của mình để xác thực chữ ký!

  7. Nếu bạn đã hiển thị điều này trong chế độ xem một phần trên trang HTML của mình kết hợp với xác thực chỉ mang theo trong .Net 4.5, thì bây giờ bạn có thể sử dụng một ViewComponentđể làm tương tự. Nó chủ yếu giống như mã Hành động điều khiển ở trên.


1
Bạn sẽ cần phải thực sự tiêm IOptions<OAuthBearerAuthenticationOptions>để sử dụng Tùy chọn; sử dụng trực tiếp một đối tượng Tùy chọn không được hỗ trợ do cấu hình được đặt tên được hỗ trợ bởi khung Mô hình Tùy chọn.
Matt DeKrey

2
Đã cập nhật những gì tôi đang sử dụng, mặc dù bây giờ câu trả lời sẽ được viết lại. Cảm ơn đã chọc tôi!
Matt DeKrey

5
Số 5 đã được thay đổi thành như sau trong Microsoft.AspNet.Authentication.OAuthBearer - beta 5 - 6 và có thể là betas trước đó nhưng chưa xác nhận những điều đó. auth.AddPolicy ("Bearer", AuthorizationPolicyBuilder () .AddAuthenticationScheme (OAuthBearerAuthenticationDefaults.AuthenticationScheme) .RequireAuthenticatedUser (). Build ();
Dynamiclynk

5
@MattDeKrey Tôi đã sử dụng câu trả lời này làm điểm khởi đầu cho một ví dụ về xác thực dựa trên mã thông báo đơn giản và đã cập nhật nó để hoạt động với phiên bản beta 7 - xem github.com/mrsheepuk/ASPNETSelfCreatedTokenAuthExample - cũng kết hợp một vài gợi ý từ những nhận xét này.
Mark Hughes

2
Được cập nhật lại cho RC1 - phiên bản cũ cho Beta7Beta8 có sẵn trong các chi nhánh trên GitHub.
Mark Hughes

83

Hoạt động từ câu trả lời tuyệt vời của Matt Dekrey , tôi đã tạo ra một ví dụ hoạt động đầy đủ về xác thực dựa trên mã thông báo, hoạt động chống lại ASP.NET Core (1.0.1). Bạn có thể tìm thấy mã đầy đủ trong kho lưu trữ này trên GitHub (các nhánh thay thế cho 1.0.0-rc1 , beta8 , beta7 ), nhưng tóm lại, các bước quan trọng là:

Tạo khóa cho ứng dụng của bạn

Trong ví dụ của tôi, tôi tạo một khóa ngẫu nhiên mỗi khi ứng dụng khởi động, bạn sẽ cần tạo một khóa và lưu trữ ở đâu đó và cung cấp cho ứng dụng của bạn. Xem tệp này để biết cách tôi tạo khóa ngẫu nhiên và cách bạn có thể nhập tệp từ tệp .json . Như được đề xuất trong các nhận xét của @kspearrin, API Bảo vệ Dữ liệu có vẻ như là một ứng cử viên lý tưởng để quản lý các khóa "chính xác", nhưng tôi vẫn chưa làm việc nếu điều đó là có thể. Vui lòng gửi yêu cầu kéo nếu bạn giải quyết nó!

Startup.cs - ConfigureService

Tại đây, chúng tôi cần tải một khóa riêng cho các mã thông báo của chúng tôi được ký kết, chúng tôi cũng sẽ sử dụng để xác minh mã thông báo khi chúng được trình bày. Chúng tôi đang lưu trữ khóa trong một biến cấp độ lớp keymà chúng tôi sẽ sử dụng lại trong phương thức Cấu hình bên dưới. TokenAuthOptions là một lớp đơn giản chứa danh tính, đối tượng và nhà phát hành ký mà chúng ta sẽ cần trong TokenContoder để tạo các khóa của chúng tôi.

// 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());
});

Chúng tôi cũng đã thiết lập một chính sách ủy quyền để cho phép chúng tôi sử dụng [Authorize("Bearer")]trên các điểm cuối và các lớp mà chúng tôi muốn bảo vệ.

Startup.cs - Cấu hình

Ở đây, chúng ta cần cấu hình 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)
    }
});

Mã thông báo

Trong bộ điều khiển mã thông báo, bạn cần có một phương thức để tạo các khóa đã ký bằng cách sử dụng khóa được tải trong Startup.cs. Chúng tôi đã đăng ký một phiên bản TokenAuthOptions trong Khởi động, vì vậy chúng tôi cần phải thêm nó vào hàm tạo cho TokenContoder:

[Route("api/[controller]")]
public class TokenController : Controller
{
    private readonly TokenAuthOptions tokenOptions;

    public TokenController(TokenAuthOptions tokenOptions)
    {
        this.tokenOptions = tokenOptions;
    }
...

Sau đó, bạn sẽ cần tạo mã thông báo trong trình xử lý của mình cho điểm cuối đăng nhập, trong ví dụ của tôi, tôi đang lấy tên người dùng và mật khẩu và xác thực những người sử dụng câu lệnh if, nhưng điều quan trọng bạn cần làm là tạo hoặc tải khiếu nại nhận dạng dựa trên và tạo mã thông báo cho điều đó:

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);
}

Và đó nên là nó. Chỉ cần thêm [Authorize("Bearer")]vào bất kỳ phương thức hoặc lớp nào bạn muốn bảo vệ và bạn sẽ gặp lỗi nếu bạn cố truy cập mà không có mã thông báo. Nếu bạn muốn trả về một lỗi thay vì 500, bạn sẽ cần phải đăng ký một trình xử lý ngoại lệ tùy chỉnh như trong ví dụ của tôi ở đây .


1
Đây là một ví dụ thực sự xuất sắc và bao gồm tất cả các phần còn thiếu tôi cần để làm ví dụ của @ MattDeKrey hoạt động, cảm ơn rất nhiều! Lưu ý rằng bất kỳ ai vẫn nhắm mục tiêu beta7 thay vì beta8 vẫn có thể tìm thấy ví dụ đó trong lịch sử github
nickspoon

1
Rất vui vì nó đã giúp @nickspoon - nếu bạn có bất kỳ vấn đề nào với nó, hãy cho tôi biết hoặc bật trong yêu cầu kéo trên github và tôi sẽ cập nhật!
Mark Hughes

2
Cảm ơn vì điều này, tuy nhiên tôi không hiểu tại sao một cái gì đó hoạt động tốt trong API Web của ASP.Net 4 lại yêu cầu khá nhiều cấu hình trong ASP.Net 5. Có vẻ như là một bước lùi.
JMK

1
Tôi nghĩ rằng họ thực sự đang thúc đẩy "xác thực xã hội" cho ASP.NET 5, điều này có nghĩa là tôi cho rằng, nhưng có những ứng dụng không phù hợp nên tôi không chắc là tôi đồng ý với hướng đi của họ @JMK
Mark Hughes

1
@YuriyP Tôi cần cập nhật câu trả lời này cho RC2 - Tôi chưa cập nhật ứng dụng nội bộ của chúng tôi sử dụng ứng dụng này cho RC2 vì vậy tôi không chắc chắn những gì liên quan. Tôi sẽ cập nhật một khi tôi đã tìm ra sự khác biệt ...
Mark Hughes

3

Bạn có thể xem các mẫu kết nối OpenId minh họa cách xử lý các cơ chế xác thực khác nhau, bao gồm Mã thông báo JWT:

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

Nếu bạn xem dự án Cordova Backend, cấu hình cho API giống như sau:

           // 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"
            });
        });

Logic trong /Providers/AuthorizationProvider.cs và RessourceControll của dự án đó cũng đáng để xem qua;).

Ngoài ra, bạn cũng có thể sử dụng mã sau để xác thực mã thông báo (cũng có một đoạn mã để làm cho nó hoạt động với 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);
                }
            };
        });

Để phát hành mã thông báo, bạn có thể sử dụng các gói máy chủ openId Connect như vậy:

        // 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: Tôi đã triển khai một ứng dụng một trang với triển khai xác thực dựa trên mã thông báo bằng cách sử dụng khung công tác mặt trước Aurelia và lõi ASP.NET. Ngoài ra còn có một tín hiệu R kết nối liên tục. Tuy nhiên tôi chưa thực hiện bất kỳ triển khai DB nào. Mã có thể được nhìn thấy ở đây: https://github.com/alexandre-spieser/AureliaAspNetCoreAuth

Hi vọng điêu nay co ich,

Tốt,

Alex


1

Hãy xem OpenIddict - đó là một dự án mới (tại thời điểm viết bài) giúp dễ dàng định cấu hình việc tạo mã thông báo JWT và làm mới mã thông báo trong ASP.NET 5. Việc xác thực mã thông báo được xử lý bởi phần mềm khác.

Giả sử bạn sử dụng Identityvới Entity Framework, dòng cuối cùng là những gì bạn thêm vào ConfigureServicesphương thức của mình :

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

Trong Configure, bạn thiết lập OpenIddict để phục vụ mã thông báo 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;
});

Bạn cũng định cấu hình xác thực mã thông báo trong 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/";
});

Có một hoặc hai điều nhỏ khác, chẳng hạn như DbContext của bạn cần xuất phát từ OpenIddictContext.

Bạn có thể xem giải thích đầy đủ về bài đăng trên blog này: http://capesean.co.za/blog/asp-net-5-jwt-tokens/

Một bản demo chức năng có sẵn tại: https://github.com/capesean/openiddict-test

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.