Build a Secure API in Rust with JWT Authentication Using Axum
In today's world, securing APIs is a top priority for web developers, and one of the most effective methods of securing endpoints is JWT-based authentication. JSON Web Tokens (JWTs) are a compact and self-contained way to securely transmit information between parties, making them ideal for authenticating users in web applications.
In this tutorial, we'll walk you through creating a simple Axum API in Rust that leverages JWTs to authenticate users. By the end of the article, you'll have a working example where:
- A route generates a JWT.
- A protected route is secured using JWT authentication.
Let’s dive in!
Step 1: Setting Up Axum and Dependencies
Before starting, create a new Rust project:
cargo new jwt_axum_auth
cd jwt_axum_auth
Now, open the Cargo.toml
file and add these dependencies:
[dependencies]
axum = "0.6"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
jsonwebtoken = "7.2"
These dependencies include Axum (web framework), Tokio (async runtime), Serde (serialization), and jsonwebtoken (for handling JWTs). Run cargo build
to make sure everything is installed correctly.
Step 2: Building a Basic Axum Server
Let’s start by setting up a minimal Axum server with a simple route to ensure everything works.
In your src/main.rs
, add the following:
use axum::{Router, routing::get};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
// Create a basic router with a hello world endpoint
let app = Router::new().route("/", get(hello));
// Define the address the server will run on
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
// Start the server
println!("Server running at http://{addr}");
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn hello() -> &'static str {
"Hello, World!"
}
This code sets up a basic Axum app with one route (/
) that simply returns "Hello, World!"
. To run the app, execute:
cargo run
You should be able to visit http://localhost:3000
and see "Hello, World!" displayed.
Step 3: Implementing JWT Authentication
Now we’ll implement JWT creation and validation. JSON Web Tokens are used to securely transmit information between a client and server, allowing us to authenticate requests.
In the main.rs
file, add the following to handle the JWT logic:
use jsonwebtoken::{encode, decode, Header, Algorithm, EncodingKey, DecodingKey, Validation};
use serde::{Serialize, Deserialize};
use std::time::{SystemTime, UNIX_EPOCH};
// Define a struct to represent the claims in the JWT
#[derive(Serialize, Deserialize)]
struct Claims {
sub: String, // Subject (user ID or username)
exp: usize, // Expiration time
}
// Secret key for signing and verifying JWTs
const SECRET_KEY: &[u8] = b"secret";
// Function to generate a JWT
fn generate_jwt(sub: String) -> String {
let exp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600; // 1 hour expiration
let claims = Claims { sub, exp: exp as usize };
let header = Header::new(Algorithm::HS256);
let encoding_key = EncodingKey::from_secret(SECRET_KEY);
encode(&header, &claims, &encoding_key).unwrap()
}
// Function to validate a JWT
fn validate_jwt(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
let decoding_key = DecodingKey::from_secret(SECRET_KEY);
let validation = Validation::new(Algorithm::HS256);
decode::<Claims>(token, &decoding_key, &validation).map(|data| data.claims)
}
Explanation of JWT Handling:
- Claims: A struct that holds the JWT payload. Here, it contains the subject (
sub
), which could be the user ID or username, and the expiration time (exp
). - generate_jwt: A function that generates a JWT with a subject (like a username) and an expiration time (1 hour from the current time).
- validate_jwt: A function to decode and validate the JWT. If the token is valid and not expired, it returns the claims; otherwise, it returns an error.
Step 4: Securing Routes with JWT Authentication
Next, we’ll secure routes by adding a middleware that checks for a valid JWT before allowing access to certain endpoints.
Create the middleware function in main.rs
:
use axum::http::{Request, StatusCode};
use axum::middleware::Next;
use axum::response::Response;
async fn jwt_auth<B>(req: Request<B>, next: Next<B>) -> Result<Response, StatusCode> {
let headers = req.headers();
if let Some(auth_header) = headers.get("Authorization") {
if let Ok(auth_str) = auth_header.to_str() {
if let Some(token) = auth_str.strip_prefix("Bearer ") {
match validate_jwt(token) {
Ok(_) => return Ok(next.run(req).await), // JWT is valid, proceed to next handler
Err(_) => return Err(StatusCode::UNAUTHORIZED), // Invalid JWT
}
}
}
}
Err(StatusCode::UNAUTHORIZED) // No Authorization header or invalid token
}
- jwt_auth: This function checks the
Authorization
header for aBearer
token. If valid, the request is passed to the next handler; otherwise, it returns a401 Unauthorized
.
Now, we’ll modify the main app to apply this middleware to a protected route:
use axum::middleware::from_fn;
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/protected", get(protected)) // Protected route
.layer(from_fn(jwt_auth)) // Apply JWT authentication middleware
.route("/", get(hello))
.route("/generate", get(generate_jwt_route)); // Route to generate JWTs
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Server running at http://{addr}");
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn protected() -> &'static str {
"This is a protected route!"
}
// Route to generate JWT
async fn generate_jwt_route() -> String {
let token = generate_jwt("user123".to_string()); // Example user ID
format!("Generated JWT: {}", token)
}
What’s New:
- /protected: A route that is protected by JWT authentication. You must send a valid JWT to access it.
- /generate: A route that generates a JWT for the user
user123
and returns it in the response.
To generate a JWT, visit http://localhost:3000/generate
. You’ll receive a JWT token, which can be used in the Authorization
header as a Bearer
token to access the /protected
route.
Concepts and Explanations
-
JWT Basics: JWTs are compact, URL-safe tokens that contain three parts: the header, the payload, and the signature. They allow information exchange between a client and server in a secure manner.
-
Axum Middleware: Middleware in Axum runs before or after route handlers. In our case, it’s used to validate JWTs before accessing protected routes.
-
Expiration Handling: We set an expiration (
exp
) for the token to prevent unauthorized use beyond a certain period.
Challenges or Questions
- Modify the
/generate
route to accept a dynamic user ID via query parameters (e.g.,/generate?user_id=someuser
). - Implement a refresh token system where a user can refresh their JWT after it expires.
Recap and Conclusion
In this tutorial, we learned how to secure routes in an Axum API using JWTs. Here’s a recap of what we covered:
- Setting up Axum: We created a simple Axum server.
- JWT creation and validation: We implemented functions for generating and validating JWTs.
- Securing routes: We added middleware to protect routes requiring authentication.
- Generating JWTs: We created a route that allows us to generate a JWT programmatically.
This setup provides a foundation for adding secure authentication to your Axum APIs.
As a next step, you might want to explore adding refresh tokens, handling different user roles, or integrating with a real database to store user information.
Happy coding!