Search code examples
c#asp.net-web-apioauth-2.0access-tokenjwt

JWT and Web API (JwtAuthForWebAPI?) - Looking For An Example


I've got a Web API project fronted by Angular, and I want to secure it using a JWT token. I've already got user/pass validation happening, so I think i just need to implement the JWT part.

I believe I've settled on JwtAuthForWebAPI so an example using that would be great.

I assume any method not decorated with [Authorize] will behave as it always does, and that any method decorated with [Authorize] will 401 if the token passed by the client doesn't match.

What I can't yet figure out it how to send the token back to the client upon initial authentication.

I'm trying to just use a magic string to begin, so I have this code:

RegisterRoutes(GlobalConfiguration.Configuration.Routes);
var builder = new SecurityTokenBuilder();
var jwtHandler = new JwtAuthenticationMessageHandler
{
    AllowedAudience = "http://xxxx.com",
    Issuer = "corp",
    SigningToken = builder.CreateFromKey(Convert.ToBase64String(new byte[]{4,2,2,6}))
};

GlobalConfiguration.Configuration.MessageHandlers.Add(jwtHandler);

But I'm not sure how that gets back to the client initially. I think I understand how to handle this on the client, but bonus points if you can also show the Angular side of this interaction.


Solution

  • I ended-up having to take a information from several different places to create a solution that works for me (in reality, the beginnings of a production viable solution - but it works!)

    I got rid of JwtAuthForWebAPI (though I did borrow one piece from it to allow requests with no Authorization header to flow through to WebAPI Controller methods not guarded by [Authorize]).

    Instead I'm using Microsoft's JWT Library (JSON Web Token Handler for the Microsoft .NET Framework - from NuGet).

    In my authentication method, after doing the actual authentication, I create the string version of the token and pass it back along with the authenticated name (the same username passed into me, in this case) and a role which, in reality, would likely be derived during authentication.

    Here's the method:

    [HttpPost]
    public LoginResult PostSignIn([FromBody] Credentials credentials)
    {
        var auth = new LoginResult() { Authenticated = false };
    
        if (TryLogon(credentials.UserName, credentials.Password))
        {
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(new[]
                {
                    new Claim(ClaimTypes.Name, credentials.UserName), 
                    new Claim(ClaimTypes.Role, "Admin")
                }),
    
                AppliesToAddress = ConfigurationManager.AppSettings["JwtAllowedAudience"],
                TokenIssuerName = ConfigurationManager.AppSettings["JwtValidIssuer"],
                SigningCredentials = new SigningCredentials(new 
                    InMemorySymmetricSecurityKey(JwtTokenValidationHandler.SymmetricKey),
                    "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
                    "http://www.w3.org/2001/04/xmlenc#sha256")
                };
    
                var tokenHandler = new JwtSecurityTokenHandler();
                var token = tokenHandler.CreateToken(tokenDescriptor);
                var tokenString = tokenHandler.WriteToken(token);
    
                auth.Token = tokenString;
                auth.Authenticated = true;
            }
    
        return auth;
    }
    

    UPDATE

    There was a question about handling the token on subsequent requests. What I did was create a DelegatingHandler to try and read/decode the token, then create a Principal and set it into Thread.CurrentPrincipal and HttpContext.Current.User (you need to set it into both). Finally, I decorate the controller methods with the appropriate access restrictions.

    Here's the meat of the DelegatingHandler:

    private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
    {
        token = null;
        IEnumerable<string> authzHeaders;
        if (!request.Headers.TryGetValues("Authorization", out authzHeaders) || authzHeaders.Count() > 1)
        {
            return false;
        }
        var bearerToken = authzHeaders.ElementAt(0);
        token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
        return true;
    }
    
    
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpStatusCode statusCode;
        string token;
    
        var authHeader = request.Headers.Authorization;
        if (authHeader == null)
        {
            // missing authorization header
            return base.SendAsync(request, cancellationToken);
        }
    
        if (!TryRetrieveToken(request, out token))
        {
            statusCode = HttpStatusCode.Unauthorized;
            return Task<HttpResponseMessage>.Factory.StartNew(() => new HttpResponseMessage(statusCode));
        }
    
        try
        {
            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
            TokenValidationParameters validationParameters =
                new TokenValidationParameters()
                {
                    AllowedAudience = ConfigurationManager.AppSettings["JwtAllowedAudience"],
                    ValidIssuer = ConfigurationManager.AppSettings["JwtValidIssuer"],
                    SigningToken = new BinarySecretSecurityToken(SymmetricKey)
                };
    
            IPrincipal principal = tokenHandler.ValidateToken(token, validationParameters);
            Thread.CurrentPrincipal = principal;
            HttpContext.Current.User = principal;
    
            return base.SendAsync(request, cancellationToken);
        }
        catch (SecurityTokenValidationException e)
        {
            statusCode = HttpStatusCode.Unauthorized;
        }
        catch (Exception)
        {
            statusCode = HttpStatusCode.InternalServerError;
        }
    
        return Task<HttpResponseMessage>.Factory.StartNew(() => new HttpResponseMessage(statusCode));
    }
    

    Don't forget to add it into the MessageHandlers pipeline:

    public static void Start()
    {
        GlobalConfiguration.Configuration.MessageHandlers.Add(new JwtTokenValidationHandler());
    }
    

    Finally, decorate your controller methods:

    [Authorize(Roles = "OneRoleHere")]
    [GET("/api/admin/settings/product/allorgs")]
    [HttpGet]
    public List<Org> GetAllOrganizations()
    {
        return QueryableDependencies.GetMergedOrganizations().ToList();
    }
    
    [Authorize(Roles = "ADifferentRoleHere")]
    [GET("/api/admin/settings/product/allorgswithapproval")]
    [HttpGet]
    public List<ApprovableOrg> GetAllOrganizationsWithApproval()
    {
        return QueryableDependencies.GetMergedOrganizationsWithApproval().ToList();
    }