Create a Web API with Custom Authentication and ASP.NET Core Identity

Hemant SinghHemant Singh
11 min read

Hey folks! So, in a previous article, we created a Web API with JWT token authentication and ASP.NET Core Identity. However, in some scenarios, we may not necessarily be open to using JWT tokens. Rather, we may want our own custom tokens. For such scenarios, we need to create our own custom token generator, as well as custom authentication handler. Let us see how to do that!

If you are in a bit of a hurry, you can find the final code here.

.NET version: 6.0 (SDK version 6.0.8)

IDE: VS Code

I will not generate the entire application from scratch here. What I recommend is to visit the previous article link to create the application with JWT, and then come back here and make the changes on top of that.

Current authentication logic

So right now, our authentication middleware in Program.cs looks like this

builder.Services.AddAuthentication(options => {
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(options =>
                {
                    options.RequireHttpsMetadata = false;
                    options.SaveToken = true;
                    options.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidateIssuer = true,
                        ValidateIssuerSigningKey = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidAudience = builder.Configuration["token:audience"],
                        ValidIssuer = builder.Configuration["token:issuer"],
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["token:key"])),
                        // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
                        ClockSkew = TimeSpan.Zero
                    };
                });

The token generation logic looks like this. As you can see, we are creating a JWT Token based on claims.

private async Task<AuthResponse> GetTokens(User user)
{
            //create claims details based on the user information
            var claims = new[] {
                        new Claim(JwtRegisteredClaimNames.Sub, _configuration["token:subject"]),
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                        new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()),
                        new Claim("UserId", user.Id),
                        new Claim("UserName", user.UserName),
                        new Claim("Email", user.Email)
                    };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["token:key"]));
            var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                _configuration["token:issuer"],
                _configuration["token:audience"],
                claims,
                expires: DateTime.UtcNow.AddMinutes(Convert.ToDouble(_configuration["token:accessTokenExpiryMinutes"])),
                signingCredentials: signIn);
            var tokenStr = new JwtSecurityTokenHandler().WriteToken(token);

            var refreshTokenStr = GetRefreshToken();
            var authResponse = new AuthResponse { AccessToken = tokenStr, RefreshToken = refreshTokenStr };
            return await Task.FromResult(authResponse);
}

When receiving a token refresh request, we fetch the ClaimsPrincipal from expired token like this:

[HttpPost("refresh")]
public async Task<IActionResult> Refresh(RefreshRequest request)
{
        . . . 

        //fetch email from expired token string
        var principal = GetPrincipalFromExpiredToken(request.AccessToken);
        var userEmail = principal.FindFirstValue("Email"); //fetch the email claim's value

        . . .
}

public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = false, //you might want to validate the audience and issuer depending on your use case
            ValidateIssuer = false,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["token:key"])),
            ValidateLifetime = false //here we are saying that we don't care about the token's expiration date
        };
        var tokenHandler = new JwtSecurityTokenHandler();
        var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken);
        var jwtSecurityToken = securityToken as JwtSecurityToken;
        if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
            throw new SecurityTokenException("Invalid token");
        return principal;
}

The token validation logic (verifying that the auth token present in an incoming request is valid) is handled by the JWT middleware automatically, we did not have to write any code for that.

Changes for custom authentication logic

Below are the changes we will need to make in order to switch to custom authentication logic

  1. Modify token generation logic to generate a custom token

  2. Create an AuthenticationSchemeOptions derived class

  3. Create an AuthenticationHandler derived class to handle token validation

  4. Finally, register both the scheme and authentication handler classes in Program.cs

1. Modify token generation logic to generate a custom token

Create Custom Security Token model

Currently, we are generating a token of type JwtSecurityToken (you can see its source code here). Let us change the logic to create a custom token, let us call it as CustomSecurityToken. It will inherit from SecurityToken which is an abstract class provided in .NET as a generic template for a token. You don't necessarily need to inherit from SecurityToken class, you can have your own custom model entirely if you wish so.

The JWT token keeps the contents of token public so that anybody can view it in plaintext. However, it has an encrypted signature that allows the server to verify that the token is indeed valid. However, for our custom token, we will keep it simple by encrypting the whole token so that nobody can read the token contents except for our server. Your token requirements may be different, so you can adjust the logic accordingly.

The SecurityToken class looks like this

 public abstract class SecurityToken
 {
        protected SecurityToken();
        public abstract string Id { get; }
        public abstract string Issuer { get; }
        public abstract SecurityKey SecurityKey { get; }
        public abstract SecurityKey SigningKey { get; set; }
        public abstract DateTime ValidFrom { get; }
        public abstract DateTime ValidTo { get; |}
  }

Our CustomSecurityToken will inherit from this class

public class CustomSecurityToken : SecurityToken
{
    public override string Id { get; }
    public override string Issuer  { get; }
    public override SecurityKey SecurityKey  { get; }
    public override SecurityKey SigningKey { get; set; }
    public override DateTime ValidFrom { get; }
    public override DateTime ValidTo { get; }    
    public string Audience { get; }
    public string IEnumerable<Claim> Claims { get; }
}

I have added additional properties Audience and Claims. As you can see, all properties except SecurityKey are kept as get only - this is to prevent anyone from modifying these values once a token is created.

Now let us fill out this class. First we will add private variables that will store the values of each public field. Next, we will add a constructor that will initialize all these private variables. We will add [JsonIgnore] to a few properties that we are not using so that they are not included in the serialized token later.

public class CustomSecurityToken : SecurityToken
{

    private string _id, _issuer, _audience;
    private DateTime _validFrom, _validTo;
    private Dictionary<string, string> _claims;

    [JsonIgnore]
    public override string Id { get { return _id; } }

    public override string Issuer { get { return _issuer; } }

    [JsonIgnore]
    public override SecurityKey SecurityKey { get; }  //not going to use this property

    [JsonIgnore]
    public override SecurityKey SigningKey { get; set; }   //not going to use this property

    [JsonIgnore]
    public override DateTime ValidFrom { get { return _validFrom; } }   //not going to use this property

    public override DateTime ValidTo { get { return _validTo; } }

    public string Audience { get { return _audience; } }
    public Dictionary<string, string> Claims { get { return _claims; } }

    public CustomSecurityToken(string issuer, string audience, Dictionary<string, string> claims, DateTime expires)
    {
        _issuer = issuer;
        _audience = audience;
        _claims = claims;
        _validFrom = DateTime.Now;
        _validTo = expires;
    }
}

Create Custom Security Token model handler

Just like for JWT token, a JwtSecurityTokenHandler is provided, similarly, for our custom security token, we will create a CustomSecurityTokenHandler class. This class will have the code to encrypt and decrypt a token.

public class CustomSecurityTokenHandler
{

       public string GetEncryptedString(CustomSecurityToken token, string key)
       {
            //encryption code here
       }

       public CustomSecurityToken GetDecryptedToken(string tokenString, string key)
       {
            //decryption code here
       } 
}

The encryption logic looks like shown below.

  • First we are serializing the token object into a JSON string.

  • Next, we are using AES object to perform symmetric encryption on this string, which gives us a byte array (symmetric means both encryption and decryption is done using the same secret key).

  • Finally, we are encoding this byte array into a Base64 string.

// To generate encrypted string of this token
public string GetEncryptedString(CustomSecurityToken token, string key)
{
            var jsonSerializedToken = JsonSerializer.Serialize(token);
            byte[] iv = new byte[16];
            byte[] array;

            using (Aes aes = Aes.Create())
            {
                aes.Key = Encoding.UTF8.GetBytes(key);
                aes.IV = iv;

                ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

                using (MemoryStream memoryStream = new MemoryStream())
                {
                    using (CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, encryptor, CryptoStreamMode.Write))
                    {
                        using (StreamWriter streamWriter = new StreamWriter((Stream)cryptoStream))
                        {
                            streamWriter.Write(jsonSerializedToken);
                        }

                        array = memoryStream.ToArray();
                    }
                }
            }

            return Convert.ToBase64String(array);
}

The decryption logic looks like shown below.

  • First, we are decoding the base64 string into its equivalent byte array.

  • Next, we are decrypting the byte array to the original json string.

  • Once we have the json string, ideally we should deserialize it into an object of CustomSecurityToken class. However, since all of the properties of this class are get only, it is not possible to set their values at the time of deserialization, resulting in a token object with all properties null.

  • To overcome this issue, we first convert the json string to a dictionary of key-value, then we read the values of issuer, audience, claims etc. Finally, we create a token object by passing these values in a constructor.

public CustomSecurityToken GetDecryptedToken(string tokenString, string key)
{
            byte[] iv = new byte[16];
            byte[] buffer = Convert.FromBase64String(tokenString);

            using (Aes aes = Aes.Create())
            {
                aes.Key = Encoding.UTF8.GetBytes(key);
                aes.IV = iv;
                ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

                using (MemoryStream memoryStream = new MemoryStream(buffer))
                {
                    using (CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, decryptor, CryptoStreamMode.Read))
                    {
                        using (StreamReader streamReader = new StreamReader((Stream)cryptoStream))
                        {
                            var jsonTokenString = streamReader.ReadToEnd();
                            var tokenParams = JsonSerializer.Deserialize<Dictionary<string, dynamic>>(jsonTokenString);
                            var issuer = tokenParams["Issuer"].ToString();
                            var audience = tokenParams["Audience"].ToString();
                            var validTo = JsonSerializer.Deserialize<DateTime>(tokenParams["ValidTo"]);
                            var claims = JsonSerializer.Deserialize<Dictionary<string,string>>(tokenParams["Claims"]);
                            var token = new CustomSecurityToken(issuer, audience, claims, validTo);
                            return token;
                        }
                    }
                }
            }
}

Modify the GetTokens() function in the login flow

Now that we have our CustomSecurityToken, we should modify the GetTokens function that generates access and refresh tokens for an authenticated user. The main changes here are replacing JwtSecurityToken with CustomSecurityToken and JwtSecurityTokenHandler.WriteToken() with CustomSecurityTokenHandler.EncryptToken().

 private async Task<AuthResponse> GetTokens(User user)
    {
            //create claims details based on the user information
            var claims = new[] {
                        new Claim("Subject", _configuration["token:subject"]),
                        new Claim("Id", Guid.NewGuid().ToString()),
                        new Claim("Iat", DateTime.UtcNow.ToString()),
                        new Claim("UserId", user.Id),
                        new Claim("UserName", user.UserName),
                        new Claim("Email", user.Email)
                    };
            var claimsDictionary = claims.ToDictionary(c => c.Type, c => c.Value);
            var key = _configuration["token:key"];
            var token = new CustomSecurityToken(
                _configuration["token:issuer"],
                _configuration["token:audience"],
                claimsDictionary,
                expires: DateTime.UtcNow.AddMinutes(Convert.ToDouble(_configuration["token:accessTokenExpiryMinutes"])));
            var tokenStr = new CustomSecurityTokenHandler().GetEncryptedString(token, key);
            var refreshTokenStr = GetRefreshToken();
            var authResponse = new AuthResponse { AccessToken = tokenStr, RefreshToken = refreshTokenStr };
            return await Task.FromResult(authResponse);
    }

Modify the GetPrincipalFromExpiredToken() function in the refresh token flow

The JwtSecurityTokenHandler was providing us with a ClaimsPrincipal object. However, for our custom token, we will have to create the principal from scratch. Also, since this function is going to be used at multiple places, we will move it to our CustomSecurityTokenHandler class and remove the word Expired from the function's name.

public ClaimsPrincipal GetPrincipalFromToken(string tokenString, IConfiguration config)
{
            var key = config["token:key"];

            var token = GetDecryptedToken(tokenString, key);
            // create claims array from the model
            var claims = token.Claims.Select(c => new Claim(c.Key, c.Value)).ToList();

            // generate claimsIdentity on the name of the class
            var claimsIdentity = new ClaimsIdentity(claims,
                        nameof(CustomSecurityTokenHandler));

            // generate principal
            var principal = new ClaimsPrincipal(claimsIdentity);

            return principal;
}

2. Create an AuthenticationSchemeOptions derived class

Next, we will create an AuthenticationSchemOptions derived class. For now, we are not adding any body in the class, as we want to inherit all the default functionality of base class. But in future, if needed we can override the base class behavior.

public class CustomAuthenticationSchemeOptions : AuthenticationSchemeOptions
{

}

The base class structure looks like this

public class AuthenticationSchemeOptions
    {
        public AuthenticationSchemeOptions();

        public string? ClaimsIssuer { get; set; }

        public object? Events { get; set; }

        public Type? EventsType { get; set; }

        public string? ForwardDefault { get; set; }

        public string? ForwardAuthenticate { get; set; }

        public string? ForwardForbid { get; set; }

        public string? ForwardSignOut { get; set; }

        public Func<HttpContext, string?>? ForwardDefaultSelector { get; set; }

        public virtual void Validate();

        public virtual void Validate(string scheme);
    }

3. Create an AuthenticationHandler derived class to handle token validation

Now we will need to create and implement a class from AuthenticationHandler. This class will intercept all incoming request, and validate the token whether it is valid or not. When we were using JWT, this was being taken care of automatically by the JWT middleware. But now that we have our own custom token, we will have to add this function.

You may also notice that this handler's constructor is taking the CustomAuthenticationSchemeOptions as one of the params.

public class CustomAuthHandler
        : AuthenticationHandler<CustomAuthenticationSchemeOptions>
{
    public CustomAuthHandler(
        IOptionsMonitor<CustomAuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // handle authentication logic here
    }
}

Let us now add code to the HandleAuthenticateAsync method. This method will be triggered whenever a secured endpoint is accessed. We will fetch the Authorization header and read the token string from it. Then we will decrypt the token and validate the email id and token expiry. If both look good, we will proceed to generate a principal object using the token. Finally, we will generate an AuthenticationTicket and pass it on to the middleware.

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
        try
        {
            if (!Request.Headers.ContainsKey(HeaderNames.Authorization))
            {
                return AuthenticateResult.Fail("Header Not Found.");
            }

            var header = Request.Headers[HeaderNames.Authorization].ToString();

            string tokenString = header.Substring("bearer".Length).Trim();

            var key = _config["token:key"];
            var tokenHandler = new CustomSecurityTokenHandler();

            var token = tokenHandler.GetDecryptedToken(tokenString, key);
            var email = token.Claims["Email"];

            var isTokenExpired = token.ValidTo < DateTime.Now;
            var userNotExist = await _userManager.FindByEmailAsync(email) == null;

            if(isTokenExpired || userNotExist)
            {
                return AuthenticateResult.Fail($"Unauthorized");               
            }  

            //if user is authenticated, control will reach here
            var principal = tokenHandler.GetPrincipalFromToken(tokenString, _config);

            // generate AuthenticationTicket from the Identity
            // and current authentication scheme
            var ticket = new AuthenticationTicket(principal, this.Scheme.Name);

            // pass on the ticket to the middleware
            return AuthenticateResult.Success(ticket);
        }
        catch(Exception ex)
        {
            return AuthenticateResult.Fail($"Unauthorized: {ex.Message}");
        }
}

4. Finally, register both the scheme and authentication handler classes in Program.cs

This is the final step, where we register our custom authentication handler with the middleware.

We will go to below section in Program.cs

builder.Services.AddAuthentication(options => {
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(options =>
                {
                    . . .
                });

and replace it with these lines

builder.Services
        .AddAuthentication(options => {
                    options.DefaultAuthenticateScheme = "MyCustomScheme";
                    options.DefaultChallengeScheme = "MyCustomScheme";
                    options.DefaultScheme = "MyCustomScheme";
                })
        .AddScheme<CustomAuthenticationSchemeOptions, CustomAuthHandler>(
            "MyCustomScheme", options => { });

Basically we are giving our custom authentication scheme a name MyCustomScheme and also declaring it to be the default scheme to be used. This way, any time [Authorized] attribute is used, it will trigger our custom auth scheme.

Test with Postman

image.png

Now we will use the token generated in previous step to access the protected Articles endpoint

image.png

Next - .NET MVC App Calling Web API for Authentication

0
Subscribe to my newsletter

Read articles from Hemant Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Hemant Singh
Hemant Singh