The following article shows how to developing token authentication using ASP.NET Core.
Token based authentication overview
Nowadays, Token based authentication is very common on the web and any major API or web applications use tokens.
Token authentication is stateless, secure and designed to be scalable. In fact, it is quickly becoming a de facto standard for modern single-page applications and mobile apps.
The problems with server based authentication
Authentication is the process by which an application confirms user identity. Applications have traditionally persisted identity through session cookies, relying on session IDs stored server-side. A few major problems caused by this technique:
- Scalability: if sessions are stored in memory, this provides problems with scalability;
- CORS: as we want to expand our application to let our data be used across multiple mobile devices, we have to worry about cross-origin resource sharing (CORS);
- CSRF: we will also have protection against cross-site request forgery(CSRF);
- Sessions: Every time a user is authenticated, the server will need to create a record on our server;
How token based authentication works
Token based authentication is stateless. It don’t store any information about our user on the server or in a session.
Here’s the common steps of the token based authentication:
- user requests access by using username / password;
- application provides a signed token to the client;
- client stores that token and sends it along with every request;
- server verifies token and responds with data;
Every single request will require the token. The token should be sent in the HTTP header to keep the idea of stateless HTTP requests.
Implementing Token based authentication using ASP.Net Core
This example shows how to developing token authentication using ASP.NET Core, the following UML schema shows the architecture of project:
Setup the project
First of all, is necessary create new ASP.NET Core project. I suggest to use ASP.NET Yeoman Generator to generate project using Web application template and Visual Studio Code to edit the code.
Once the project is successfully created, add the following configurations to your appsettings.json
:
{ |
“Logging”: { |
“IncludeScopes”: false, |
“LogLevel”: { |
“Default”: “Debug”, |
“System”: “Information”, |
“Microsoft”: “Information” |
} |
}, |
“TokenAuthentication”: { |
“SecretKey”: “secretkey_secretkey123!”, |
“Issuer”: “DemoIssuer”, |
“Audience”: “DemoAudience”, |
“TokenPath”: “/api/token”, |
“CookieName”: “access_token” |
} |
} |
The TokenAuthentication
section configures some common information about token generation, for example the SectionKey
used by token.
Tokens transmission / validation
There are two ways to transmit the authorization tokens:
- using HTTP Authorization headers (aka Bearer authentication);
- using browser cookies to save the authentication token;
Bearer token validation
The Microsoft.AspNetCore.Authentication.JwtBearer
package enables you to protect routes by using a JWT Token.
To enable Bearer token authentication, import the following Nuget package Microsoft.AspNetCore.Authentication.JwtBearer
in the project.json:
{ |
“dependencies”: { |
“Microsoft.NETCore.App”: { |
“version”: “1.0.1”, |
“type”: “platform” |
}, |
“Microsoft.AspNetCore.Mvc”: “1.0.1”, |
“Microsoft.AspNetCore.Routing”: “1.0.1”, |
“Microsoft.AspNetCore.Server.IISIntegration”: “1.0.0”, |
“Microsoft.AspNetCore.Server.Kestrel”: “1.0.1”, |
“Microsoft.Extensions.Configuration.EnvironmentVariables”: “1.0.0”, |
“Microsoft.Extensions.Configuration.FileExtensions”: “1.0.0”, |
“Microsoft.Extensions.Configuration.Json”: “1.0.0”, |
“Microsoft.Extensions.Configuration.CommandLine”: “1.0.0”, |
“Microsoft.Extensions.Logging”: “1.0.0”, |
“Microsoft.Extensions.Logging.Console”: “1.0.0”, |
“Microsoft.Extensions.Logging.Debug”: “1.0.0”, |
“Microsoft.Extensions.Options.ConfigurationExtensions”: “1.0.0”, |
“Microsoft.AspNetCore.Authentication.JwtBearer”:”1.0.0″, |
“Microsoft.AspNetCore.Authentication.Cookies”: “1.0.0” |
}, |
“tools”: { |
“Microsoft.AspNetCore.Server.IISIntegration.Tools”: “1.0.0-preview2-final” |
}, |
“frameworks”: { |
“netcoreapp1.0”: { |
“imports”: [ |
“dotnet5.6”, |
“portable-net45+win8” |
] |
} |
}, |
“buildOptions”: { |
“emitEntryPoint”: true, |
“preserveCompilationContext”: true |
}, |
“runtimeOptions”: { |
“configProperties”: { |
“System.GC.Server”: true |
} |
}, |
“publishOptions”: { |
“include”: [ |
“wwwroot”, |
“**/*.cshtml”, |
“appsettings.json”, |
“web.config” |
] |
}, |
“scripts”: { |
“postpublish”: [ “dotnet publish-iis –publish-folder %publish:OutputPath% –framework %publish:FullTargetFramework%” ] |
}, |
“tooling”: { |
“defaultNamespace”: “Blog.TokenAuthGettingStarted” |
} |
} |
To initialize the Bearer authentication you need to split your Startup.cs
file and use another partial class, for example Startup.Auth.cs
:
public partial class Startup |
{ |
public SymmetricSecurityKey signingKey; |
private void ConfigureAuth(IApplicationBuilder app) |
{ |
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration.GetSection(“TokenAuthentication:SecretKey”).Value)); |
var tokenValidationParameters = new TokenValidationParameters |
{ |
// The signing key must match! |
ValidateIssuerSigningKey = true, |
IssuerSigningKey = signingKey, |
// Validate the JWT Issuer (iss) claim |
ValidateIssuer = true, |
ValidIssuer = Configuration.GetSection(“TokenAuthentication:Issuer”).Value, |
// Validate the JWT Audience (aud) claim |
ValidateAudience = true, |
ValidAudience = Configuration.GetSection(“TokenAuthentication:Audience”).Value, |
// Validate the token expiry |
ValidateLifetime = true, |
// If you want to allow a certain amount of clock drift, set that here: |
ClockSkew = TimeSpan.Zero |
}; |
app.UseJwtBearerAuthentication(new JwtBearerOptions |
{ |
AutomaticAuthenticate = true, |
AutomaticChallenge = true, |
TokenValidationParameters = tokenValidationParameters |
}); |
} |
} |
The Startup.Auth.cs
file initialize the Bearer Authentication using configurations defined in the appsettings.json
file. The tokenValidationParamaters
object will be used also by Cookie validation.
Cookies validation
Cookies validation enables the Token transport over browser cookies, to enable the Cookie token authentication you need to add the following package inside the project.json
:
"Microsoft.AspNetCore.Authentication.Cookies": "1.0.0"
and create a custom validator for the input token.
To create the new validator add the following CustomJwtDataFormat.cs
file:
using System; |
using System.IdentityModel.Tokens.Jwt; |
using System.Security.Claims; |
using Microsoft.AspNetCore.Authentication; |
using Microsoft.AspNetCore.Http.Authentication; |
using Microsoft.IdentityModel.Tokens; |
namespace CustomTokenAuthProvider |
{ |
public class CustomJwtDataFormat : ISecureDataFormat<AuthenticationTicket> |
{ |
private readonly string algorithm; |
private readonly TokenValidationParameters validationParameters; |
public CustomJwtDataFormat(string algorithm, TokenValidationParameters validationParameters) |
{ |
this.algorithm = algorithm; |
this.validationParameters = validationParameters; |
} |
public AuthenticationTicket Unprotect(string protectedText) |
=> Unprotect(protectedText, null); |
public AuthenticationTicket Unprotect(string protectedText, string purpose) |
{ |
var handler = new JwtSecurityTokenHandler(); |
ClaimsPrincipal principal = null; |
SecurityToken validToken = null; |
try |
{ |
principal = handler.ValidateToken(protectedText, this.validationParameters, out validToken); |
var validJwt = validToken as JwtSecurityToken; |
if (validJwt == null) |
{ |
throw new ArgumentException(“Invalid JWT”); |
} |
if (!validJwt.Header.Alg.Equals(algorithm, StringComparison.Ordinal)) |
{ |
throw new ArgumentException($”Algorithm must be ‘{algorithm}'”); |
} |
} |
catch (SecurityTokenValidationException) |
{ |
return null; |
} |
catch (ArgumentException) |
{ |
return null; |
} |
// Token validation passed |
return new AuthenticationTicket(principal, new AuthenticationProperties(), “Cookie”); |
} |
public string Protect(AuthenticationTicket data) |
{ |
throw new NotImplementedException(); |
} |
public string Protect(AuthenticationTicket data, string purpose) |
{ |
throw new NotImplementedException(); |
} |
} |
} |
Unprotect
method decript and validate information provided by the input token. Call the following method in the Startup.Auth.cs
file, to use the Cookie authentication:
app.UseCookieAuthentication(new CookieAuthenticationOptions |
{ |
AutomaticAuthenticate = true, |
AutomaticChallenge = true, |
AuthenticationScheme = “Cookie”, |
CookieName = Configuration.GetSection(“TokenAuthentication:CookieName”).Value, |
TicketDataFormat = new CustomJwtDataFormat( |
SecurityAlgorithms.HmacSha256, |
tokenValidationParameters) |
}); |
Token generation
There isn’t native support to Token generation in ASP.NET Core, but it is possible write a custom token generator middleware from scratch.
Firstly, you need to create a class which implement token options :
using System; |
using System.Security.Claims; |
using System.Threading.Tasks; |
using Microsoft.IdentityModel.Tokens; |
namespace CustomTokenAuthProvider |
{ |
public class TokenProviderOptions |
{ |
/// <summary> |
/// The relative request path to listen on. |
/// </summary> |
/// <remarks>The default path is <c>/token</c>.</remarks> |
public string Path { get; set; } = “/token”; |
/// <summary> |
/// The Issuer (iss) claim for generated tokens. |
/// </summary> |
public string Issuer { get; set; } |
/// <summary> |
/// The Audience (aud) claim for the generated tokens. |
/// </summary> |
public string Audience { get; set; } |
/// <summary> |
/// The expiration time for the generated tokens. |
/// </summary> |
/// <remarks>The default is five minutes (300 seconds).</remarks> |
public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(5); |
/// <summary> |
/// The signing key to use when generating tokens. |
/// </summary> |
public SigningCredentials SigningCredentials { get; set; } |
/// <summary> |
/// Resolves a user identity given a username and password. |
/// </summary> |
public Func<string, string, Task<ClaimsIdentity>> IdentityResolver { get; set; } |
} |
} |
The middleware class will use TokenProviderOptions.cs
to generate tokens:
using System; |
using System.IdentityModel.Tokens.Jwt; |
using System.Security.Claims; |
using System.Threading.Tasks; |
using Microsoft.AspNetCore.Http; |
using Microsoft.Extensions.Options; |
using Newtonsoft.Json; |
namespace CustomTokenAuthProvider |
{ |
public class TokenProviderMiddleware |
{ |
private readonly RequestDelegate _next; |
private readonly TokenProviderOptions _options; |
private readonly JsonSerializerSettings _serializerSettings; |
public TokenProviderMiddleware( |
RequestDelegate next, |
IOptions<TokenProviderOptions> options) |
{ |
_next = next; |
_options = options.Value; |
ThrowIfInvalidOptions(_options); |
_serializerSettings = new JsonSerializerSettings |
{ |
Formatting = Formatting.Indented |
}; |
} |
public Task Invoke(HttpContext context) |
{ |
// If the request path doesn’t match, skip |
if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal)) |
{ |
return _next(context); |
} |
// Request must be POST with Content-Type: application/x-www-form-urlencoded |
if (!context.Request.Method.Equals(“POST”) |
|| !context.Request.HasFormContentType) |
{ |
context.Response.StatusCode = 400; |
return context.Response.WriteAsync(“Bad request.”); |
} |
return GenerateToken(context); |
} |
private async Task GenerateToken(HttpContext context) |
{ |
var username = context.Request.Form[“username”]; |
var password = context.Request.Form[“password”]; |
var identity = await _options.IdentityResolver(username, password); |
if (identity == null) |
{ |
context.Response.StatusCode = 400; |
await context.Response.WriteAsync(“Invalid username or password.”); |
return; |
} |
var now = DateTime.UtcNow; |
// Specifically add the jti (nonce), iat (issued timestamp), and sub (subject/user) claims. |
// You can add other claims here, if you want: |
var claims = new Claim[] |
{ |
new Claim(JwtRegisteredClaimNames.Sub, username), |
new Claim(JwtRegisteredClaimNames.Jti, await _options.NonceGenerator()), |
new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUniversalTime().ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) |
}; |
// Create the JWT and write it to a string |
var jwt = new JwtSecurityToken( |
issuer: _options.Issuer, |
audience: _options.Audience, |
claims: claims, |
notBefore: now, |
expires: now.Add(_options.Expiration), |
signingCredentials: _options.SigningCredentials); |
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); |
var response = new |
{ |
access_token = encodedJwt, |
expires_in = (int)_options.Expiration.TotalSeconds |
}; |
// Serialize and return the response |
context.Response.ContentType = “application/json”; |
await context.Response.WriteAsync(JsonConvert.SerializeObject(response, _serializerSettings)); |
} |
private static void ThrowIfInvalidOptions(TokenProviderOptions options) |
{ |
if (string.IsNullOrEmpty(options.Path)) |
{ |
throw new ArgumentNullException(nameof(TokenProviderOptions.Path)); |
} |
if (string.IsNullOrEmpty(options.Issuer)) |
{ |
throw new ArgumentNullException(nameof(TokenProviderOptions.Issuer)); |
} |
if (string.IsNullOrEmpty(options.Audience)) |
{ |
throw new ArgumentNullException(nameof(TokenProviderOptions.Audience)); |
} |
if (options.Expiration == TimeSpan.Zero) |
{ |
throw new ArgumentException(“Must be a non-zero TimeSpan.”, nameof(TokenProviderOptions.Expiration)); |
} |
if (options.IdentityResolver == null) |
{ |
throw new ArgumentNullException(nameof(TokenProviderOptions.IdentityResolver)); |
} |
if (options.SigningCredentials == null) |
{ |
throw new ArgumentNullException(nameof(TokenProviderOptions.SigningCredentials)); |
} |
if (options.NonceGenerator == null) |
{ |
throw new ArgumentNullException(nameof(TokenProviderOptions.NonceGenerator)); |
} |
} |
} |
} |
The TokenProviderMiddleware
class implement the Invoke
method to generate tokens by using the TokenProviderOptions
. In order to initialize the middleware, it is necessary modify the Startup.Auth.cs
file and add in the ConfigureAuth
method:
using System; |
using System.Text; |
using System.Security.Claims; |
using System.Security.Principal; |
using System.Threading.Tasks; |
using CustomTokenAuthProvider; |
using Microsoft.AspNetCore.Builder; |
using Microsoft.IdentityModel.Tokens; |
using Microsoft.Extensions.Options; |
namespace Blog.TokenAuthGettingStarted |
{ |
public partial class Startup |
{ |
private void ConfigureAuth(IApplicationBuilder app) |
{ |
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration.GetSection(“TokenAuthentication:SecretKey”).Value)); |
var tokenProviderOptions = new TokenProviderOptions |
{ |
Path = Configuration.GetSection(“TokenAuthentication:TokenPath”).Value, |
Audience = Configuration.GetSection(“TokenAuthentication:Audience”).Value, |
Issuer = Configuration.GetSection(“TokenAuthentication:Issuer”).Value, |
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256), |
IdentityResolver = GetIdentity |
}; |
var tokenValidationParameters = new TokenValidationParameters |
{ |
// The signing key must match! |
ValidateIssuerSigningKey = true, |
IssuerSigningKey = signingKey, |
// Validate the JWT Issuer (iss) claim |
ValidateIssuer = true, |
ValidIssuer = Configuration.GetSection(“TokenAuthentication:Issuer”).Value, |
// Validate the JWT Audience (aud) claim |
ValidateAudience = true, |
ValidAudience = Configuration.GetSection(“TokenAuthentication:Audience”).Value, |
// Validate the token expiry |
ValidateLifetime = true, |
// If you want to allow a certain amount of clock drift, set that here: |
ClockSkew = TimeSpan.Zero |
}; |
app.UseJwtBearerAuthentication(new JwtBearerOptions |
{ |
AutomaticAuthenticate = true, |
AutomaticChallenge = true, |
TokenValidationParameters = tokenValidationParameters |
}); |
app.UseCookieAuthentication(new CookieAuthenticationOptions |
{ |
AutomaticAuthenticate = true, |
AutomaticChallenge = true, |
AuthenticationScheme = “Cookie”, |
CookieName = Configuration.GetSection(“TokenAuthentication:CookieName”).Value, |
TicketDataFormat = new CustomJwtDataFormat( |
SecurityAlgorithms.HmacSha256, |
tokenValidationParameters) |
}); |
app.UseMiddleware<TokenProviderMiddleware>(Options.Create(tokenProviderOptions)); |
} |
private Task<ClaimsIdentity> GetIdentity(string username, string password) |
{ |
// DEMO CODE, DON NOT USE IN PRODUCTION!!! |
if (username == “TEST” && password == “TEST123”) |
{ |
return Task.FromResult(new ClaimsIdentity(new GenericIdentity(username, “Token”), new Claim[] { })); |
} |
// Account doesn’t exists |
return Task.FromResult<ClaimsIdentity>(null); |
} |
} |
} |
The tokenProviderOptions
defines the options of the token generator. The IdentityResolver
is the Task method which will check the identity of users. For demo purposes, the IdentityResolver
is implemented by a simple method called GetIdentity.
Final steps
Now is possible call the ConfigureAuth
method inside the Startup.cs
file:
using Microsoft.AspNetCore.Builder; |
using Microsoft.AspNetCore.Hosting; |
using Microsoft.Extensions.Configuration; |
using Microsoft.Extensions.DependencyInjection; |
using Microsoft.Extensions.Logging; |
using Microsoft.IdentityModel.Tokens; |
namespace Blog.TokenAuthGettingStarted |
{ |
public partial class Startup |
{ |
public SymmetricSecurityKey signingKey; |
public Startup(IHostingEnvironment env) |
{ |
var builder = new ConfigurationBuilder() |
.SetBasePath(env.ContentRootPath) |
.AddJsonFile(“appsettings.json”, optional: true, reloadOnChange: true) |
.AddJsonFile($”appsettings.{env.EnvironmentName}.json”, optional: true) |
.AddEnvironmentVariables(); |
Configuration = builder.Build(); |
} |
public IConfigurationRoot Configuration { get; } |
// This method gets called by the runtime. Use this method to add services to the container. |
public void ConfigureServices(IServiceCollection services) |
{ |
// Add framework services. |
services.AddMvc(); |
} |
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. |
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) |
{ |
loggerFactory.AddConsole(Configuration.GetSection(“Logging”)); |
loggerFactory.AddDebug(); |
ConfigureAuth(app); |
app.UseMvc(); |
} |
} |
} |
Getting token
You can obtain the JWT token by calling the following route /api/token/
using POST
and passing the username and password data:
POST api/token
Content-Type: application/x-www-form-urlencoded
username=TEST&password=TEST123
Authorize controllers
All controllers decorated by the attribute [Authorize]
are protected by the JWT authentication.
In each http call you need to pass the access_token
parameter:
You need to pass the token in the header request:
Authorization:Bearer MY_TOKEN