Search code examples
c#asp.netauthenticationjwt

Using JWT with a secret key to handle Single Sign On


I have a running members only website that works as it should with login etc. I now need to support Single Sign On where I will recieve the username of the profile in a JWT token, and then log the user in. The external system that will send this request, does NOT know any other information than the Username - so the password is not shared in the token, nor is it possible.

JWT has been chosen as the method of choice to enable this. We are going to use a secret key to exchange the information, the secret key is ofcourse only know by the sender and reciever.

What I am looking for is a simple example of how this could look code-wise, both in the sender end and in the recieving end. The reciever end will be a WebAPI endpoint that will handle the login and then redirect to the frontpage of the members-only website if the login is successfull.

It is developed in C#.


Solution

  • Elementary tips

    • Usually and commonly, the user/password are exchanged for a new JWT

    • JWT should not store anything sensible like a password. Some companies like Microsoft store not a sensible values but a lot of information https://gist.github.com/jrichardsz/6512f9b2b4c38620bf00e24c71b6cf2d

    • For an enterprises, the encrypt and decrypt process should not be executed at the microservice layer. It should be performed by another server like: Auth0, okta, Keycloack, etc.

    • Your microservice only receives the token and send it to the security platform,. The best way to do this is with a middleware on any language. This is for c#

      enter image description here

    • The security platform returns a response with data in a syntax that client (c# in your case) understand if token is legit, valid, not expired, user exist, user is allowed to perform the operation, etc. I used this in some implementation in which I said: "User is allowed to perform this operation and this is its username"

      endpoint: acme-security.com/v1/oauth2/token/validate

      request: eyJ0eXAiOiJKV1Qi...

      response:

      {
      "isAllowed": true,
      "subject": "jane@blindspot.com"
      }
      
    • Also you can do this with an api gateway

      enter image description here

    • On a more real scenario, we need more than a simple jwt validation. We need to ensure that user admin/guest are allowed to execute specific endpoints. We don't want that a employee invoke directly the payment endpoint set its own salary :/

    • Follow the oauth2 spec if you will implement your own security platform

    • Jwt should be sent as Authorization Header

    • IMPORTANT: If you will implement your own jwt generation and validation, use ENV variables to hide it the secret and protect it as much as you can for your production environment

    JWT handling with c# netcore 5

    If the previous steps are understood, you only need one of these approaches:

    • Use some IAM or security platforms like auth0, okta, keycloack, etc
    • A simple c# web/microservice/api (that also functions as security platform) to receive token and return if user is allowed
    • A class to to the validation inside of the target microservice

    JWT Generation

    public string GenerateToken(User user)
    {
        // generate token that is valid for 7 days
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(SUPER_SECRET_KEY);
        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);
    }
    

    JWT Decode or validation

    public int? ValidateToken(string token)
    {
        if (token == null) 
            return null;
    
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(SUPER_SECRET_KEY);
        try
        {
            tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
                ClockSkew = TimeSpan.Zero
            }, out SecurityToken validatedToken);
    
            var jwtToken = (JwtSecurityToken)validatedToken;
            var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
    
            // return user id from JWT token if validation successful
            return userId;
        }
        catch
        {
            // return null if validation fails
            return null;
        }
    }
    

    Source: https://jasonwatmore.com/post/2021/06/02/net-5-create-and-validate-jwt-tokens-use-custom-jwt-middleware

    This code throws errors when token is expired or some buddy tries to send a jwt without your SUPER_SECRET_KEY

    You could use these snippets directly in your token generation and inside of your endpoints

    [HttpGet]
    public ActionResult<List<Dictionary<string, object>>> findAllEmployees()
    {
        //get jwt from http received headers
        validationResponse = ValidateToken(jwt)
        //if you have a security platform, callhere instead
        //ValidateToken(jwt)
        //if validationResponse.isAllowed ....
        var list = this.employeeService.findAllEmployees();
        return Ok(list);
    }
    

    or with a middleware like a pro, to keep clean your final controllers

    Lectures