Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement JWT Refresh Tokens in asp.net core web api (no 3rd party)?

Tags:

I'm in the process of implementing a web api using asp.net core that is using JWT. I am not using a third party solution such as IdentityServer4 as I am trying to learn.

I've gotten the JWT configuration to work, but am stumped on how to implement refresh tokens for when the JWT's expire.

Below is some sample code in my Configure method inside startup.cs.

app.UseJwtBearerAuthentication(new JwtBearerOptions()
{
    AuthenticationScheme = "Jwt",
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    TokenValidationParameters = new TokenValidationParameters()
    {
        ValidAudience = Configuration["Tokens:Audience"],
        ValidIssuer = Configuration["Tokens:Issuer"],
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"])),
        ValidateLifetime = true,
        ClockSkew = TimeSpan.Zero
    }
});

Below is the Controller method used to generate the JWT. I have set expiration to 30 seconds for testing purposes.

    [Route("Token")]
    [HttpPost]
    public async Task<IActionResult> CreateToken([FromBody] CredentialViewModel model)
    {
        try
        {
            var user = await _userManager.FindByNameAsync(model.Username);

            if (user != null)
            {
                if (_hasher.VerifyHashedPassword(user, user.PasswordHash, model.Password) == PasswordVerificationResult.Success)
                {
                    var userClaims = await _userManager.GetClaimsAsync(user);

                    var claims = new[]
                    {
                        new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
                    }.Union(userClaims);

                    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.Key));
                    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

                    var token = new JwtSecurityToken(
                            issuer: _jwt.Issuer,
                            audience: _jwt.Audience,
                            claims: claims,
                            expires: DateTime.UtcNow.AddSeconds(30),
                            signingCredentials: creds
                        );

                    return Ok(new
                    {
                        access_token = new JwtSecurityTokenHandler().WriteToken(token),
                        expiration = token.ValidTo
                    });
                }
            }
        }
        catch (Exception)
        {

        }

        return BadRequest("Failed to generate token.");
    }

Would be very grateful for some guidance.

like image 685
DJDJ Avatar asked Apr 18 '17 08:04

DJDJ


1 Answers

First of all, you need to generate a refresh token and persist it somewhere. This is because you want to be able to invalidate it if needed. If you were to follow the same pattern as an access token - where all the data is contained within the token - a token that ends up in the wrong hands can the be used to generate new access tokens for the lifetime of the refresh token, which can be a really long time.

So what the heck do you need to persist?

You need a unique identifier of some sort that's not easy guessable, a GUID will do just fine. You also need the data to be able to issue a new access token, most likely a username. Having a username you can then skip the VerifyHashedPassword(...)-part but for the rest of it, just follow the same logic.

To obtain a refresh token, you normally use the scope "offline_access", this being something that you provide in your model (CredentialViewModel) when making a token request. Unlike a normal access token request, you don't need to provide your username and password, but instead the refresh token. When getting a request with a refresh token, you lookup the persisted identifier and issue a token for the user found.

Following is pseudo code for the happy path:

[Route("Token")]
[HttpPost]
public async Task<IActionResult> CreateToken([FromBody] CredentialViewModel model)
{
    if (model.GrantType is "refresh_token")
    {
        // Lookup which user is tied to model.RefreshToken
        // Generate access token from the username (no password check required)
        // Return the token (access + expiration)
    }
    else if (model.GrantType is "password")
    {
        if (model.Scopes contains "offline_access")
        {
            // Generate access token
            // Generate refresh token (random GUID + model.username)
            // Persist refresh token
            // Return the complete token (access + refresh + expiration)
        }
        else
        {
            // Generate access token
            // Return the token (access + expiration)
        }
    }
}
like image 54
naslund Avatar answered Sep 21 '22 08:09

naslund