Token Based Authentication in ASP.NET Core (refreshed)

55,398

Solution 1

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.

Solution 2

This is really a duplicate of another answer of mine, which I tend to keep more up-to-date as it gets more attention. Comments there may also be useful to you!

Updated for .Net Core 2:

Previous versions of this answer used RSA; it's really not necessary if your same code that is generating the tokens is also verifying the tokens. However, if you're distributing the responsibility, you probably still want to do this using an instance of Microsoft.IdentityModel.Tokens.RsaSecurityKey.

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

    const string TokenAudience = "Myself";
    const string TokenIssuer = "MyProject";
    
  2. Add this to your Startup.cs's ConfigureServices. We'll use dependency injection later to access these settings. I'm assuming that your authenticationConfiguration is a ConfigurationSection or Configuration object such that you can have a different config for debug and production. Make sure you store your key securely! It can be any string.

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

    I've seen other answers change other settings, such as ClockSkew; the defaults are set such that it should work for distributed environments whose clocks aren't exactly in sync. These are the only settings you need to change.

  3. Set up Authentication. You should have this line before any middleware that requires your User info, such as app.UseMvc().

    app.UseAuthentication();
    

    Note that this will not cause your token to be emitted with the SignInManager or anything else. You will need to provide your own mechanism for outputting your JWT - see below.

  4. 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.AddAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationTypes(JwtBearerDefaults.AuthenticationType)
            .RequireAuthenticatedUser().Build());
    });
    
  5. Here comes the tricky part: building the token.

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

    Then, in your controller where you want your token, something like the following:

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

    Here, I'm assuming you already have a principal. If you are using Identity, you can use IUserClaimsPrincipalFactory<> to transform your User into a ClaimsPrincipal.

  6. To test it: Get a token, put it into the form at jwt.io. The instructions I provided above also allow you to use the secret from your config to validate the signature!

  7. 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.

Solution 3

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!

Solution 4

You can use OpenIddict to serve the tokens (logging in) and then use UseJwtBearerAuthentication to validate them when an API/Controller is accessed.

This is essentially all the configuration you need in Startup.cs:

ConfigureServices:

services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    // this line is added for OpenIddict to plug in
    .AddOpenIddictCore<Application>(config => config.UseEntityFramework());

Configure

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

There are one or two other minor things, such as your DbContext needs to derive from OpenIddictContext<ApplicationUser, Application, ApplicationRole, string>.

You can see a full length explanation (including the functioning github repo) on this blog post of mine: http://capesean.co.za/blog/asp-net-5-jwt-tokens/

Solution 5

You can have a look at the OpenId connect samples which illustrate how to deal with different authentication mechanisms, including JWT Tokens:

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

If you look at the Cordova Backend project, the configuration for the API is like so:

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

The logic in /Providers/AuthorizationProvider.cs and the RessourceController of that project are also worth having a look at ;).

Moreover, I have implemented a single page application with token based authentication implementation using the Aurelia front end framework and ASP.NET core. There is also a signal R persistent connection. However I have not done any DB implementation. Code can be seen here: https://github.com/alexandre-spieser/AureliaAspNetCoreAuth

Hope this helps,

Best,

Alex

Share:
55,398
Patryk Golebiowski
Author by

Patryk Golebiowski

Updated on July 05, 2022

Comments

  • Patryk Golebiowski
    Patryk Golebiowski almost 2 years

    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:

    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!

  • Patryk Golebiowski
    Patryk Golebiowski almost 9 years
    Thank you very much for your answer! I am just wondering - what do you think about signing my own strings with HMAC-SHA256 and releasing such tokens? I am just wondering if this is a secure enough solution :)
  • Matt DeKrey
    Matt DeKrey almost 9 years
    I'm by no means a security expert - and a comment box wouldn't have enough space for me to leave a thorough explanation. It really depends on your use-case, but I believe the old ASP.Net used the machine key, which, iirc, was usually SHA256 when people customized it.
  • Kévin Chalet
    Kévin Chalet almost 9 years
    @MattDeKrey note that RSACryptoServiceProvider.ToXmlString and RSACryptoServiceProvider.FromXmlString haven't been ported to CoreCLR. This means that you won't be able to target dnxcore50 when using these methods.
  • Kévin Chalet
    Kévin Chalet almost 9 years
    @Randolph using a symmetric algorithm to sign your access tokens is not recommended if the resource server (aka your "API") and the authorization server (the component that creates the tokens) are not part of the same application. You should really use RSA-SHA512, as suggested by Matt.
  • Kévin Chalet
    Kévin Chalet almost 9 years
    @Randolph one last remark: if you plan to support external clients (i.e clients that you don't own), you should REALLY consider adopting a standard protocol like OAuth2 or OpenID Connect, instead of creating your own endpoint. See my answer if you need more information.
  • zoranpro
    zoranpro over 8 years
    Is it possible instead of adding [Authorize("Bearer")] to put only [Authorize] ?
  • Mark Hughes
    Mark Hughes over 8 years
    I believe that would work @zoranpro - provided you only had the one authentication middleware registered in your startup.cs. If you had multiple registered, then [Authorize] would allow someone authenticated by ANY of those methods to access - which might well be fine depending on your usage.
  • Piotrek
    Piotrek about 8 years
    If I configure everything as you wrote in this answer, how can I pass a token to request? As a header? Url parameter? I can't find anything like that mentioned in your code, whatever I do, I always receive 401 Unauthorized response from server.
  • Piotrek
    Piotrek about 8 years
    OK, I found it: the Header name should be: "Authorization" and value: "Bearer [token]"
  • Thomas Hagström
    Thomas Hagström about 8 years
    This and only this answer worked for ASP.NET 5 RC, after having scourged the internet and associates for a solution! Thank you so much, @MarkHughes and you should really write your own Q&A for this answer, with your excellent example.
  • Camilo Terevinto
    Camilo Terevinto almost 8 years
    @MattDeKrey I know this is an old post, but could you please update it so it works with RC2? I've been searching for hours how to convert your "OAuthBearerAuthenticationOptions" to the new RC2 properties/classes
  • Camilo Terevinto
    Camilo Terevinto almost 8 years
    @MarkHughes please update for RC2, since your UseJwtBearerAuthentication syntax no longer works
  • Matt DeKrey
    Matt DeKrey almost 8 years
    Yep, I need to upgrade the application I lifted this from over to RC2, too. I'm working on it!
  • Kévin Chalet
    Kévin Chalet almost 8 years
    Updated to target ASP.NET Core RTM and ASOS beta6.
  • P. Duw
    P. Duw almost 8 years
    @MarkHughes now that .Net Core has RTMed, I suggest you update your answer/example
  • Warren  P
    Warren P almost 8 years
    I believe this whole thing won't work in RTM due to some changes in the APIs.
  • Mark Hughes
    Mark Hughes over 7 years
    @CamiloTerevinto I'm sure you've worked it out by now, but in case not, I've updated the answer for dotnet core 1.0.1.
  • kloarubeek
    kloarubeek over 7 years
    It didn't work for me until I found out that the Audience was without the scheme (so localhost:54540 instead of localhost:54540). When I changed that, it works like a charm!
  • Willy
    Willy almost 7 years
    This is exactly what i'm looking for! Bless you ... I'm gona try that example. Thanks very much really !