How to Implement JWT Auth in Axum
· 7 min read
What is JWT
JWT, or JSON Web Token, is a compact and self-contained way of securely transmitting information between parties as a JSON object.
Here are the key points about JWT:
Structure
A JWT consists of three parts separated by dots (.):
- Header
- Payload
- Signature
The typical format looks like this: xxxxx.yyyyy.zzzzz 1
Components
- Header: Contains metadata about the token, such as the type of token and the hashing algorithm used.
- Payload: Contains claims or statements about the user and additional data.
- Signature: Ensures the token hasn't been altered. It's created by combining the encoded header, encoded payload, and a secret 1.
Use Cases
- Authentication: The most common scenario. Once a user logs in, each subsequent request includes the JWT, allowing access to routes, services, and resources permitted with that token 1.
- Information Exchange: JWTs can securely transmit information between parties, as they can be signed to ensure the sender's authenticity and that the content hasn't been tampered with 1.
Benefits
- Compact: JWTs can be sent through URLs, POST parameters, or inside HTTP headers, and are transmitted quickly due to their small size 1.
- Self-contained: The payload contains all the required information about the user, avoiding the need to query the database multiple times 1.
- Widely supported: JWTs are supported across different platforms and languages 2.
Security
- JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA 1.
- While JWTs can be encrypted to provide secrecy between parties, they are typically used as signed tokens 1.
Claims
JWTs contain claims, which are statements about the entity (typically the user) and additional data. There are three types of claims:
- Registered claims: Predefined claims like iss (issuer), exp (expiration time), sub (subject), aud (audience) 1.
- Public claims: Defined at will by those using JWTs 1.
- Private claims: Custom claims to share information between parties 1.
Workflow
- The application requests authorization from the authorization server.
- Upon authorization, the server returns an access token (JWT) to the application.
- The application uses the token to access protected resources (like APIs).
The code structure
➜ jwt-auth git:(main) tree .
.
├── Cargo.toml
├── README.md
├── create_blog.png
├─ ─ keys
│ ├── private.pem
│ └── public.pem
├── signin.png
├── src
│ ├── auth.rs
│ ├── config.rs
│ ├── errors.rs
│ ├── jwt.rs
│ ├── lib.rs
│ ├── main.rs
│ ├── middleware
│ │ └── mod.rs
│ └── models
│ ├── blog.rs
│ ├── mod.rs
│ └── user.rs
Dependency
Cargo.toml
[package]
name = "jwt-auth"
version = "0.1.0"
edition = "2021"
[dependencies]
jwt-simple = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
uuid ={ workspace = true }
anyhow = { workspace = true }
axum = {workspace = true}
axum-extra = { workspace = true}
tokio = { workspace = true }
thiserror = {workspace = true }
The models
models/mod.rs
pub mod blog;
pub mod user;
- blog.rs
use serde::Serialize;
use uuid::Uuid;
use super::user::User;
#[derive(Debug, Serialize)]
pub struct Blog {
id: String,
author: User,
title: String,
content: String,
}
impl Blog {
pub fn new(author: User, title: String, content: String) -> Self {
Self {
id: Uuid::new_v4().to_string(),
author,
title,
content,
}
}
}
- user.rs
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct User {
user_id: String,
user_name: String,
email: String,
}
impl User {
pub fn new(user_name: String, email: String) -> Self {
Self {
user_id: Uuid::new_v4().to_string(),
user_name,
email,
}
}
}
Generate the public(encode) and private(decode) keys
openssl genpkey -algorithm ed25519 -out private.pem
✗ cat private.pem
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEICOsPn3KRn4b6dSDm6BsWTMPLxr4DsydA72X2A7xhPHj
-----END PRIVATE KEY-----
openssl pkey -in private.pem -pubout -out public.pem
The Routers
in lib.rs
, we create the routes:
use axum::{
extract::State, http::StatusCode, middleware::from_fn_with_state, response::IntoResponse,
routing::post, Extension, Json, Router,
};
use config::AppConfig;
use errors::AppError;
use jwt::{DecodingKey, EncodingKey};
use middleware::verify_token;
use models::{blog::Blog, user::User};
use serde::{Deserialize, Serialize};
use std::{ops::Deref, sync::Arc};
pub mod auth;
pub mod config;
pub mod errors;
pub mod jwt;
pub mod middleware;
pub mod models;
#[derive(Clone)]
pub struct AppState {
state: Arc<AppStateInner>,
}
#[derive(Clone)]
pub struct AppStateInner {
pk: EncodingKey,
dk: DecodingKey,
}
impl AppState {
pub fn new(config: &AppConfig) -> Result<Self, AppError> {
let pk = EncodingKey::load(&config.private_pem)?;
let dk = DecodingKey::load(&config.public_pem)?;
Ok(Self {
state: Arc::new(AppStateInner { pk, dk }),
})
}
}
impl Deref for AppState {
type Target = AppStateInner;
fn deref(&self) -> &Self::Target {
&self.state
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SignInUser {
username: String,
password: String,
}
#[derive(Debug, Serialize)]
pub struct SignInResponse {
token: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateBlog {
title: String,
content: String,
}
pub async fn get_router(state: AppState) -> Result<Router, AppError> {
let api = Router::new()
.route("/blog", post(create_blog))
.layer(from_fn_with_state(state.clone(), verify_token::<AppState>))
.route("/signin", post(signin_handler))
.with_state(state);
Ok(api)
}
#[axum::debug_handler]
async fn signin_handler(
State(state): State<AppState>,
Json(input): Json<SignInUser>,
) -> Result<impl IntoResponse, AppError> {
if input.username.is_empty() {
return Ok((StatusCode::BAD_REQUEST, "invalid user name").into_response());
}
if input.password.len() < 10 {
return Ok((StatusCode::BAD_REQUEST, "too short password").into_response());
}
let user = User::new(input.username, input.password);
let token = state.pk.sign(user)?;
Ok((StatusCode::OK, Json(SignInResponse { token })).into_response())
}
#[axum::debug_handler]
async fn create_blog(
Extension(user): Extension<User>,
State(_state): State<AppState>,
Json(create_blog): Json<CreateBlog>,
) -> Result<impl IntoResponse, AppError> {
let blog = Blog::new(user, create_blog.title, create_blog.content);
Ok((StatusCode::OK, Json(blog)).into_response())
}