Cookbook / Auth and sessions
JWT authentication
Issue tokens at login and protect a router so handlers only run for valid bearer tokens.
Jwt issues and verifies signed tokens. Its middleware::<Claims>() rejects requests without a valid Authorization: Bearer ... header with 401, and puts your claims type into the request for handlers to read.
use axeon::auth::jwt::{now, Jwt};
use axeon::{Json, Router, Server, ServerError};
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)]
struct Claims {
sub: String,
exp: u64,
}
#[derive(Deserialize)]
struct Login {
username: String,
password: String,
}
fn main() {
// Load at least 32 random bytes from your configuration in real life.
let jwt = Jwt::hs256(b"load at least 32 random bytes from config!");
let mut app = Server::new();
app.state(jwt.clone());
app.post("/login", |req| async move {
let login: Login = req.json()?;
// Check the credentials against your user store here.
if login.password != "correct horse" {
return Err(ServerError::Unauthorized("wrong username or password".into()));
}
let jwt = req.state::<Jwt>().unwrap();
let claims = Claims { sub: login.username, exp: now() + 3600 };
Ok(Json(jwt.issue(&claims)?))
});
let mut api = Router::new();
api.middleware(jwt.middleware::<Claims>()); // 401 without a valid token
api.get("/me", |req| async move {
req.extension::<Claims>().map(|claims| claims.sub.clone())
});
app.mount("/api", api);
app.listen("127.0.0.1:3000").expect("server failed to start");
}
TOKEN=$(curl -s -X POST http://127.0.0.1:3000/login \
-H 'content-type: application/json' \
-d '{"username":"ada","password":"correct horse"}' | tr -d '"')
curl http://127.0.0.1:3000/api/me -H "authorization: Bearer $TOKEN" # ada
curl -i http://127.0.0.1:3000/api/me # 401
Notes#
- Always include
expin your claims. Expired tokens are rejected. - Hash passwords with Argon2 or bcrypt, never compare them in plain text as this demo does.
- For opaque tokens or API keys use
auth::BearerAuthwith your own async lookup, andauth::BasicAuthfor internal tools.