Token Based Authentication in ASP.NET Core (refreshed)

I'm working with ASP.NET Core application. I'm trying to implement Token Based Authentication but can not figure out how to use new Security System.

My scenario: A client requests a token. My server should authorize the user and return access_token which will be used by the client in following requests.

Here are two great articles about implementing exactly what I need:

  • Token Based Authentication using ASP.NET Web API 2, Owin, and Identity
  • Using JSON Web tokens
  • The problem is - it is not obvious for me how to do the same thing in ASP.NET Core.

    My question is: how to configure ASP.NET Core Web Api application to work with token based authentication? What direction should I pursue? Have you written any articles about the newest version, or know where I could find ones?

    Thank you!


    Working from Matt Dekrey's fabulous answer, I've created a fully working example of token-based authentication, working against ASP.NET Core (1.0.1). You can find the full code in this repository on GitHub (alternative branches for 1.0.0-rc1, beta8, beta7), but in brief, the important steps are:

    Generate a key for your application

    In my example, I generate a random key each time the app starts, you'll need to generate one and store it somewhere and provide it to your application. See this file for how I'm generating a random key and how you might import it from a .json file. As suggested in the comments by @kspearrin, the Data Protection API seems like an ideal candidate for managing the keys "correctly", but I've not worked out if that's possible yet. Please submit a pull request if you work it out!

    Startup.cs - ConfigureServices

    Here, we need to load a private key for our tokens to be signed with, which we will also use to verify tokens as they are presented. We're storing the key in a class-level variable key which we'll re-use in the Configure method below. TokenAuthOptions is a simple class which holds the signing identity, audience and issuer that we'll need in the TokenController to create our keys.

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

    We've also set up an authorization policy to allow us to use [Authorize("Bearer")] on the endpoints and classes we wish to protect.

    Startup.cs - Configure

    Here, we need to configure the 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

    In the token controller, you need to have a method to generate signed keys using the key that was loaded in Startup.cs. We've registered a TokenAuthOptions instance in Startup, so we need to inject that in the constructor for TokenController:

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

    Then you'll need to generate the token in your handler for the login endpoint, in my example I'm taking a username and password and validating those using an if statement, but the key thing you need to do is create or load a claims-based identity and generate the token for that:

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

    And that should be it. Just add [Authorize("Bearer")] to any method or class you want to protect, and you should get an error if you attempt to access it without a token present. If you want to return a 401 instead of a 500 error, you'll need to register a custom exception handler as I have in my example here.


  • Generate an RSA key just for your application. A very basic example is below, but there's lots of information about how security keys are handled in the .Net Framework; I highly recommend that you go read some of it, at least.

    private static string GenerateRsaKeys()
    {
        RSACryptoServiceProvider myRSA = new RSACryptoServiceProvider(2048);
        RSAParameters publicKey = myRSA.ExportParameters(true);
        return myRSA.ToXmlString(includePrivateParameters: true);
    }
    

    Save this out to a .xml file and include it with your application; I embedded it in my DLL because it's a small personal project I figured that no one should get access to my assembly anyway, but there's lots of reasons why this is not a good idea and so I am not providing that example here. Ultimately, you have to decide what is best for your project.

    Note: It was pointed out that the ToXmlString and FromXmlString are not available in .NET Core. Instead, you can save/load the values yourself using RSAParameters ExportParameters(bool includePrivateParameters) and void ImportParameters(RSAParameters parameters) in a Core-compliant way, such as using JSON.

  • Create a few constants that we'll be using later; here's what I did:

    const string TokenAudience = "Myself";
    const string TokenIssuer = "MyProject";
    
  • Add this to your Startup.cs's ConfigureServices . We'll use dependency injection later to access these settings. I'm leaving out accessing the RSA xml stream; but I'm assuming you have access to it in a stream variable.

    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;
    });
    
  • Set up Bearer Authentication. If you're using Identity, do this before the UseIdentity line. Note that any third-party authentication lines, such as UseGoogleAuthentication , must go before the UseIdentity line. You do not need any UseCookieAuthentication if you are using Identity.

    app.UseOAuthBearerAuthentication();
    
  • You may want to specify an AuthorizationPolicy . This will allow you to specify controllers and actions that only allow Bearer tokens as authentication using [Authorize("Bearer")] .

    services.ConfigureAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationTypes(OAuthBearerAuthenticationDefaults.AuthenticationType)
            .RequireAuthenticatedUser().Build());
    });
    
  • Here comes the tricky part: building the token. I'm not going to provide all my code here, but it should be enough to reproduce. (I have a few unrelated proprietary things right around this code in my own codebase.)

    This bit is injected from the constructor; this is why we configured the options above rather than simply passing them to the UseOAuthBearerAuthentication()

    private readonly OAuthBearerAuthenticationOptions bearerOptions;
    private readonly SigningCredentials signingCredentials;
    

    Then, in your /Token action...

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

    The var token is your bearer token - you can return this as a string to the user to pass as you'd expect for Bearer authentication.

  • If you were rendering this in a partial view on your HTML page in combination with the bearer-only authentication in .Net 4.5, you can now use a ViewComponent to do the same. It's mostly the same as the Controller Action code above.


  • To achieve what you describe, you'll need both an OAuth2/OpenID Connect authorization server and a middleware validating access tokens for your API. Katana used to offer an OAuthAuthorizationServerMiddleware , but it doesn't exist anymore in ASP.NET Core.

    I suggest having a look to AspNet.Security.OpenIdConnect.Server , an experimental fork of the OAuth2 authorization server middleware which is used by the tutorial you mentioned: there's an OWIN/Katana 3 version, and an ASP.NET Core version that supports both net451 (.NET Desktop) and netstandard1.4 (compatible with .NET Core).

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

    Don't miss the MVC Core sample that shows how to configure an OpenID Connect authorization server using AspNet.Security.OpenIdConnect.Server and how to validate the encrypted access tokens issued by the server middleware: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/blob/dev/samples/Mvc/Mvc.Server/Startup.cs

    You can also read this blog post, that explains how to implement the resource owner password grant, which is the OAuth2 equivalent of basic authentication: 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's only one application and since it's 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"
      }
    }
    

    Good luck!

    链接地址: http://www.djcxy.com/p/3810.html

    上一篇: ASP.NET Core 1.0 Web API中的简单JWT身份验证

    下一篇: ASP.NET Core中基于令牌的身份验证(刷新)