In modern web development, security is paramount. Rust, known for its safety and performance, offers the Axum framework for building web applications. Implementing robust authentication mechanisms is essential to protect user data and maintain application integrity. This guide provides a step-by-step approach to setting up Axum authentication for secure Rust web apps.

Understanding Axum and Authentication

Axum is a web framework for Rust that emphasizes modularity and ease of use. It allows developers to build scalable and secure web services. Authentication in Axum can be achieved through middleware, which intercepts requests to verify user credentials before granting access to protected resources.

Prerequisites

  • Rust installed on your development machine
  • Basic knowledge of Rust and Axum framework
  • Experience with HTTP and web security concepts
  • Dependencies: axum, tower, tokio, serde, serde_json, jsonwebtoken

Setting Up the Project

Create a new Rust project and add dependencies in Cargo.toml:

[package]
name = "axum-auth-example"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
jsonwebtoken = "8.1"
tower = "0.4"

Implementing JWT Authentication

JSON Web Tokens (JWT) are a popular method for stateless authentication. We'll generate tokens upon login and verify them for protected routes.

Creating the Token

Define a struct for user claims and a function to create tokens:

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

#[derive(Serialize)]
struct Claims {
    sub: String,
    exp: usize,
}

fn create_jwt(user_id: &str) -> String {
    let expiration = chrono::Utc::now()
        .checked_add_signed(chrono::Duration::hours(24))
        .expect("valid timestamp")
        .timestamp() as usize;

    let claims = Claims {
        sub: user_id.to_owned(),
        exp: expiration,
    };

    encode(&Header::default(), &claims, &EncodingKey::from_secret(b"secret"))
        .expect("Token creation failed")
}

Verifying the Token

Implement middleware to verify JWTs on protected routes:

use axum::{
    async_trait,
    extract::{FromRequest, RequestParts},
    http::StatusCode,
    response::IntoResponse,
    middleware::Next,
};
use jsonwebtoken::{decode, Validation, DecodingKey, TokenData};
use serde::Deserialize;

#[derive(Deserialize)]
struct Claims {
    sub: String,
    exp: usize,
}

struct AuthenticatedUser {
    user_id: String,
}

#[async_trait]
impl FromRequest for AuthenticatedUser
where
    B: Send,
{
    type Rejection = (StatusCode, String);

    async fn from_request(req: &mut RequestParts) -> Result {
        let auth_header = req.headers()
            .and_then(|headers| headers.get("authorization"))
            .and_then(|value| value.to_str().ok());

        if let Some(header_value) = auth_header {
            if header_value.starts_with("Bearer ") {
                let token = &header_value[7..];
                let token_data: TokenData = decode(
                    token,
                    &DecodingKey::from_secret(b"secret"),
                    &Validation::default(),
                ).map_err(|_| (StatusCode::UNAUTHORIZED, "Invalid token".to_string()))?;

                return Ok(AuthenticatedUser {
                    user_id: token_data.claims.sub,
                });
            }
        }
        Err((StatusCode::UNAUTHORIZED, "Missing or invalid authorization header".to_string()))
    }
}

Protecting Routes with Middleware

Apply middleware to routes that require authentication:

use axum::{
    routing::get,
    Router,
};

async fn protected_route(user: AuthenticatedUser) -> String {
    format!("Hello, {}!", user.user_id)
}

fn app() -> Router {
    Router::new()
        .route("/protected", get(protected_route))
        .layer(axum::middleware::from_fn_with_state(
            (),
            |req, next| {
                let fut = AuthenticatedUser::from_request(req);
                async move {
                    match fut.await {
                        Ok(user) => next.run(req).await,
                        Err((status, msg))) => {
                            let response = axum::response::Response::builder()
                                .status(status)
                                .body(msg.into())
                                .unwrap();
                            response
                        }
                    }
                }
            }
        ))
}

Testing the Authentication

Start the server and test with curl:

curl -H "Authorization: Bearer your_jwt_token" http://localhost:3000/protected

If the token is valid, you'll receive a personalized greeting. Otherwise, you'll get an unauthorized error.

Conclusion

Implementing JWT authentication in Axum enhances your web application's security by ensuring only authorized users can access sensitive routes. Combining middleware with token verification provides a scalable and stateless authentication solution suitable for modern web services built with Rust.