When you opt into JWT auth, the scaffold generates a complete stateless authentication system using jsonwebtoken.

What gets generated

src/
├── auth/
│   ├── mod.rs            # JWT encode/decode, Claims struct
│   ├── middleware.rs     # Axum extractor — verifies token on protected routes
│   └── handlers.rs       # POST /auth/register, POST /auth/login
└── models/
    └── user.rs           # User model with hashed password storage

Endpoints

MethodPathDescription
POST/auth/registerCreate a new user account
POST/auth/loginReturn a signed JWT

Register

curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "hunter2"}'

Login

curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "hunter2"}'

# {"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}

Protecting a route

Use the AuthUser extractor on any handler:

use crate::auth::middleware::AuthUser;

pub async fn my_protected_handler(
    AuthUser(claims): AuthUser,
    State(state): State<AppState>,
) -> Result<Json<MyResponse>, AppError> {
    // claims.sub is the authenticated user's ID
    Ok(Json(MyResponse { user_id: claims.sub }))
}

Environment variables

JWT_SECRET=a-long-random-string-change-in-production
JWT_EXPIRY_HOURS=24

Production security

Generate a strong JWT_SECRET for production. A minimum of 32 random bytes is recommended:

openssl rand -hex 32

Password hashing

Passwords are hashed using argon2 with secure defaults. Plain-text passwords are never stored.