0

I am writing C# code that runs against an Azure cloud. My application is an ASP.NET Core web service that exposes methods but no UI.

Most (not all) of my methods require a JSON Web Token (JWT) for authorization. I have this in my Startup.cs:

services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
    .AddIdentityServerAuthentication(options =>
    {
        options.Authority = Configuration["AppSettings:IdentityAuthEndpoint"];
        options.RequireHttpsMetadata = bool.Parse(Configuration["AppSettings:RequireHttps"]);
        options.ApiName = "data";
        options.EnableCaching = true;
    });

This throws an error if the JWT is missing or completely bad. But if the JWT is just expired, the above lets the user through.

According to this Stack Overflow answer, I can check that context.User.Claims is empty: https://stackoverflow.com/a/62289801/4290962

That seems to be true. But is there a standard best-practice way to detect this and throw a standardized error? Or do I need to write custom code to check the claims? I can do that, of course. I'm just thinking there ought to be a prettier solution.

Thanks in advance!

ProgrammingLlama
  • 36,677
  • 7
  • 67
  • 86
Claus Appel
  • 1,015
  • 10
  • 28

2 Answers2

1

I would recommend you to write a middleware for this, This will give you better control on what you want to achieve. Like you said it's good to look for an existing solution, but here's one that I'm using in one of my enterprise application.

Intercept Token - Using this middleware on every request

public class JwtMiddleware
{
    private readonly RequestDelegate _next;

    public JwtMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, IUserService userService)
    {
        var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
        if (token != null) AttachUserToContext(context, userService, token);
        await _next(context);
    }

    private void AttachUserToContext(HttpContext context, IUserService userService, string token)
    {
        try
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Encoding.ASCII.GetBytes(AppConfigBuilder.Build().Security.JwtSecret);
            tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                ClockSkew = TimeSpan.Zero
            }, out SecurityToken validatedToken);
            var jwtToken = (JwtSecurityToken)validatedToken;
            var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
            context.Items["User"] = userService.GetUserById(userId);
        }
        catch
        {
        }
    }
}

Register it on StartUp

app.UseMiddleware<JwtMiddleware>();

Then create an Authentication filter

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var user = (User)context.HttpContext.Items["User"];
        if (user == null)
        {
            context.Result = new JsonResult(new Response {
                IsSuccess = false,
                ResponseStatus = ResponseStatus.ERROR,
                Message="Request terminated. Unauthorized access to protected resource.", 
                Info=new List<string>() { 
                    "Verify the auth token sending through this request",
                    "Verify if your token is invalid or expired", 
                    "Request for a new token by logging in again" } 
                })
            { StatusCode = StatusCodes.Status401Unauthorized };
        }
    }
}

That's all need to do. Now for any endpoint to be secured, Just use [Authorize] attribute like this. This will validate your token and emit appropriate standard response.

    [HttpGet]
    [Authorize]
    public IActionResult Get()
    {
        //Boilerplate code
        var response = userService.GetAllUsers();
        return (response.IsSuccess) ? Ok(response) : BadRequest(response);
    }

Just for info: You can define your own standard Response class on Authorize filter. The one I'm using is from ExpressGlobalExceptionHandler library.

This is the JWT token generation logic I've created

    private string GenerateJwtToken(User user)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(appConfig.Security.JwtSecret);
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }),
            Expires = DateTime.UtcNow.AddDays(7),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }
Sangeeth Nandakumar
  • 1,362
  • 1
  • 12
  • 23
  • Thanks. This is similar to what I have, actually. Can you please explain exactly where in your code you verify that the token is not expired? Is that in the call to `tokenHandler.ValidateToken`? – Claus Appel Jan 29 '21 at 12:55
  • Yes. ValidateToken is an InBuilt function inside Microsoft's JwtSecurityTokenHandler. It checks for token validation, expiry and it's signature, so you don't need to worry about manually writing a validation – Sangeeth Nandakumar Jan 29 '21 at 13:08
  • @ClausAppel More specifically, During creation of JWT token, I'm setting expiry. The validate method will internally validates it. I've edited the answer and added JWT creation method for you just to refer – Sangeeth Nandakumar Jan 29 '21 at 13:11
  • Now it throws an exception saying: "Signature validation failed. Unable to match key". How does it know how to validate the signature? Do I need to tell it where to find the expected signing key (like I did above with `option.Authority`)? How do I do that? – Claus Appel Jan 29 '21 at 14:33
  • "Signature validation failed. Unable to match key" - This error says it can't validate the token. While you generate a JWT token you should pass a "JwtSecret". That should be 126bit. Can you share your JWT generation and validation logic? I'm assuming something wrong there. For me I'm loading JWT secret from appsettings.json – Sangeeth Nandakumar Jan 29 '21 at 14:40
  • I do not have the code that generates the JWT. I'll need to ask a colleague about that. But I have two questions: 1. You say you read your JWT secret from settings. Is that this line: `AppConfigBuilder.Build().Security.JwtSecret`? 2. Are you recommending that I have BOTH this filter AND my original `AddIdentityServerAuthentication` call, or do you recommend that I remove the call to `AddIdentityServerAuthentication`? – Claus Appel Feb 01 '21 at 08:11
0

I ended up doing the below, based on this answer: https://stackoverflow.com/a/34423434/4290962

Note that _rejectExpiredJwt is a configurable boolean.

public JwtSecurityToken ValidateToken(string tokenStr)
{
    var rsa = new RSACryptoServiceProvider();
    rsa.ImportParameters(
        new RSAParameters
        {
            Modulus = FromBase64Url(_modulus),
            Exponent = FromBase64Url(_exponent)
        });

    var validationParameters = new TokenValidationParameters
    {
        RequireExpirationTime = true,
        RequireSignedTokens = true,
        ValidateAudience = false,
        ValidateIssuer = true,
        ValidIssuer = _issuer,
        ValidateLifetime = _rejectExpiredJwt,
        IssuerSigningKey = new RsaSecurityKey(rsa)
    };

    var handler = new JwtSecurityTokenHandler();
    handler.ValidateToken(tokenStr, validationParameters, out SecurityToken validatedSecurityToken);
    var validatedJwt = validatedSecurityToken as JwtSecurityToken;
    return validatedJwt ?? throw new UnauthorizedException(
        $"Token is not a {nameof(JwtSecurityToken)} but a {validatedSecurityToken.GetType().Name}.");

    static byte[] FromBase64Url(string base64Url)
    {
        string padded = base64Url.Length % 4 == 0
            ? base64Url
            : base64Url + "====".Substring(base64Url.Length % 4);
        string base64 = padded.Replace("_", "/")
            .Replace("-", "+");
        return Convert.FromBase64String(base64);
    }
}
Claus Appel
  • 1,015
  • 10
  • 28