Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core 3.1 - How to persist JWT Tokens once Authenticated

I have been trying to get JWT Authentication working and it is not entirely clear how this needs to be done, and what the best ways are to do this in ASP.NET Core 3.1.

I was using Cookie based authentication which I assume is tied to the session id, which is tied to the running server instance. If I want to use multiple servers with different IP addresses and ports, I assume that cookies would no longer work and therefor require something else that can be validated across systems.

I have been following various web examples but it is not clear what to do beyond the point where I have a JWT Token once the user has been "Authenticated" - Logged In. Once users are logged in they can access any part of the system via: html links (the menu).

How do I pass the tokens around with all subsequent requests?

Do I redirect the user to a Welcome page after the user has been authenticated and store the token in the browser sessionStore or localStorage or Cookie? What is the best way to deal with this.

options.success = function (obj) {
     sessionStorage.setItem("token", obj.token);
     sessionStorage.setItem("userName",$("#userName").val());
}

HTTP HEADERS

Would the Authorization HTTP Header variable work and would this be sent around in all subsequent requests by the browser, acting as the HTTP client. How long does this HTTP header last, is it lost once the TCP socket is closed? How do I set this HTTP Header Variable in ASP.NET Core 3.1? Would the server then use this Header to validate the token, and also pass it on again for use in subsequent requests?

Currently I have this, which returns the token in the body once the user is authenticated:

        var claims = await GetClaims(user);
        var token = GenerateSecurityToken(claims);

        return Ok(new { Token = token })

AJAX CALLS

I have several forms and several AJAX calls, how do implement this as a manual approach seems rather tedious.

Is there a way to get the JWT token from a hidden form variable similar to the AntiForgery token @Html.AntiForgeryToken() as used in all my Ajax calls?

jQuery using the hidden form variable:

request = $.ajax({
    async: true,
    url: url,
    type: "POST",
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    headers: {
        RequestVerificationToken:
        $('input:hidden[name="__RequestVerificationToken"]').val()
    },
    WHAT DO I ADD FOR JWT ? 
    data: JSON.stringify(data)
}).done(function() {
    completion();
}).fail(function() {
    // fail
});

HTML FORMS

I have Razor Pages and have some forms which then POST back to the controllers. How do I include the token?

CONTROLLERS

Is there anything else that needs to be performed when using JWT besides what I have in my Startup.cs? I know I need to deal with Token refreshes but I will leave for a seperate question.

LINKS FROM THE MENU - HTTP GET

I could manipulated the menu / links presented to the user, by adding the token to the end of the URL, but how should this be done?

like image 611
Wayne Avatar asked Mar 30 '20 02:03

Wayne


1 Answers

After quite a bit of reading I found some answers along with a working solution.

HTTP HEADERS Once you have the token, the token needs to be persisted in order to get access to the system. Using HTTP headers to store the token is not going to persist as the HTTP protocols 1.0 and 1.1 and 1.2 will close the TCP socket at some point along with the state it had, the token. Not ideal for WebClients where you do not control Http connections, but could be used for Mobile development, Android or IOS were you can control the HttpHeaders.

LOCAL STORAGE You could use the browsers localStorage or sessionStorage, but these have some security risks where JavaScript can read the values - XSS attack.

COOKIES Another option is to store the token within the Cookie; The cookie will be passed along with every http request, and nothing special on the client side needs to happen regarding this. This method is not prone to XSS attacks. But is prone to CSRF. But again CORS can help with this.

It is also best to set the Cookie to be HttpOnly, this way the cookie will only be delivered over HTTPS. Read more here

Here is my implementation based on an article I found here

Startup.cs ConfigureServices...

        // openssl rand -hex 16 => 32 bytes when read
        var jwt_key = Configuration.GetSection("JwtOption:IssuerSigningKey").Value;
        var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwt_key));
        var tokenValidationParameters = new TokenValidationParameters
            {
                // The signing key must match!
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = signingKey,

                // Validate the JWT Issuer (iss) claim
                ValidateIssuer = true,
                ValidIssuer = "some uri",

                // Validate the JWT Audience (aud) claim
                ValidateAudience = true,
                ValidAudience = "the web",

                // Validate the token expiry
                ValidateLifetime = true,

                // If you want to allow a certain amount of clock drift, set that here:
                ClockSkew = TimeSpan.Zero
            };

        services.AddSingleton(tokenValidationParameters);

        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
        {
            int minute = 60;
            int hour = minute * 60;
            int day = hour * 24;
            int week = day * 7;
            int year = 365 * day;

            options.LoginPath = "/auth/login";
            options.AccessDeniedPath = "/auth/accessdenied";
            options.Cookie.IsEssential = true;
            options.SlidingExpiration = true;
            options.ExpireTimeSpan = TimeSpan.FromSeconds(day/2);

            options.Cookie.Name = "access_token";

            options.TicketDataFormat = new CustomJwtDataFormat(
                SecurityAlgorithms.HmacSha256,
                tokenValidationParameters);
        });

CustomJwtDataFormat This will be validating our tokens.

public class CustomJwtDataFormat :ISecureDataFormat<AuthenticationTicket>
{
    private readonly string algorithm;
    private readonly TokenValidationParameters validationParameters;

    public CustomJwtDataFormat(string algorithm, TokenValidationParameters validationParameters)
    {
        this.algorithm = algorithm;
        this.validationParameters = validationParameters;
    }

    public AuthenticationTicket Unprotect(string protectedText)
        => Unprotect(protectedText, null);

    public AuthenticationTicket Unprotect(string protectedText, string purpose)
    {
        var handler = new JwtSecurityTokenHandler();
        ClaimsPrincipal principal = null;
        SecurityToken validToken = null;

        try
        {
            principal = handler.ValidateToken(protectedText, this.validationParameters, out validToken);

            var validJwt = validToken as JwtSecurityToken;

            if (validJwt == null)
            {
                throw new ArgumentException("Invalid JWT");
            }

            if (!validJwt.Header.Alg.Equals(algorithm, StringComparison.Ordinal))
            {
                throw new ArgumentException($"Algorithm must be '{algorithm}'");
            }

            // Additional custom validation of JWT claims here (if any)
        }
        catch (SecurityTokenValidationException e)
        {
            System.Console.WriteLine(e);
            return null;
        }
        catch (ArgumentException e)
        {
            System.Console.WriteLine(e);
            return null;
        }

        // Validation passed. Return a valid AuthenticationTicket:
        return new AuthenticationTicket(principal, new AuthenticationProperties(), "Cookie");
    }

    // This ISecureDataFormat implementation is decode-only
    public string Protect(AuthenticationTicket data)
    {
        throw new NotImplementedException();
    }

    public string Protect(AuthenticationTicket data, string purpose)
    {
        throw new NotImplementedException();
    }
}

LoginController After the username and password is validated, call SignInUser

    private string GenerateSecurityToken(List<Claim> claims)
    {  
        var tokenHandler = new JwtSecurityTokenHandler();
        var expire = System.DateTime.UtcNow.AddMinutes(userService.GetJwtExpireDate());
        var tokenDescriptor = new SecurityTokenDescriptor
        {  
            Subject = new ClaimsIdentity(claims),
            Expires = expire,
            SigningCredentials = new SigningCredentials(tokenValidationParameters.IssuerSigningKey, SecurityAlgorithms.HmacSha256Signature),
            Audience = tokenValidationParameters.ValidAudience,
            Issuer = tokenValidationParameters.ValidIssuer
        };  

        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }

    private async Task<List<Claim>> GetClaims(UserModel user) {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Name, user.Email),
            new Claim(ClaimTypes.Email, user.Email),
        };

        // add roles
        var roleList = await userService.UserRoles(user.Email);
        foreach (var role in roleList)
        {
            var claim = new Claim(ClaimTypes.Role, role.Role);
            claims.Add(claim);
        }

        return claims;
    }

    private async Task<IActionResult> SignInUser(UserModel user, bool rememberMe)
    {
        var claims = await GetClaims(user);
        var token = GenerateSecurityToken(claims);

        // return Ok(new { Token = token });
        // HttpContext.Request.Headers.Add("Authorization", $"Bearer {token}");

        // HttpContext.Response.Cookies.Append(
        HttpContext.Response.Cookies.Append("access_token", token, new CookieOptions { HttpOnly = true, Secure = true }); 
        return RedirectToAction("Index", "Home", new { area = "" });
    }
like image 71
Wayne Avatar answered Nov 11 '22 10:11

Wayne