So we want to secure our API layer right? we want to write something in one place that will take care of our authentication.
You can use a bearer token and validate each incoming request using a database lookup costly), or you can use JWT token. I like JWT because JWT can store any type of data, which is where it excels in combination with OAuth. With a JWT access token, you need far less database lookups while still not compromising security. You can also can add additional information that will help you avoid database lookups and just pull it from the token.
So here is how a JWT looks like
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0MyIsInVzZXJuYW1lIjoidGVzdDMiLCJ0bWlkIjoiYTA0MzdjYzQtOGQ0MS00YWE5LTk4ZjAtMmIwMjE4NzJmMjdhIiwidWlkIjoiMzkwZGNkMDMtYzgwMC00YTQ1LTVlMmItMDhkNzViZDNhOTZjIiwiZGlzcGxheSI6IlRlc3QgVXNlciIsInJvbGUiOiJBZG1pbiIsImp0aSI6Im15VG9rZW4tNjFjYjNmMWMtNjY2Yy00MTg0LTlmNGMtYTAxMjIxNWNmNDBkIiwibmJmIjoxNTcyMzg2NjYwLCJleHAiOjE1NzI0MDEwNjAsImlhdCI6MTU3MjM4NjY2MCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo2MTEwMy8iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjYxMTAzLyJ9.bSGhYLOcBR3kwxBAzF4kNme5ZJ3fI9qfJdMsyqHm7d0
And if you take this exact token and paste it on https://jwt.io/ you will see:
You can see on the right side that we can see all the claims (the data) on that token. So in this example, I have my display string for that user, and that saves me database lookups.
The reason we can avoid database lookups, is because JWT contains a base64 encoded version of the data you need to determine the identity and scope of access. When we generate the token, we use a signature (a secret). when reading the token, we must use the same secret. This comparison is way faster than accessing the database to check that the token exists.
Since we don’t have database lookups, we cannot revoke a token. A token is valid until it expires. Hence it is a good idea to have short expiration time for tokens. If you must be able to revoke a token immediately, this is not the best solution for you. It all depends on the system you are working on.
So now that we know what and why to use JWT token. Let’s see how to use it in .Net Core.
In your Startup.cs you will need to add:
public void AddAuthentication(IServiceCollection services) { services.AddAuthentication(option => { option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; option.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; } ).AddJwtBearer(options => { options.RequireHttpsMetadata = true; options.TokenValidationParameters = JWTConfigurationProvider.TokenValidationParameters(Configuration); }); }
And some code to get the TokenValidationParameters
private static TokenValidationParameters GetTokenValidationParameters(IConfiguration config) { return new TokenValidationParameters { AuthenticationType = AuthenticationType, ValidateLifetime = true, ValidateIssuer = true, ValidateAudience = true, ValidAudience = Audience(config), ValidIssuer = Issuer(config), IssuerSigningKey = SymmetricSecurityKey(config,"SigningKey") }; }
And now we need to add the Authorize attribute on our controller (or routes):
[Authorize] [Route("api/users")] [Produces("application/json")] public class UsersController : Controller {}
And now, every incoming call will be checked if allowed. meaning if the incoming token is what we expect and if still valid.
So how does it work with a client? First off we need to sign in to get a token. That route should not have the Authorize attribute since the user is not logged in yet.
We just call our route with the username and password AND with Basic Auth
The response will be the JWT token from the server.
On our next calls, we need to use that token by using Bearer Token as the type
Since our token expiration time is short, we need to expose a refresh token route. we can use a different route, or just use the same route that can accept both Basic (for the first time the user logs in) and Bearer (for refresh token).
Here is my route to get a token
You can s
[HttpPost] [BasicAuthentication] [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] [ProducesResponseType(typeof(List), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(List), StatusCodes.Status500InternalServerError)] public async Task Post() { string token = null; if (User.Identity.AuthenticationType == Core.Security.JWTConfigurationProvider.AuthenticationType) { var username = (User.Identity as ClaimsIdentity)?.FindFirst(Core.Security.ClaimTypes.Username)?.Value; if (username != null) { var roleClaim = (User.Identity as ClaimsIdentity)?.FindFirst(ClaimTypes.Role)?.Value; UserRole role = (UserRole)Enum.Parse(typeof(UserRole), roleClaim); token = await _service.GetTokenAsync(username, role); } } if (User.Identity.AuthenticationType == "Basic") { var username = (User.Identity as ClaimsIdentity)?.FindFirst(Core.Security.ClaimTypes.Username).Value; var password = (User.Identity as ClaimsIdentity)?.FindFirst(Core.Security.ClaimTypes.Password).Value; token = await _service.GetTokenAsync(username, password); } if (token.IsEmpty()) return Unauthorized(); return Ok(token); }
You can see that the same route can handle both authentication methods. but how?
The key is adding my own custom BasicAuthenticationAttribute
public class BasicAuthenticationAttribute : AuthorizeAttribute, IAsyncAuthorizationFilter { public async Task OnAuthorizationAsync(AuthorizationFilterContext authorizationFilterContext) { await Task.Run(() => Authenticate(authorizationFilterContext)); } private void Authenticate(AuthorizationFilterContext authorizationFilterContext) { var context = authorizationFilterContext.HttpContext; var authHeader = context.Request.Headers["Authorization"][0]; if (authHeader == null || !authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase)) { return; } var token = authHeader.Substring("Basic ".Length).Trim(); var credentialstring = Encoding.UTF8.GetString(Convert.FromBase64String(token)); var credentials = credentialstring.Split(':'); if (credentials.Length < 2) { authorizationFilterContext.Result = new UnauthorizedResult(); return; } var claims = new List{ new Claim(Core.Security.ClaimTypes.Username, credentials[0]), new Claim(Core.Security.ClaimTypes.Password, credentials[1]) }; var identity = new ClaimsIdentity(claims, "Basic"); authorizationFilterContext.HttpContext.User = new ClaimsPrincipal(identity); } }
This customer ”’AuthorizeAttribute”’ is reading the token. If the incoming token is Basic (meaning this is the initial log in), we pull the username and password and build a ClaimIdentity with those values.
If it is a JWT token, we do nothing and move on to the controller.
And as we saw before, the controller route is checking which authentication type is coming. If it is a JWT, it means we need to refresh the token (create a new token). If it is a basic type, we need to check that the username and password are valid and create the token.
So now we have one route to handle both refresh and create tokens. Sweet!