19

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.

abatishchev
  • 98,240
  • 88
  • 296
  • 433
Michael
  • 4,010
  • 4
  • 28
  • 49
  • Check my answer in here: http://stackoverflow.com/questions/40281050/jwt-authentication-for-asp-net-web-api/40284152#40284152 – cuongle Mar 10 '17 at 16:30

1 Answers1

21

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();
}
Sameer Alibhai
  • 3,092
  • 4
  • 36
  • 36
Michael
  • 4,010
  • 4
  • 28
  • 49
  • @Michael, how are you handling the decoding of the incoming token? – Mark Walsh Aug 12 '14 at 21:31
  • 1
    Thanks @Michael, great response. I don't know why there is no official documentation around this from MS... – Mark Walsh Aug 19 '14 at 10:09
  • @Michael what reference am I missing? LoginResult and Credentials are undefined – Sameer Alibhai Sep 17 '14 at 20:37
  • @SameerAlibhai - Those are my own POCOs. LoginResult is meant to communicate the result of attempting to login (the credentials were/weren't processed, and the user was/wasn't authenticated). Credentials are the users credentials - username/password etc. Use whatever is appropriate for your environment. – Michael Sep 17 '14 at 20:42
  • You can use Headers.Authorization.Scheme and Headers.Authorization.Parameter to simplify the TryRetrieveToken method. – Paul Hiles Feb 04 '15 at 16:37
  • @Michael What is `SymmetricKey`? Is it another `POCO`? If so, can you show that one as well please? – Dayan Apr 07 '15 at 15:39
  • @Dayan You need to sign the token so it can be validated when it's presented back. That could be done several ways, but a symmetric key is the easiest. It's just a key you keep in two different places - where you sign the token, and where you validate the token. As for what it is, it's just a random base64 encoded byte[]. Here's one I just generated: LYdTiahQpxMl4IdFpn5WKKjah0qavBgk – Michael Apr 07 '15 at 16:34
  • Very nice! Just one question - does this token support sliding expiration? – Pavle Gartner Mar 24 '16 at 07:26
  • 1
    @PavleGartner I'm not too familiar with sliding expiration but I don't believe this will support them as-is. – Michael Mar 28 '16 at 02:51
  • @Michael thanks! I implemented it by calling refresh endpoint like with OAuth (http://stackoverflow.com/questions/26739167/jwt-json-web-token-automatic-prolongation-of-expiration) – Pavle Gartner Mar 28 '16 at 07:22