This article demonstrates how to implement Token Authentication and Authorization using JWT (JSON Web Token) in ASP.NET CORE. The approach used in this article does not use any client side cookies for Authentication and Authorization. Which means, Token is not stored in client browser, it’s completely handled from server side. Since this article is mainly focused on implementing ASP.NET CORE Authentication and Authorization, we will not be going deep into Token Configuration and Token Creation. Only a brief explanation is given for Token Configuration and Creation from the implementation point of view. There are many articles which explain it in detail. This article includes the complete code and a LoginDemo.sln project.
Contents
Before going to the topic, let’s have a brief explanation of Authentication and Authorization.
Authentication: Grant access/permission for users to enter the application. It’s like giving access/permission for a person to enter a building.
Authorization: This comes after Authentication. Grant permission for users only to certain pages of the application. It’s like a person who has access/permission to enter a building which has 10 floors, can ONLY go to the 2nd or 4th floor.
As it says, JWToken is a JSON format string value. JWToken is issued for each valid user (Authentication). Token is created only once during user login. User will use that token in all subsequent HTTP requests for Authorization until that user log out from the application.
We would not go into to each and every detail of JWToken
configuration. There are lot of articles which explain that. Configure JWT using Microsoft.AspNetCore
. Authentication.JwtBearer
and Microsoft.IdentityModel.Tokens
. This is done in Startup.cs ConfigurationServices()
method.
You can see in the below code, there are two parts in token configuration, services.AddAuthentication()
and AddJwtBearer()
.
services.AddAuthentication()
: This section is to configure the Authentication Scheme or Mechanism we are going to use. Here, we tell ASP.NET Core to use JWT Bearer Token Authentication. This is very important as this is going to be used in Configure()
method later.
AddJwtBearer()
: In this section, we configure the Token
with Secret Key, Expiration Date, Consumer, etc. Secret Key is to encrypt and decrypt the token. Same secret key should be used while creating the token which we will see in “Create Token” topic.
public void ConfigureServices(IServiceCollection services)
{
services.AddSession(options => {
options.IdleTimeout = TimeSpan.FromMinutes(60);
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
var SecretKey = Encoding.ASCII.GetBytes
("YourKey-2374-OFFKDI940NG7:56753253-tyuw-5769-0921-kfirox29zoxv");
services.AddAuthentication(auth =>
{
auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(token =>
{
token.RequireHttpsMetadata = false;
token.SaveToken = true;
token.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(SecretKey),
ValidateIssuer = true,
ValidIssuer = "http://localhost:45092/",
ValidateAudience = true,
ValidAudience = "http://localhost:45092/",
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
}
We need a model class for user to login. Create a model class for User with user id, password and other credentials. Create a class User.cs under “Models” folder.
public class User
{
public string USERID { get; set; }
public string PASSWORD { get; set; }
public string FIRST_NAME { get; set; }
public string LAST_NAME { get; set; }
public string EMAILID { get; set; }
public string PHONE { get; set; }
public string ACCESS_LEVEL { get; set; }
public string READ_ONLY { get; set; }
}
Step 1
Let’s create a class TokenProvider.cs which would create/generate token for the user. Token is created only once and used in all subsequent request until user logoff. Under root folder of the solution, create a class TokenProvider.cs.
Step 2
Before creating the Token
, we need to get the UserID
from the login page and check if the user is present in our database. For demo purposes, list of users are hard coded values stored in a list. In the real world, this would be from database or some data source. Let’s add a property (UserList
) to TokenProvider.cs class. This property is our user data store which has few hardcoded values.
private List UserList = new List
{
new User { USERID = "jsmith@email.com", PASSWORD = "test",
EMAILID = "jsmith@email.com", FIRST_NAME = "John",
LAST_NAME = "Smith", PHONE = "356-735-2748",
ACCESS_LEVEL = "Director", READ_ONLY = "true" },
new User { USERID = "srob@email.com", PASSWORD = "test",
FIRST_NAME = "Steve", LAST_NAME = "Rob",
EMAILID = "srob@email.com", PHONE = "567-479-8537",
ACCESS_LEVEL = "Supervisor", READ_ONLY = "false" },
new User { USERID = "dwill@email.com", PASSWORD = "test",
FIRST_NAME = "DJ", LAST_NAME = "Will",
EMAILID = "dwill@email.com", PHONE = "599-306-6010",
ACCESS_LEVEL = "Analyst", READ_ONLY = "false" },
new User { USERID = "JBlack@email.com", PASSWORD = "test",
FIRST_NAME = "Joe", LAST_NAME = "Black",
EMAILID = "JBlack@email.com", PHONE = "764-460-8610",
ACCESS_LEVEL = "Analyst", READ_ONLY = "true" }
};
Step 3
We need to set user permission for the application in the token (Authorization). In the token, we need to tell what level of permission user can have. User permissions are created as Claims
. While creating token, we are going to set user permission in Claims
Object collection and assign it to Token
. These Claims
values will be used to grant permission/authorize the user in controllers. In MVC controllers action methods, we would be using “ACCESS_LEVEL
” and “READ_ONLY
” claims to set user permission. For demo purposes, user claims are hard coded. Here, you can connect to your database and get user permission.
Let’s add a method (GetUserClaims()
) to get user permission levels and build claims object collection in TokenProvider.cs class.
private IEnumerable GetUserClaims(User user)
{
IEnumerable claims = new Claim[]
{
new Claim(ClaimTypes.Name, user.FIRST_NAME + " " + user.LAST_NAME),
new Claim("USERID", user.USERID),
new Claim("EMAILID", user.EMAILID),
new Claim("PHONE", user.PHONE),
new Claim("ACCESS_LEVEL", user.ACCESS_LEVEL.ToUpper()),
new Claim("READ_ONLY", user.READ_ONLY.ToUpper())
};
return claims;
}
Step 4
Now it’s time to create the token for the user. First, get the user id from login page and check if the user is in the UserList
collection property declared above. If the user id is in the list, then we have a registered user. If not, then authentication fails. Do not issue the token.
Second, get the password from login page and check if the password matches with the password in the UserList
. If yes, then create a token for user. If not, authentication fails and token is not created/issued.
To create JWToken
, we would be using two namespaces, System.IdentityModel.Tokens.Jwt
and Microsoft.IdentityModel.Tokens
. Let’s create a token using JwtSecurityToken()
class (Here, I am not covering the details of token creation. There are lot of articles which explain JWT token creation). While creating token, user claims values are loaded within the token “claims
” property. We are calling the above function GetUserClaims()
which loads claims for the User. Token
is created in LoginUser()
method which takes UserID
and Password
as input.
Let’s create a function LoginUser()
which takes UserID
and Password
as input parameters in TokenProvider.cs.
public string LoginUser(string UserID, string Password)
{
var user = UserList.SingleOrDefault(x => x.USERID == UserID);
if (user == null)
return null;
if (Password == user.PASSWORD)
{
var key = Encoding.ASCII.GetBytes
("YourKey-2374-OFFKDI940NG7:56753253-tyuw-5769-0921-kfirox29zoxv");
var JWToken = new JwtSecurityToken(
issuer: "http://localhost:45092/",
audience: "http://localhost:45092/",
claims: GetUserClaims(user),
notBefore: new DateTimeOffset(DateTime.Now).DateTime,
expires: new DateTimeOffset(DateTime.Now.AddDays(1)).DateTime,
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
);
var token = new JwtSecurityTokenHandler().WriteToken(JWToken);
return token;
}
else
{
return null;
}
}
Few points to consider...
While creating token, we need to provide the same security key which is configured in Startup.cs for JWToken
configuration.
var key = Encoding.ASCII.GetBytes
("YourKey-2374-OFFKDI940NG7:56753253-tyuw-5769-0921-kfirox29zoxv");
“issuer
” and “audience
” should be the same value which is configured in Startup.cs in ConfigureServices()
method.
Finally, the TokenProvider.cs class looks like this:
using LoginDemo.Models;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace LoginDemo
{
public class TokenProvider
{
public string LoginUser(string UserID, string Password)
{
var user = UserList.SingleOrDefault(x => x.USERID == UserID);
if (user == null)
return null;
if (Password == user.PASSWORD)
{
var key = Encoding.ASCII.GetBytes
("YourKey-2374-OFFKDI940NG7:56753253-tyuw-5769-0921-kfirox29zoxv");
var JWToken = new JwtSecurityToken(
issuer: "http://localhost:45092/",
audience: "http://localhost:45092/",
claims: GetUserClaims(user),
notBefore: new DateTimeOffset(DateTime.Now).DateTime,
expires: new DateTimeOffset(DateTime.Now.AddDays(1)).DateTime,
signingCredentials: new SigningCredentials
(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
);
var token = new JwtSecurityTokenHandler().WriteToken(JWToken);
return token;
}
else
{
return null;
}
}
private List UserList = new List
{
new User { USERID = "jsmith@email.com",
PASSWORD = "test", EMAILID = "jsmith@email.com",
FIRST_NAME = "John", LAST_NAME = "Smith",
PHONE = "356-735-2748", ACCESS_LEVEL = "Director",
READ_ONLY = "true" },
new User { USERID = "srob@email.com", PASSWORD = "test",
FIRST_NAME = "Steve", LAST_NAME = "Rob",
EMAILID = "srob@email.com", PHONE = "567-479-8537",
ACCESS_LEVEL = "Supervisor", READ_ONLY = "false" },
new User { USERID = "dwill@email.com", PASSWORD = "test",
FIRST_NAME = "DJ", LAST_NAME = "Will",
EMAILID = "dwill@email.com", PHONE = "599-306-6010",
ACCESS_LEVEL = "Analyst", READ_ONLY = "false" },
new User { USERID = "JBlack@email.com", PASSWORD = "test",
FIRST_NAME = "Joe", LAST_NAME = "Black",
EMAILID = "JBlack@email.com", PHONE = "764-460-8610",
ACCESS_LEVEL = "Analyst", READ_ONLY = "true" }
};
private IEnumerable GetUserClaims(User user)
{
IEnumerable claims = new Claim[]
{
new Claim(ClaimTypes.Name, user.FIRST_NAME + " " + user.LAST_NAME),
new Claim("USERID", user.USERID),
new Claim("EMAILID", user.EMAILID),
new Claim("PHONE", user.PHONE),
new Claim("ACCESS_LEVEL", user.ACCESS_LEVEL.ToUpper()),
new Claim("READ_ONLY", user.READ_ONLY.ToUpper())
};
return claims;
}
}
}
Now that we have authenticated the user and issued the token for that user, we need to store this token somewhere until the user logs out from the application. This is required since the token needs to be passed in each and every subsequent HTTP request after successful login. As mentioned above, we are not going to use any client (browser) side cookies to store the token.
Rather, we would be storing the token in server side in a user SESSION
. Create a SESSION
variable and store the token in it. After successful login, for each subsequent request, we would get the token from the session
variable and insert into incoming HTTP Request.
We would be doing this in HomeController
action method below, gets the token from TokenProvider.cs, create a Session
object “JWToken
” and store the token.
In HomeController.cs, there is a “LoginUser
” action method. From Index.cshtml, user would input User ID and Password and submit the page to “LoginUser
” action method in the HomeController.cs. In ‘LoginUser
” controller action method, we will be adding the token to session object name “JWToken
”.
HttpContext.Session.SetString("JWToken", userToken);
Here comes the crucial part of the whole implementation. This part is more of a concept and few lines of code. We are going to do two things here:
- Insert the token into HTTP Request
- Load user claims into HTTP Request
Let’s understand the concept first. Trying to keep it simple, please bear with me.
Authentication and Authorization is handled through HTTP Request, to do that:
- Token should be part of HTTP Request and it should come from HTTP Request Header.
ClaimsPrinciple
and ClaimsIdentity
(HttpContext.User.Identity
) object is created from current HTTP Context. - User Claims are read from HTTP Request header and loaded into HTTP
Claims
Identity object - In other words, Authorization is done through incoming HTTP Request, NOT directly reading from the Token.
- By doing this, HTTP Request itself is Authorized for that user.
To achieve the above:
- We need to insert the
Token
(which is stored in user session variable “JWToken
”) into each incoming HTTP Request. - Read user claims values from
Token
and load it into HTTP Context Claims
Principle object. - If token in not available in
session
variable “JWToken
”, then HTTP Request header “Authorization
” would be empty. In that case, Claims Principle for that user will not be set in HTTP Context. That would deny permission for the user.
The below picture gives an idea about how we are going to insert the Token to HTTP header and set the Claims Principle in HTTP Context.
Custom Middleware app.Use()
The main idea to have this custom middleware to insert the token into incoming HTTP Request. Now we have logged in user Token stored in Session
variable “JWToken
”, We need to insert that token into all subsequent incoming HTTP Request. For this, we are going to write a few lines of code into ASP.NET Core Middleware. This is nothing but HTTP pipeline. Custom Middleware is added in Startup.cs Configure()
method.
P.S.: Token
is created only once during user login.
Middleware app.UseAuthentication()
Now we need to validate the token and load the claims to HTTP Request
context. UseAuthentication()
does this job for us. Before HTTP Request hits the MVC controller, UseAuthentication()
does the following:
- Decrypting and Validating the Token using the secret key provided in
AddJwtBearer()
configuration (under ConfigureServices()
method in Startup.cs) - Setting the
User
object in HTTP Request Context - Finally, read the
Claims
values from Token
and load it to HttpContext.User.Identity
object
Custom Middleware Code
In Startup.cs, add the following code to Configure()
method. Add the below code after app.UseCookiePolicy()
. Here, the code execution sequence is important.
app.UseSession();
app.Use(async (context, next) =>
{
var JWToken = context.Session.GetString("JWToken");
if (!string.IsNullOrEmpty(JWToken))
{
context.Request.Headers.Add("Authorization", "Bearer " + JWToken);
}
await next();
});
app.UseAuthentication();
Let’s go through the code:
app.UseSession()
is configuration for using Session
objects. - To write custom middleware, use
app.Use()
. - First, we need the
Token
before we insert it to HTTP Request. We have stored the token in Session
. Get the Token
from session
variable “JWToken
”.
var JWToken = context.Session.GetString("JWToken");
- The next line checks if
Token
is available in Session
. If not, user is not Authenticated. So user permission is denied. - If the
Token
is present in Session
variable “JWToken
”, then we have Authenticated user. - Now, we need to add the Token to the HTTP
Request
(Remember, User Identity is created through HTTP Request
). The below code adds the token to all incoming HTTP Request
s.
context.Request.Headers.Add("Authorization", "Bearer " + JWToken);
- Note, we are adding the Token to a “
Authorization
” header of the HTTP Request
. Yes, it’s important to add the token to “Authorization
” header and the token should be concatenated with a keyword “Bearer
”. - The next line of code is
app.UseAuthentication()
.
- This line of code will look for the Authentication mechanism configured in
ConfigureServices()
method. In our ConfigureService()
, we have used “AddJwtBearer
” configuration, which is part of Microsoft.AspNetCore.Authentication namespace
. - Inside
AddJWtBeared()
, we have our token configuration with secret key, expiration date, etc. - When HTTP
Request
comes in, app.UseAuthentication()
will look for “Authorization
” header in the HTTP Request
. It will read the value stored in “Authorization
” header and pass it to Microsoft.AspNetCore.Authentication
. Microsoft.AspNetCore.Authentication
will evaluate and validate the token as per the configuration we have set for the token. This includes decrypting the token using the secret key we have given in the configuration and reading the claims from the token and loading the claims to HttpContext.User.Identity
object. Here, HTTP Context itself is Authenticated and Authorized. - This complete execution is valid only for one HTTP
Request
(that particular incoming request). We have to do this for all subsequent HTTP Request
. That’s the reason we store the Token
in session
variable and assign the Token
to HTTP Request
“Authorization
” header for all subsequent incoming HTTP Request
. All incoming HTTP Request
and outgoing HTTP Response
goes through the HTTP Pipeline in Startup.cs Configure()
method.
Finally, Startup.cs Configure() method looks like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseSession();
app.Use(async (context, next) =>
{
var JWToken = context.Session.GetString("JWToken");
if (!string.IsNullOrEmpty(JWToken))
{
context.Request.Headers.Add("Authorization", "Bearer " + JWToken);
}
await next();
});
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Now let’s create a simple login page (Index.cshtml) with user id and password textbox. Add User.cs model to view page. Here you can see the IF
condition User.Identity.IsAuthenticated
which checks if the user is Authenticated or not. “User
” object is part of System.Security.Claims
which is set in HTTP Context by the middleware. If user is authenticated, we show the user name from the claims
identity name property. If not, then we ask the user to login.
@model LoginDemo.Models.User
@{
ViewData["Title"] = "Home Page";
}
<div style="padding-top:50px;"></div>
<div style="padding-top:50px;">
@if (User.Identity.IsAuthenticated)
{
<div class="row">
You are Logged in as
<span style="font-size:large;color:forestgreen;">
@User.Identity.Name</span>
</div>
<div class="row" style="padding-top:50px;">
@Html.ActionLink("Log Off", "Logoff",
"Home", null, new { @class = "btn btn-primary btn-lg rph-login-button" })
</div>
}
else
{
<div class="row">
<div class="col-lg-4 col-md-4 col-sm-4">
<div>
@using (Html.BeginForm("LoginUser", "Home",
FormMethod.Post, new { role = "form" }))
{
<div>
@Html.AntiForgeryToken()
<div>
<label>User ID</label><br />
</div>
<div>
@Html.TextBoxFor(m => m.USERID,
new {@class = "form-control txtbox"})
</div>
<div style="padding-top:20px;"></div>
<div>
<label>Password</label><br />
</div>
<div>
@Html.PasswordFor(m => m.USERID,
new {@class = "form-control txtbox"})
</div>
</div>
<div class="padding-left:35%;width:40%;">
<div class="padding-top:20px;">
<input class="btn btn-primary
btn-lg rph-login-button"
type="submit" value="Login"/>
</div>
</div>
}
</div>
</div>
<div class="col-lg-8 col-md-8 col-sm-8">
<div style="padding-top:50px;">
<div><b>Please login with any of the below User ID,
Password is span style="font-size:large;color:forestgreen;"
>test</span> for all Users</b></div>
<div style="padding-top:10px;">
<ui style="list-style: none;">
<li>jsmith@email.com - Director, Read Only - true</li>
<li>srob@email.com - Supervisor, Read Only - false</li>
<li>dwill@email.com - Analyst, Read Only - false</li>
<li>JBlack@email.com - Analyst, Read Only - true</li>
</ui>
</div>
</div>
</div>
</div>
}
</div>
Let’s add two Action
methods in HomeController.cs. One for Index
(Login) page and the other one to submit the login page.
public IActionResult Index()
{
return View();
}
public IActionResult LoginUser(User user)
{
TokenProvider _tokenProvider = new TokenProvider();
var userToken = _tokenProvider.LoginUser(user.USERID.Trim(), user.PASSWORD.Trim());
if (userToken != null)
{
HttpContext.Session.SetString("JWToken", userToken);
}
return Redirect("~/Home/Index");
}
Action
method LoginUser(User user)
takes the user id and password values from login page. The below line does the authentication by checking user id and password in data store.
var userToken = _tokenProvider.LoginUser(user.USERID.Trim(), user.PASSWORD.Trim());
Next lines check if there is a token issued by TokenProvider()
. If yes, then save the token in user Session
variable “JWToken
”.
if (userToken != null)
{
HttpContext.Session.SetString("JWToken", userToken);
}
Then, redirect the page to Index.cshtml:
return Redirect("~/Home/Index");
During the page redirection, we have already stored the token in session
object. Now the page redirection goes through the HTTP pipeline in Startup.cs. Now the custom middleware will stop the HTTP Request
and insert the token into HTTP Request
header “Authorization
”. Please refer to "Middleware" for more details.
If token
in not available in session
variable “JWToken
”, then HTTP Request
header “Authorization
” would be empty. In that case, HTTP Context
will not be set for that user. Redirection will ask the user to login.
Let's log off the user. When there is no token, then HTTP Context cannot be set for the user. So, remove the token
from session
object. To remove the token
from session
, clear the session
for the user and redirect to another controller action.
Add a controller action method Logoff()
. Clear the session
for the user and redirect to Index
action method. It is important to redirect to another controller action method. Let's see why? Say, in Logoff()
action method, we return a View()
instead of Redirect()
. In this case, view page will be rendered to the browser and still users can access that page, User.Identity.IsAuthenticated
is still true
. When ASP.NET executes controller action method, it's in the process of HTTP RESPONSE
. Which means it had already passed through HTTP REQUEST
. User Claims Principle is set in HTTP Request
. By logging off the user, we need to clear the Claims Principle for that user as well. Clearing the session alone is not enough. So we need to go through the HTTP Pipeline again. Redirection to another controller goes through the HTTP Pipeline and it will look for the Token
in session
variable "JWToken
". But we have cleared the session
, token
is not in session
anymore. Without token, Claims Principle cannot be set in the HTTP Context. This will completely log out the user.
public IActionResult Logoff()
{
HttpContext.Session.Clear();
return Redirect("~/Home/Index");
}
Controller Code
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using LoginDemo.Models;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
namespace LoginDemo.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public IActionResult LoginUser(User user)
{
TokenProvider _tokenProvider = new TokenProvider();
var userToken = _tokenProvider.LoginUser(user.USERID.Trim(),
user.PASSWORD.Trim());
if (userToken != null)
{
HttpContext.Session.SetString("JWToken", userToken);
}
return Redirect("~/Home/Index");
}
public IActionResult Logoff()
{
HttpContext.Session.Clear();
return Redirect("~/Home/Index");
}
}
}
Login Page
LoginDemo.sln
In Part 2, we will cover Authorization for users. We are going to see:
- How to give page level access to users
- How to create custom authorize filter attribute to restrict users on controller level and action method level
- Decorate controller action methods with custom authorize attributes
- Restrict users from directly accessing a page without login
Go to Part 2