In this article, we’ll explore how to generate and verify JSON Web Tokens (JWTs) in Rust using the jsonwebtoken crate. While the library offers support for a range of cryptographic algorithms, we’ll focus specifically on the HS256 and RS256 algorithms.

Before diving into the implementation details, let’s take a moment to understand how these algorithms work and why RS256 is generally considered more secure than HS256.

HS256 (HMAC with SHA-256) is a symmetric algorithm that relies on a shared secret key to generate and verify the JWT signature. This key must be known to both parties involved in generating and verifying the JWT.

On the other hand, RS256 (RSA Signature with SHA-256) is an asymmetric algorithm that uses a public-private key pair. The JWT signature is generated using the private key, and the signature can be verified using the corresponding public key.

While anyone can verify the authenticity of the JWT using the public key, only the party with the private key can generate a valid signature. This makes RS256 more secure than HS256 because it’s harder for an attacker to guess the private key compared to a shared secret.

While we won’t cover every aspect of JWTs in this article, such as their structure and the different algorithms used for generating and verifying signatures, I’ll provide some helpful resources at the end of the article to help you continue your learning.

Our main focus here is on demonstrating how to use the HS256 and RS256 algorithms to sign and verify JWTs in Rust using the jsonwebtoken crate. By the end of this article, you should have a practical understanding of how to work with JWTs in your own Rust projects!

More practice:

Rust - How to Generate and Verify (JWTs) JSON Web Tokens

Run a Rust Project Built using HS256 Algorithm

If you are interested in learning more about how the HS256 algorithm can be used to implement authentication in a Rust application, you may refer to the article titled ‘JWT Authentication in Rust using Axum Framework‘. However, if you want to quickly try out a demo of the project without having to write any code, you can follow the steps outlined below:

  1. Download or clone the Rust Axum JWT authentication project from its GitHub repository at Once you have the source code, open it in your preferred IDE or text editor.
  2. In the root directory’s terminal, start a PostgreSQL server in a Docker container by executing the command docker-compose up -d.
  3. Install the SQLX CLI tool by running the command cargo install sqlx-cli and then apply the PostgreSQL database’s “up” migration script using the command sqlx migrate run.
  4. Install all the required crates and build the project by running the command cargo build.
  5. Start the Axum HTTP server by running the command cargo run in the terminal.
  6. To test the JWT authentication flow, import the Rust HS256 JWT.postman_collection.json file into Postman or the Thunder Client extension in Visual Studio Code. This file contains pre-defined HTTP requests for each of the API endpoints, making it easy to test the application without manually creating the request bodies and URLs.

Getting Started with a Rust Project that Uses RS256 Algorithm

If you’re interested in learning how to implement secure authentication using the RS256 algorithm in Rust, you can check out the detailed guide titled ‘Rust and Axum Framework: JWT Access and Refresh Tokens‘. However, if you’d like to quickly set up the project and see it in action, follow the steps outlined below:

  1. Download or clone the Rust Axum RS256 JWT project from its GitHub repository ( and open the source code in your preferred code editor.
  2. Launch the PostgreSQL, Redis, and pgAdmin servers in their respective Docker containers by running the command docker-compose up -d in the project’s root directory terminal.
  3. Install the SQLx command-line utility by running cargo install sqlx-cli if you haven’t already done so. Then, push the migration script to the PostgreSQL database by running sqlx migrate run.
  4. Install the required crates and build the project by running the command cargo build.
  5. Start the Axum HTTP server by running the command cargo run.
  6. To test the Axum RS256 JWT authentication flow, import the Postman collection named Rust_JWT_RS256.postman_collection.json into your Postman client or the Thunder Client VS Code extension. This collection includes pre-defined requests, their respective HTTP methods, and the necessary request bodies for the POST and PATCH requests.

How to Sign and Verify JWTs using HS256 Algorithm

In this section, we will cover the essential steps for signing and verifying JWTs using the HS256 algorithm. Fortunately, this process is straightforward, so we won’t go into every detail of the jsonwebtoken crate’s properties and methods. Instead, we’ll focus on the key steps for signing and verifying your JWTs with HS256.

Sign JWTs using the HS256 Algorithm

To sign a JWT with the HS256 algorithm using the jsonwebtoken crate in Rust, there are a few steps we need to follow. First, we need to define the token claims. These claims are predefined by the JWT standard, but we can also include our own custom claims. In Rust, we create a struct to hold the fields. Some common fields in a JWT claim include:

  • "iss" (Issuer): this issuer of the token
  • "sub" (Subject): the subject of the token
  • "aud" (Audience): the audience for the token
  • "exp" (Expiration Time): the expiration time of the token
  • "iat" (Issued At): the time the token was issued
  • "nbf" (Not Before): the time before which the token is not valid

We can name the struct TokenClaims or Claims. The struct must implement the serde::Serialize trait since the jsonwebtoken::encode() function expects a type that implements this trait for the token claims.

The second step is to define the JWT secret, which is typically stored in an environment variable. With the JWT secret and claims in place, we can create an instance of the TokenClaims struct with the required values for the fields.

Finally, we can call the jsonwebtoken::encode() function, which will encode the header and claims and sign the payload using the HS256 algorithm.

use serde::{Serialize, Deserialize};
use jsonwebtoken::{encode, EncodingKey, Header};

#[derive(Debug, Serialize, Deserialize)]
pub struct TokenClaims {
    pub sub: String,
    pub iat: usize,
    pub exp: usize,

let secret = "my_ultra_secure_jwt_secret";

 let now = chrono::Utc::now();
    let iat = now.timestamp() as usize;
    let exp = (now + chrono::Duration::minutes(60)).timestamp() as usize;
    let claims: TokenClaims = TokenClaims {

    let token = encode(

Calling the Header::default() function will return a JWT header with the default algorithm of HS256. Once we’ve provided the required arguments to the jsonwebtoken::encode() function, the function will return a Result containing the token that we can unwrap to get the token.

If your IDE or text editor supports IntelliSense, you can hover over each property and method to get a quick summary of what it does. Alternatively, you can visit the official jsonwebtoken crate documentation at to learn more about the available features and usage details.

Verify JWTs using the HS256 Algorithm

Now, let’s take a closer look at how we can extract the token claims by decoding and validating the JWT using the HS256 algorithm. Luckily, the jsonwebtoken crate provides a handy decode() function that can do just that.

To use this function, we need to make sure that our TokenClaims struct implements the serde::Deserialize trait since the jsonwebtoken::decode() function requires a type that implements this trait.

It’s worth noting that the jsonwebtoken::decode() function will return an error if the signature is invalid or the claims fail validation. Therefore, we need to handle these cases properly to prevent our application from crashing.

To decode and validate the JWT, we can simply call the jsonwebtoken::decode() function and pass in the token to decode, the secret key to use for decoding, and a set of validation options as arguments.

use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct TokenClaims {
    pub sub: String,
    pub iat: usize,
    pub exp: usize,

#[derive(Debug, Serialize)]
pub struct ErrorResponse {
    pub status: &'static str,
    pub message: String,

let token = "a.jwt.token".to_string();
let secret = "my_ultra_secure_jwt_secret";

let claims = decode::<TokenClaims>(
    .map_err(|_| {
        let json_error = ErrorResponse {
            status: "fail",
            message: "Invalid token".to_string(),
        (StatusCode::UNAUTHORIZED, Json(json_error))

In the code snippet above, we made use of the map_err method to handle any errors that may arise during decoding. In the event of a failure, the ErrorResponse will be returned, and the question mark operator (?) will pass on any errors that occur during decoding.

How to Sign and Verify JWTs using RS256 Algorithm

In this section, we will cover how to sign JWTs using the RS256 algorithm. However, before proceeding, we need to generate the private and public key pairs. There are several ways to generate these keys, including online tools such as the Online RSA Key Generator website, command-line tools like OpenSSL, or specialized software like PuTTYgen.

If you have the flexibility to choose your preferred method, I suggest using the online tool since it is easy to use and doesn’t require any installation on your system.

Generate the Public and Private Key Pairs Online

Let’s start by generating the asymmetric keys, which consist of a private and a public key. It’s important to note that when generating the keys, a key length of 4096 bits should be used instead of 1024 bits or 2048 bits. This is because the jsonwebtoken crate has been tightening its security, and using a key length of 1024 bits will result in an error.

To generate the asymmetric keys, follow the steps outlined below:

  1. Visit the Online RSA Key Generator website and select a key size of 4096 bits. Then, click on the “Generate New Keys” button and wait for a moment as the keys are generated.
  2. Once the keys are generated, you’ll need to copy the private key and convert it to base64 format using a tool like After encoding the key, paste it as the value of the ACCESS_TOKEN_PRIVATE_KEY field in a .env file.
  3. Similarly, copy the public key corresponding to the private key from the Online RSA Key Generator website. Convert the public key to base64 format using the same tool and paste the resulting base64-encoded key as the value of the ACCESS_TOKEN_PUBLIC_KEY field.
  4. To create the private and public keys for the refresh token, repeat the steps mentioned above for generating the access token. Then, encode the private key to base64 format and add it to the .env file as the value of the REFRESH_TOKEN_PRIVATE_KEY field.
  5. Add the base64-encoded public key corresponding to the private key as the value of the REFRESH_TOKEN_PUBLIC_KEY field.

Note that we convert the private and public keys to base64 format to prevent unnecessary warnings in the terminal when Docker Compose retrieves credentials from the .env file.

After following these steps, your .env file should look similar to the example below.




Generate the Asymmetric Keys with OpenSSL

Although I recommend using the online tool to generate the asymmetric keys, you can also generate them using OpenSSL, which is included by default in many Linux distributions. However, you’ll need to install OpenSSL as a standalone binary if you are a Windows user.

Once you generate the keys, be sure to encode them in base64 format before adding them to the environment variables file. To generate a private key of length 4096 and write it to a file called private_key.pem, run the following command:

openssl genrsa -out private_key.pem 4096

To view the contents of the private_key.pem file, you can use the command cat private_key.pem. To generate the corresponding public key, you can use the following command which outputs the key into a public_key.pem file:

openssl rsa -in private_key.pem -pubout -out public_key.pem

Create Utility Functions to Sign and Verify the Tokens

Now that we have the asymmetric keys for the access and refresh tokens, we’ll create two essential utility functions: generate_jwt_token and verify_jwt_token. These functions will respectively use the generated private and public keys to sign and verify JWTs.

But before we do that, we need to define two Rust structs that will hold the metadata and claims of the JWT. The TokenDetails struct will contain the metadata of the JWT and will be returned by both utility functions. The TokenClaims struct will define the JWT claims as specified in the JWT standard.

To get started, let’s create a new Rust file called inside the src directory and add the following code to it:


use base64::{engine::general_purpose, Engine as _};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Serialize, Deserialize)]
pub struct TokenDetails {
    pub token: Option<String>,
    pub token_uuid: uuid::Uuid,
    pub user_id: uuid::Uuid,
    pub expires_in: Option<i64>,

#[derive(Debug, Serialize, Deserialize)]
pub struct TokenClaims {
    pub sub: String,
    pub token_uuid: String,
    pub exp: i64,
    pub iat: i64,
    pub nbf: i64,

Sign the Tokens using the Private Keys

Let’s start with the generate_jwt_token function. It takes in a user ID, a time-to-live (TTL) value for the token, and a private key as input parameters. In this function, we first decode the base64-encoded private key back to a UTF8 string.

After decoding the base64-encoded private key, we create instances of the TokenDetails and TokenClaims structs that respectively hold the metadata and claims of the JWT. These structs are then passed to the jsonwebtoken::encode() function, which generates the JWT using the RS256 algorithm.

Finally, we add the generated token to the TokenDetails struct and return it from the function. Here’s the code for the function:


pub fn generate_jwt_token(
    user_id: uuid::Uuid,
    ttl: i64,
    private_key: String,
) -> Result<TokenDetails, jsonwebtoken::errors::Error> {
    let bytes_private_key = general_purpose::STANDARD.decode(private_key).unwrap();
    let decoded_private_key = String::from_utf8(bytes_private_key).unwrap();

    let now = chrono::Utc::now();
    let mut token_details = TokenDetails {
        token_uuid: Uuid::new_v4(),
        expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp()),
        token: None,

    let claims = TokenClaims {
        sub: token_details.user_id.to_string(),
        token_uuid: token_details.token_uuid.to_string(),
        exp: token_details.expires_in.unwrap(),
        iat: now.timestamp(),
        nbf: now.timestamp(),

    let header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256);
    let token = jsonwebtoken::encode(
    token_details.token = Some(token);

Verify the Tokens using the Public Keys

To validate JWTs and extract their payloads, we need to implement the verify_jwt_token function. This function takes a public key and a token as input parameters. The first step is to decode the base64-encoded public key back to a UTF8 string.

After decoding the public key, we create a new instance of the jsonwebtoken::Validation struct, where we specify the RS256 algorithm used in signing the JWT.

Then, we call the jsonwebtoken::decode() function, which decodes the JWT using the provided public key and validation parameters. The result is stored in a decoded variable, which contains the claims of the JWT.

As the ‘sub‘ and ‘token_uuid‘ fields of the TokenClaims struct are stored as strings, we need to convert them into UUID types. To do this, we use the Uuid::parse_str() function and assign the parsed values to the ‘user_id‘ and ‘token_uuid‘ variables, respectively.

Finally, we create a new instance of the TokenDetails struct with the parsed user_id and token_uuid values, set the expires_in and token fields to ‘None‘, and return it from the function.

Here is the implementation of the verify_jwt_token function:


pub fn verify_jwt_token(
    public_key: String,
    token: &str,
) -> Result<TokenDetails, jsonwebtoken::errors::Error> {
    let bytes_public_key = general_purpose::STANDARD.decode(public_key).unwrap();
    let decoded_public_key = String::from_utf8(bytes_public_key).unwrap();

    let validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);

    let decoded = jsonwebtoken::decode::<TokenClaims>(

    let user_id = Uuid::parse_str(;
    let token_uuid = Uuid::parse_str(;

    Ok(TokenDetails {
        token: None,
        expires_in: None,

Use the Utility Functions to Sign and Verify JWTs

At this point, we have done 90% of the work by isolating the code required to sign and verify JWTs into a separate module. With this modular approach, we can now easily use the utility functions to sign access and refresh tokens, and validate them as necessary.

Typically, the code for signing JWTs would be placed in a login route handler function, while the code for verifying JWTs would be placed in a middleware function.

Sign the JWT using the RS256 Algorithm

To sign the access and refresh tokens, we need to read the private keys and their respective max-age values from the .env file. Instead of repeating the code to retrieve the environment variables, we’ll create a utility function called get_env_var() to handle this for us. In a typical Rust project, the code for retrieving environment variables would be in a separate src/ file, but for simplicity, we will keep it in the same file below.

Once we have the RSA keys, we can create a function called generate_token() that will use the token::generate_jwt_token() function to sign the JWTs. This allows us to avoid duplicating code for both the access and refresh tokens.

Finally, we will call the generate_token() function twice, once for the access token and once for the refresh token, to generate both JWTs.

fn get_env_var(var_name: &str) -> String {
    std::env::var(var_name).unwrap_or_else(|_| panic!("{} must be set", var_name))

let access_token_private_key = get_env_var("ACCESS_TOKEN_PRIVATE_KEY");
let access_token_max_age = get_env_var("ACCESS_TOKEN_MAXAGE");

let refresh_token_private_key = get_env_var("REFRESH_TOKEN_PRIVATE_KEY");
let refresh_token_max_age = get_env_var("REFRESH_TOKEN_MAXAGE");

fn generate_token(
    user_id: uuid::Uuid,
    max_age: i64,
    private_key: String,
) -> Result<TokenDetails, (StatusCode, Json<serde_json::Value>)> {
    token::generate_jwt_token(user_id, max_age, private_key).map_err(|e| {
        let error_response = serde_json::json!({
            "status": "error",
            "message": format!("error generating token: {}", e),
        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))

let access_token_details = generate_token(,

let refresh_token_details = generate_token(,

Verify the JWT using the RS256 Algorithm

Now that we have the code for signing the access and refresh tokens, we can move on to implementing the code that will validate and extract their payloads. Our solution is to use the token::verify_jwt_token() function, which we created earlier.

To get started, we first need to obtain the public keys from the .env file. Once we have the public keys, we will call the token::verify_jwt_token() function twice, once for the access token and once for the refresh token.

use crate::token;

fn get_env_var(var_name: &str) -> String {
    std::env::var(var_name).unwrap_or_else(|_| panic!("{} must be set", var_name))

let access_token_public_key = get_env_var("ACCESS_TOKEN_PUBLIC_KEY");
let refresh_token_public_key = get_env_var("REFRESH_TOKEN_PUBLIC_KEY");

let access_token = "a.jwt.access.token".to_string();
let access_token_details =
    match token::verify_jwt_token(access_token_public_key.to_owned(), &access_token) {
        Ok(token_details) => token_details,
        Err(e) => {
            let error_response = ErrorResponse {
                status: "fail",
                message: format!("{:?}", e),
            return Err((StatusCode::UNAUTHORIZED, Json(error_response)));

let refresh_token = "a.jwt.refresh.token".to_string();
let refresh_token_details =
    match token::verify_jwt_token(refresh_token_public_key.to_owned(), &refresh_token)
        Ok(token_details) => token_details,
        Err(e) => {
            let error_response = serde_json::json!({
                "status": "fail",
                "message": format_args!("{:?}", e)
            return Err((StatusCode::UNAUTHORIZED, Json(error_response)));

While I’ve addressed errors in the match blocks using Axum-specific code, it’s worth noting that you’ll want to handle any potential errors resulting from the token::verify_jwt_token() function in a way that’s appropriate for your chosen web framework.

Helpful Resources

Here’s a list of useful resources that can help you deepen your understanding of JWTs and the associated risks. If you have any additional resources to suggest, feel free to leave them in the comments below and I’ll add them to the list.


That concludes our guide to generating and verifying JWTs in Rust using the HS256 and RS256 algorithms, as well as exploring various methods for generating RSA keys. I hope you found this tutorial useful and enjoyable. If you have any questions or feedback, please leave them in the comments below. Thank you for reading!