It's important to add extra layers of security when generating a Bearer Token in ASP.NET Core.
In part 2, we had a look at how we can set up OAuth security by generating a Bearer token. However, we recognised that there were security vulnerabilities when creating the token.
We will have a look at some extra security practices that we can put in place in-order to address the security vulnerabilities.
Generating an Authorisation Code
The first thing we want to have a look at is generating an authorisation code.
We will pass in the client credentials through basic authentication that we looked at in part 1. From there, we will generate a unique authorisation code.
The whole point of this authorisation code is that it will be passed in as a parameter when creating a token. It will be unique to the user who has created it, have an expiry date and can only be used once.
That means that if the authorisation code does not belong to the user generating the token, has expired, or has already been used, then a token can't be generated.
Create the AuthorizationCode Class
The first thing that we want to do is to create the AuthorizationCode class. This will include things like the name of the user, the code and the expiry date.
// AuthorizeCode.cs
public class AuthorizeCode
{
public AuthorizeCode(string name, string code, DateTimeOffset? expiry)
{
Name = name;
Code = code;
Expiry = expiry;
}
public virtual string Name { get; }
public virtual string Code { get; }
public virtual DateTimeOffset? Expiry { get; }
public virtual DateTimeOffset? Used { get; protected set; }
public void SetUsed()
{
Used = DateTimeOffset.Now;
}
}
Create the AuthorizeCodeService Class
Next, we are going to create an AuthorizeCodeService class.
For this example, we are going to add this class to dependency injection as a singleton class. Inside it, we are going to have a dictionary that will store all the authorisation codes.
The benefits of doing it this way is that it's quicker to set up for demo purposes.
However, the drawbacks are that if we restart our application, all the data stored in the dictionary will clear. So, it would be a good idea to store the authorisation codes into a SQL Server database.
We are going to create the following methods in our AuthorizeCodeService class.
- Create - Creates the Authorisation Code.
- Read - Reads the Authorisation Code.
- UpdateUsed - Updates the Authorisation Code to state that it has been used.
// IAuthorizeCodeService.cs
public interface IAuthorizeCodeService
{
AuthorizeCode Create(AuthorizeCode authorizeCode);
AuthorizeCode Read(string code);
AuthorizeCode UpdateUsed(string code);
}
// AuthorizeCodeService.cs
public class AuthorizeCodeService : IAuthorizeCodeService
{
protected IDictionary<string, AuthorizeCode> CodeValues { get; }
public AuthorizeCodeService()
{
CodeValues = new ConcurrentDictionary<string, AuthorizeCode>();
}
public AuthorizeCode Create(AuthorizeCode authorizeCode)
{
CodeValues.Add(new KeyValuePair<string, AuthorizeCode>(authorizeCode.Code, authorizeCode));
return authorizeCode;
}
public AuthorizeCode Read(string code)
{
if (!CodeValues.ContainsKey(code))
{
return null;
}
return CodeValues[code];
}
public AuthorizeCode UpdateUsed(string code)
{
var authorizationCode = Read(code);
if (authorizationCode == null)
{
return null;
}
authorizationCode.SetUsed();
return authorizationCode;
}
}
In addition, we need to go into our Startup class and state that the AuthorisationCodeService has a singleton lifetime span in dependency injection.
// Startup.cs
public class Startup
{
...
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton<IAuthorizeCodeService, AuthorizeCodeService>();
...
}
...
}
Add an Authorise Action to our OAuth Controller
The next thing we need to do is to add an authorise action to our OAuth controller.
We will use client id and response type as query string parameters and pass them into the action.
Before we allow the user to generate an authorisation code, we need to do a check.
We need to check that the response type is set to code.
Once that passes, we go ahead and create a random number, which gets encrypted using the SHA-256 cryptographic hash.
That is then created to the AuthorizationCodeService.
// OAuthController.cs
[Route("oauth")]
public class OAuthController : Controller
{
protected readonly IAuthorizeCodeService _authorizeCodeService;
public OAuthController([NotNull] IAuthorizeCodeService authorizeCodeService)
{
_authorizeCodeService = authorizeCodeService;
}
[Route("authorize"), HttpGet]
public IActionResult Authorize([FromQuery(Name = "client_id")] string clientId, [FromQuery(Name = "response_type")]string responseType)
{
if (responseType != "code")
{
return BadRequest(new { error = "invalid_request", error_description = "The 'response_type' querystring needs to be set to 'code'" });
}
// Generate the code
var randomNumber = RandomNumberGenerator.Create();
var bytes = new byte[32];
randomNumber.GetBytes(bytes);
var code = string.Empty;
using (var sha256 = SHA256.Create())
{
code = Convert.ToBase64String(sha256.ComputeHash(Encoding.UTF8.GetBytes(Convert.ToBase64String(bytes))));
}
_authorizeCodeService.Create(new AuthorizeCode(clientId, code, DateTimeOffset.UtcNow.AddMinutes(1)));
return Ok(new { code });
}
...
}
Make Modifications To The Token Action
Now that we've done that, we need to make modifications to the Token action.
We need to check that the client id passed in matches the username that was used in Basic authentication.
Assuming it does, we are passing in a grant type, and we need to make sure that this is set to authorization_code.
Once it does that, it then gets the authorisation code record by passing in the code into the AuthorizationCodeService.
It will throw an error if:
- The authorisation code doesn't exist.
- The user trying to generate the token is not the same user who generated the authorisation code.
- The authorisation code has been used.
- The authorisation code has expired.
Assuming that it has passed all these tests, it can go ahead and create the token.
// OAuthController.cs
[Route("oauth")]
public class OAuthController : Controller
{
protected readonly IAuthorizeCodeService _authorizeCodeService;
public OAuthController([NotNull] IAuthorizeCodeService authorizeCodeService)
{
_authorizeCodeService = authorizeCodeService;
}
...
[Route("token"), HttpPost, BasicAuthorization]
public IActionResult Token([FromForm(Name = "client_id")] string clientId, string code, [FromForm(Name = "grant_type")] string grantType)
{
if (clientId != User.Identity.Name)
{
return BadRequest(new { error = "invalid_request", error_description = "The 'client_id' passed in does not match the one that was authenticated." });
}
if (grantType != "authorization_code")
{
return BadRequest(new { error = "invalid_request", error_description = "The 'grant_type' form value must be set to 'authorization_code'" });
}
var authorizeCode = _authorizeCodeService.Read(code);
if (authorizeCode == null)
{
return BadRequest(new { error = "invalid_request", error_description = "The code could not be found." });
}
else if (authorizeCode.Name != User.Identity.Name)
{
return BadRequest(new { error = "invalid_request", error_description = "The user authenticated does not match the user who generated the code." });
}
else if (authorizeCode.Used.HasValue)
{
return BadRequest(new { error = "invalid_request", error_description = "The authorization code has already been used." });
}
else if (authorizeCode.Expiry <= DateTimeOffset.Now)
{
return BadRequest(new { error = "invalid_request", error_description = "The authorization code passed in has expired." });
}
var tokenHandler = new JwtSecurityTokenHandler();
var authenticatedUser = new AuthenticatedUser(JwtBearerDefaults.AuthenticationScheme, true, "roundthecode");
var accessToken = tokenHandler.WriteToken(tokenHandler.CreateToken(new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(authenticatedUser),
Expires = DateTime.UtcNow.AddMinutes(1),
Issuer = "me",
Audience = "you",
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes("roundthecode9999")), SecurityAlgorithms.HmacSha256Signature),
IssuedAt = DateTime.UtcNow
}));
_authorizeCodeService.UpdateUsed(code);
return Ok(new { access_token = accessToken, token_type = "bearer", expires_in = 60 });
}
...
}
How To Test These Changes
Watch our video where we go ahead, make these changes and test them in Postman.
Not only do we check that we can still create a Bearer token, but we also have a look at the new validation checks that we have implemented.
Integrating OAuth Further
We can build on from this, by creating refresh tokens. This is good if you don't want your application to be authenticated every time the token has expired.
The idea is that the refresh token would work in a similar way to the authorisation code. It must be used by the same user, only be used once and have an expiry date. However, the major change is that you wouldn't generate a new authorisation code to generate a new token.
In addition, you can pass in a state to make sure that the generating of authorisation code and token are passed from the same device. If the state doesn't match when generating a token, it can throw an error.
You can read about the OAuth2 specifications by visiting the The OAuth 2.0 Authorization Framework.