JWT Authentication Mechanism Principles and Implementation Examples (C#)


March 6, 2023(Last updated: June 21, 2023) Program

JWT Authentication Mechanism Principles and Implementation Examples (C#)


JWT (JSON Web Token) is an open standard used for securely transmitting information between parties. JWT uses JSON objects to represent the messages to be transmitted and uses digital signatures or encryption to protect these messages.

JWT is commonly used for quickly setting up login authentication for lightweight websites. Here is a Record the usage of implementing JWT in C#.

Introduction

🔗

JWT

Encoded

Consisting of three parts, namely Header, Payload, and Signature.
Using the example provided on the official website directly:

Header part includes data such as the encryption algorithm and type used by JWT.

JSON
{
  "alg": "HS256",
  "typ": "JWT"
}

Header is a JSON object that contains the following two properties:

  • alg: Indicates the encryption algorithm used, such as HS256, RS256, etc.
  • typ:Indicates the type, which is usually set to JWT.

Payload part contains the information to be transmitted.

Payload is generally also a JSON object and can contain custom properties. Some basic customer information, etc. are typically stored in the Payload part.

JSON
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Standard properties that can be included in the Payload are:

  • iss: The issuer of the JWT, indicating who issued the JWT.
  • sub: The subject of the JWT, indicating the entity (usually the user) that the JWT represents.
  • aud: The audience of the JWT, indicating who can use the JWT.
  • exp: The expiration time of the JWT, indicating when the JWT expires and can no longer be used.
  • nbf: The time when the JWT becomes valid, indicating when the JWT becomes effective.
  • iat: The time when the JWT was issued, indicating when the JWT was issued.
  • jti: The unique identifier of the JWT, used to prevent the JWT from being reused.

The properties listed above are some standards defined in the JWT specification, but custom names can also be defined to represent specific information in the application.

Signature part is the result of digitally signing the Header and Payload.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
'your-256-bit-secret'
)

The header and payload are concatenated using a period .as a delimiter, and the your-256-bit-secret is the custom string (private key) stored on the server. The resulting string, which combines these three parts, is then encrypted using a cryptographic algorithm for generating a signature.

The signature can ensure that the JWT has not been tampered with. The calculation method of the signature varies depending on the encryption algorithm used, with commonly used algorithms including HMAC and RSA.

Principle

🔗

In simple terms, a string is generated using the Header and Payload, with each string separated by a .. Then, a private key is used to sign the string, generating a JWT that includes the Header, Payload, and Signature.

When verifying the JWT, the receiver can decode the Header and Payload, and then use the same key to verify the Signature to ensure that the JWT has not been tampered with. If the verification is successful, the information contained in the JWT can be trusted.

Creating JWT Example (.NET 6)

🔗
C#
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;

// Set the signing key for JWT
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("MySuperSecretKey"));
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

var claims = new[]
{
            new Claim(JwtRegisteredClaimNames.Sub, "user123"), // Set the username
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) //  Set the JWT ID
        };

// Set the expiration time of JWT to 1 hour
var expires = DateTime.UtcNow.AddHours(1);

// Create JWT object
var token = new JwtSecurityToken(
    issuer: "MyApp",
    audience: "MyClient",
    claims: claims,
    expires: expires,
    signingCredentials: signingCredentials);

// Convert the object to a JWT string
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);

// Print the JWT string
Console.WriteLine(tokenString);

Validate JWT Example (.NET 6)

🔗
C#
//  Get the JWT carried from the front-end
string tokenString = "{insert JWT string here}";

// Set the validation parameters for JWT
var validationParameters = new TokenValidationParameters
{
    // Set the issuer and audience of JWT
    ValidIssuer = "MyApp",
    ValidAudience = "MyClient",
    // Set the signing key and encryption algorithm for JWT validation
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("MySuperSecretKey")),
    ValidateIssuerSigningKey = true,
    ValidateLifetime = true,
    ClockSkew = TimeSpan.Zero
};

try
{
    //  Validate the JWT 
    var jwtHandler = new JwtSecurityTokenHandler();
    var principal = jwtHandler.ValidateToken(tokenString, validationParameters, out var validatedToken);
    var jwtToken = validatedToken as JwtSecurityToken;

    //  Get the username and JWTID
    var username = jwtToken.Claims.First(x => x.Type == JwtRegisteredClaimNames.Sub).Value;
    var jwtId = jwtToken.Claims.First(x => x.Type == JwtRegisteredClaimNames.Jti).Value;

    // Additional validation can be performed here, 
    //such as checking if the user exists in the database or 
    //if the user has permission to access resources, etc.

    // Validation succeeded
    Console.WriteLine($"Token validated. Username: {username}, JWT ID: {jwtId}");
}
catch (SecurityTokenException e)
{
    // Validation failed
    Console.WriteLine($"Token validation failed: {e.Message}");
}

The issuer and receiver of a JWT.

  • Issuer (iss): Used to indicate who issued the JWT, typically the name or domain of the organization or application that identifies the JWT. When verifying the JWT, we can check if the issuer of the JWT matches the expected value to ensure that the JWT was issued by the correct organization or application.

  • Audience (aud): Used to indicate who can use the JWT, typically the name or domain of the application or API that identifies the JWT user. When verifying the JWT, we can check if the audience of the JWT matches the expected value to ensure that the JWT can be used by the correct application or API.


Try it by myself

🔗

The above is an implementation of JWT-related functionalities using the .NET Core library directly. Recently, I received a requirement to generate one-time download URLs for files in order to prevent the leakage of file URLs. After considering the options, I plan to use JWT to implement this feature.

The thought process is to generate a one-time download URL without creating a burden on the database. To achieve this, a time-limited JWT is generated. Additionally, JWT verification is added to the API that provides file downloads. If the JWT has expired, the download is not allowed, ensuring the one-time usability of the URL.

The flow of this feature is as follows:

  1. Generate a JWT using the file's ID (GUID).
  2. Generate a one-time download URL that includes the JWT.
  3. When downloading via the URL, verify the expiration of the JWT.
  4. Determine whether to allow the download based on the JWT validation.

Creating Objects for Header and Payload

C#
public class oJWT_Header
    {
        public string alg { get; set; }
        public string typ { get; set; }
    }
C#
public class oJWT_Payload
    {
        public long exp { get; set; }
        public long iat { get; set; }
        public Guid id { get; set; }
    }

For the payload, I primarily require the expiration time (exp) and the unique identifier (id) of the file.

Encode JWT

🔗
C#
public static string JWT_Encoder(Guid fileID)
    {
        oJWT_Header header = new oJWT_Header();
        header.alg = "HS256";
        header.typ = "JWT";

        oJWT_Payload payload = new oJWT_Payload();
        DateTimeOffset now = DateTimeOffset.UtcNow;
        long unixTimestamp = now.ToUnixTimeSeconds();
        long unixTimestamp_exp = now.AddSeconds(20).ToUnixTimeSeconds();
        payload.iat = unixTimestamp;
        payload.exp = unixTimestamp_exp;
        payload.id = fileID;

        string json_header = JsonConvert.SerializeObject(header);
        byte[] b_header = System.Text.UTF8Encoding.UTF8.GetBytes(json_header);
        string b64_header = Convert.ToBase64String(b_header);

        string json_payload = JsonConvert.SerializeObject(payload);
        byte[] b_pl = System.Text.UTF8Encoding.UTF8.GetBytes(json_payload);
        string b64_payload = Convert.ToBase64String(b_pl);

        string encryptSignature = ComputeHMACSHA256(b64_header + b64_payload, b64_payload + payload.id);
        string token = b64_header + "." + b64_payload + "." + encryptSignature;
        
        return token;
    }

Header Object should include the algorithm (alg) and token type (typ) being used.

Payload Object should include the JWT issuance time (iat), expiration time (exp), and file ID (id). The times should be converted to Unix time based on UTC. The expiration time should be the current time plus 20 seconds.

Next, encode the Header and Payload separately using Base64.

Then, encrypt the encoded Header and Payload using HMACSHA256. For the private key, I have chosen to use the Encoded Payload + fileID. This ensures that the private key for each URL is unique and does not need to be hardcoded on the server side.

HMACSHA256 encryption

🔗
C#
public static string ComputeHMACSHA256(string data, string key)
    {
        var keyBytes = Encoding.UTF8.GetBytes(key);
        using (var hmacSHA = new HMACSHA256(keyBytes))
        {
            var dataBytes = Encoding.UTF8.GetBytes(data);
            var hash = hmacSHA.ComputeHash(dataBytes, 0, dataBytes.Length);
            return BitConverter.ToString(hash).Replace("-", "").ToUpper();
        }
    }

After obtaining the encrypted signature using HMACSHA256, the JWT becomes: Encoded Header.Encoded payload.encrypted signature

Decode JWT

🔗
C#
public static bool JWT_Decoder(string JWT)
    {
        string[] ary = JWT.Split('.');
        if (ary.Length != 3) return false;
        else
        {
            string b64_header = ary[0];
            string b64_payload = ary[1];
            string Signature = ary[2];

            Byte[] ary_header = Convert.FromBase64String(b64_header);
            string json_header = System.Text.UTF8Encoding.UTF8.GetString(ary_header);
            oJWT_Header header = JsonConvert.DeserializeObject<oJWT_Header>(json_header);

            Byte[] ary_payload = Convert.FromBase64String(b64_payload);
            string json_pl = System.Text.UTF8Encoding.UTF8.GetString(ary_payload);
            oJWT_Payload payload = JsonConvert.DeserializeObject<oJWT_Payload>(json_pl);

            string encryptSignature = ComputeHMACSHA256(b64_header + b64_payload, b64_payload + payload.id);

            if (!Signature.Equals(encryptSignature)) return false;

            long exp = payload.exp;
            long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            if (now > exp) return false;
        }
        return true;
    }

The verification process is straightforward. Initially, since the JWT format uses periods . as separators, I first split the encoded token using .. If the resulting array length is not equal to 3, it means it doesn't comply with the format, and then return false.

Next, I divide the array into the base64-encoded header, base64-encoded payload, and the signature. I decode the base64-encoded header and payload to extract the relevant information. Then, I verify if the signature matches the decoded data. If it doesn't match, it indicates that the JWT has been tampered with, and then return false.

The final step is to verify the token's expiration. Since the token has a predefined expiration time of 20 seconds after its creation, I compare the "exp" (expiration) claim in the payload with the current timestamp. Returns false if the token has expired.

The above process completes the functionality of generating one-time URLs using JWT. The payload can be customized to include additional parameters for convenient usage, such as querying file paths for downloading files from an API.

Conclusion

🔗

I currently used JWT only for lightweight website login authentication, but in fact, JWT has other uses such as resource authorization, single sign-on (SSO), timed services, inter-application communication, etc. By distributing time-limited JWTs and verifying them each time whenever resources are accessed, its application can be more widely extended.

CsharpWeb



Avatar

Alvin

Software engineer, interested in financial knowledge, health concepts, psychology, independent travel, and system design.

Related Posts






Discussion (0)



  or   

No comments yet.