Introduction
Authentication is a critical yet often overlooked aspect of web service development. Getting it wrong can lead to:
- Unauthorized access to sensitive data
- Session hijacking and credential theft
- Account takeover attacks
- Compliance violations (GDPR, HIPAA, PCI-DSS)
JSON Web Tokens (JWT) have become the industry standard for stateless authentication in modern APIs. They’re compact, secure, and work seamlessly across distributed microservices without requiring shared session stores.
Rust’s strong type system and memory safety make it ideal for implementing authentication systems that are both secure and performant. This article explores how to build production-grade JWT authentication in Rust using frameworks like Axum.
Part 1: Core Concepts
What is JWT?
A JSON Web Token (JWT) is a compact, URL-safe string that contains encoded claims (user data). It has three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header Payload Signature
[Base64URL] [Base64URL] [HMAC]
Header - Declares the token type and hashing algorithm:
{
"alg": "HS256",
"typ": "JWT"
}
Payload - Contains claims (user data):
{
"sub": "user-123",
"email": "[email protected]",
"role": "admin",
"iat": 1516239022,
"exp": 1516325422
}
Signature - Created by signing the header and payload with a secret:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
JWT Flow in Web Services
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ Client โ โ Server โ
โ (Browser) โ โ (API) โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโโโโโ POST /login โโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ (username, password) โ
โ โ
โ โ โโ โโ โโ JWT Token โ โโ โโ โโ โโ โโ โโ โโ โโ โ
โ โ
โโโโโโโโโโโโโ GET /api/users โโโโโโโโโโโโโโโโโโโโโโ โ
โ Header: Authorization: Bearer TOKEN โ
โ โ
โ Verify Signature โ โ
โ Check Expiration โ โ
โ โ
โ โ โโ โโ โโ Protected Data โ โโ โโ โโ โโ โโ โโ โ
โ โ
Authentication vs Authorization
- Authentication: Verifying who you are (login with credentials)
- Authorization: Verifying what you can do (role-based access)
JWT handles both by including claims (roles, permissions) in the token.
Why JWT Over Sessions?
| Aspect | JWT | Session (Cookie) |
|---|---|---|
| State | Stateless | Requires server storage |
| Scalability | No session store needed | Needs shared cache |
| Mobile | Works seamlessly | Awkward with mobile apps |
| CORS | Easy cross-origin | Problematic |
| Microservices | Share secret key | Complex coordination |
| Security | Signed/encrypted | Secure if HTTPS |
Part 2: Implementing JWT in Rust
Dependencies Setup
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
jsonwebtoken = "9"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }
bcrypt = "0.15"
tracing = "0.1"
tracing-subscriber = "0.3"
JWT Claims Structure
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
// Define claims that go into the JWT
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: String, // Subject (usually user ID)
pub email: String, // User email
pub role: UserRole, // User role
pub iat: i64, // Issued at
pub exp: i64, // Expiration time
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum UserRole {
Admin,
User,
Guest,
}
impl Claims {
pub fn new(user_id: String, email: String, role: UserRole) -> Self {
let now = Utc::now();
let iat = now.timestamp();
let exp = (now + Duration::hours(24)).timestamp();
Claims {
sub: user_id,
email,
role,
iat,
exp,
}
}
// Check if token is expired
pub fn is_expired(&self) -> bool {
Utc::now().timestamp() > self.exp
}
}
// Secret configuration
pub struct JwtConfig {
pub secret: String,
pub algorithm: jsonwebtoken::Algorithm,
}
impl JwtConfig {
pub fn new(secret: String) -> Self {
JwtConfig {
secret,
algorithm: jsonwebtoken::Algorithm::HS256,
}
}
pub fn encoding_key(&self) -> EncodingKey {
EncodingKey::from_secret(self.secret.as_ref())
}
pub fn decoding_key(&self) -> DecodingKey {
DecodingKey::from_secret(self.secret.as_ref())
}
}
Issuing Tokens
use jsonwebtoken::Header;
pub fn issue_token(claims: &Claims, config: &JwtConfig) -> Result<String, Box<dyn std::error::Error>> {
let token = encode(
&Header::default(),
claims,
&config.encoding_key(),
)?;
Ok(token)
}
// Example usage
async fn login_handler(body: Json<LoginRequest>) -> Result<Json<LoginResponse>, AuthError> {
// Validate credentials (compare password hashes with bcrypt)
let user = authenticate_user(&body.email, &body.password).await?;
// Create claims
let claims = Claims::new(
user.id.to_string(),
user.email.clone(),
user.role,
);
// Issue token
let config = JwtConfig::new(std::env::var("JWT_SECRET")?);
let token = issue_token(&claims, &config)
.map_err(|_| AuthError::TokenGeneration)?;
Ok(Json(LoginResponse { token }))
}
Verifying Tokens
use jsonwebtoken::Validation;
pub fn verify_token(token: &str, config: &JwtConfig) -> Result<Claims, Box<dyn std::error::Error>> {
let data = decode::<Claims>(
token,
&config.decoding_key(),
&Validation::new(config.algorithm),
)?;
let claims = data.claims;
// Check expiration
if claims.is_expired() {
return Err("Token expired".into());
}
Ok(claims)
}
Part 3: Middleware for Token Extraction
Axum middleware automatically extracts and verifies tokens from requests:
use axum::{
async_trait,
extract::{FromRef, FromRequestParts},
http::request::Parts,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
#[derive(Debug)]
pub struct AuthError(pub String);
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let status = axum::http::StatusCode::UNAUTHORIZED;
let body = Json(json!({
"error": self.0,
"status": 401,
}));
(status, body).into_response()
}
}
// Application state containing JWT config
#[derive(Clone)]
pub struct AppState {
pub jwt_config: JwtConfig,
}
// Extract claims from Authorization header
#[async_trait]
impl<S> FromRequestParts<S> for Claims
where
JwtConfig: FromRef<S>,
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
// Extract Authorization header
let auth_header = parts
.headers
.get("Authorization")
.and_then(|h| h.to_str().ok())
.ok_or(AuthError("Missing Authorization header".to_string()))?;
// Parse "Bearer TOKEN"
let token = auth_header
.strip_prefix("Bearer ")
.ok_or(AuthError("Invalid Authorization header format".to_string()))?;
// Get JWT config from state
let jwt_config = JwtConfig::from_ref(state);
// Verify and extract claims
verify_token(token, &jwt_config)
.map_err(|e| AuthError(e.to_string()))
}
}
// Extract claims only if role matches
pub struct RequireRole(pub UserRole);
#[async_trait]
impl<S> FromRequestParts<S> for RequireRole
where
Claims: FromRequestParts<S, Rejection = AuthError>,
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let claims = Claims::from_request_parts(parts, state).await?;
match claims.role {
UserRole::Admin => Ok(RequireRole(UserRole::Admin)),
_ => Err(AuthError("Insufficient permissions".to_string())),
}
}
}
Part 4: Complete Authentication Example
use axum::{
extract::Json,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginRequest {
pub email: String,
pub password: String,
}
#[derive(Debug, Serialize)]
pub struct LoginResponse {
pub token: String,
}
#[derive(Debug, Serialize)]
pub struct UserProfile {
pub id: String,
pub email: String,
pub role: String,
}
// Login endpoint - public
async fn login(
Json(payload): Json<LoginRequest>,
state: axum::extract::State<AppState>,
) -> Result<Json<LoginResponse>, AuthError> {
// In production, verify password against bcrypt hash
if payload.password.len() < 8 {
return Err(AuthError("Invalid credentials".to_string()));
}
let user_id = uuid::Uuid::new_v4().to_string();
let claims = Claims::new(user_id, payload.email, UserRole::User);
let token = issue_token(&claims, &state.jwt_config)
.map_err(|_| AuthError("Token generation failed".to_string()))?;
Ok(Json(LoginResponse { token }))
}
// Protected endpoint - requires valid JWT
async fn get_profile(claims: Claims) -> Json<UserProfile> {
Json(UserProfile {
id: claims.sub.clone(),
email: claims.email.clone(),
role: format!("{:?}", claims.role),
})
}
// Admin-only endpoint
async fn admin_dashboard(
RequireRole(_): RequireRole,
claims: Claims,
) -> Json<serde_json::Value> {
Json(serde_json::json!({
"message": format!("Welcome Admin {}", claims.email),
"timestamp": chrono::Utc::now().to_rfc3339(),
}))
}
// Create router with authentication
pub fn create_app(jwt_config: JwtConfig) -> Router {
let state = AppState { jwt_config };
Router::new()
.route("/login", post(login))
.route("/profile", get(get_profile))
.route("/admin/dashboard", get(admin_dashboard))
.with_state(state)
}
Part 5: Password Hashing with bcrypt
Never store passwords in plain text. Always hash them:
use bcrypt::{hash, verify, DEFAULT_COST};
pub struct UserRepository;
impl UserRepository {
// Store hashed password
pub async fn create_user(
email: &str,
password: &str,
) -> Result<User, Box<dyn std::error::Error>> {
// Hash password with bcrypt
let hashed = hash(password, DEFAULT_COST)?;
// Store in database
let user = User {
id: uuid::Uuid::new_v4(),
email: email.to_string(),
password_hash: hashed,
role: UserRole::User,
};
// Insert into database...
Ok(user)
}
// Verify password
pub async fn verify_password(
stored_hash: &str,
provided_password: &str,
) -> Result<bool, Box<dyn std::error::Error>> {
Ok(verify(provided_password, stored_hash)?)
}
}
// Usage in login
async fn authenticate_user(email: &str, password: &str) -> Result<User, AuthError> {
let user = database::find_user_by_email(email)
.await
.map_err(|_| AuthError("User not found".to_string()))?;
let valid = UserRepository::verify_password(&user.password_hash, password)
.await
.map_err(|_| AuthError("Authentication failed".to_string()))?;
if valid {
Ok(user)
} else {
Err(AuthError("Invalid credentials".to_string()))
}
}
Part 6: Refresh Tokens
JWT tokens have expiration times. Handle token refresh:
#[derive(Debug, Serialize, Deserialize)]
pub struct TokenPair {
pub access_token: String,
pub refresh_token: String,
}
// Shorter-lived access token
pub fn issue_access_token(claims: &Claims, config: &JwtConfig) -> Result<String, Box<dyn std::error::Error>> {
let mut claims = claims.clone();
claims.exp = (Utc::now() + Duration::minutes(15)).timestamp();
encode(&Header::default(), &claims, &config.encoding_key()).map_err(|e| Box::new(e) as _)
}
// Longer-lived refresh token
pub fn issue_refresh_token(claims: &Claims, config: &JwtConfig) -> Result<String, Box<dyn std::error::Error>> {
let mut claims = claims.clone();
claims.exp = (Utc::now() + Duration::days(7)).timestamp();
encode(&Header::default(), &claims, &config.encoding_key()).map_err(|e| Box::new(e) as _)
}
// Endpoint to refresh access token
async fn refresh_token(
Json(payload): Json<serde_json::Value>,
state: axum::extract::State<AppState>,
) -> Result<Json<TokenPair>, AuthError> {
let refresh_token = payload["refresh_token"]
.as_str()
.ok_or(AuthError("Missing refresh token".to_string()))?;
// Verify refresh token
let claims = verify_token(refresh_token, &state.jwt_config)
.map_err(|_| AuthError("Invalid refresh token".to_string()))?;
// Issue new access token
let access_token = issue_access_token(&claims, &state.jwt_config)
.map_err(|_| AuthError("Token generation failed".to_string()))?;
let new_refresh_token = issue_refresh_token(&claims, &state.jwt_config)
.map_err(|_| AuthError("Token generation failed".to_string()))?;
Ok(Json(TokenPair {
access_token,
refresh_token: new_refresh_token,
}))
}
Part 7: Common Pitfalls & Best Practices
โ Pitfall: Storing Sensitive Data in JWT
// BAD: Putting sensitive data in JWT
let claims = Claims {
sub: user.id.to_string(),
email: user.email.clone(),
role: user.role,
// DON'T do this:
password_hash: user.password_hash.clone(), // Never!
credit_card: user.credit_card.clone(), // Never!
ssn: user.ssn.clone(), // Never!
iat: now.timestamp(),
exp: exp.timestamp(),
};
// GOOD: Only include necessary claims
let claims = Claims {
sub: user.id.to_string(),
email: user.email.clone(),
role: user.role,
iat: now.timestamp(),
exp: exp.timestamp(),
};
Why it matters: JWT is Base64-encoded, not encrypted. Secrets can be read by decoding the token.
โ Pitfall: Using Weak Secrets
// BAD: Weak secret
let secret = "my-super-secret"; // Easily guessed!
// GOOD: Use strong, random secret
let secret = std::env::var("JWT_SECRET")
.expect("JWT_SECRET not set");
// Generate: openssl rand -base64 32
Why it matters: A weak secret allows attackers to forge valid tokens.
โ Pitfall: Not Validating Expiration
// BAD: Accepting expired tokens
let data = decode::<Claims>(token, &key, &Validation::new(Algorithm::HS256))?;
// Token could be expired!
// GOOD: Always check expiration
if claims.is_expired() {
return Err(AuthError("Token expired".to_string()));
}
โ Best Practice: Use HTTPS
// Configure HTTPS in production
// This prevents token interception
let app = create_app(jwt_config);
let listener = tokio::net::TcpListener::bind("0.0.0.0:443")
.await?;
// Use rustls or native-tls for TLS
โ Best Practice: Token Rotation
// Issue new token pair on each refresh
pub async fn refresh_token_handler(
state: axum::extract::State<AppState>,
) -> Result<Json<TokenPair>, AuthError> {
// Verify old refresh token
let claims = verify_token(refresh_token, &state.jwt_config)?;
// Issue completely new tokens
let new_access = issue_access_token(&claims, &state.jwt_config)?;
let new_refresh = issue_refresh_token(&claims, &state.jwt_config)?;
// Invalidate old tokens in database if needed
// database::revoke_token(refresh_token).await?;
Ok(Json(TokenPair {
access_token: new_access,
refresh_token: new_refresh,
}))
}
โ Best Practice: Token Revocation
// For logout or security events, revoke tokens
pub struct TokenBlacklist {
revoked: std::collections::HashSet<String>,
}
impl TokenBlacklist {
pub fn revoke(&mut self, token: &str) {
self.revoked.insert(token.to_string());
}
pub fn is_revoked(&self, token: &str) -> bool {
self.revoked.contains(token)
}
}
// Check before verifying
async fn verify_with_blacklist(
token: &str,
config: &JwtConfig,
blacklist: &TokenBlacklist,
) -> Result<Claims, AuthError> {
if blacklist.is_revoked(token) {
return Err(AuthError("Token revoked".to_string()));
}
verify_token(token, config)
.map_err(|e| AuthError(e.to_string()))
}
Part 8: Architecture Diagram
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Client (Frontend) โ
โ - Browser/Mobile App โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโดโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโผโโโโโโโโโ โโโโโโโโโโผโโโโโโโโโ
โ POST /login โ โ GET /protected โ
โ (credentials) โ โ with JWT โ
โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ โ
โ โ
โโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Rust Web Service (Axum) โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Login Handler โ โ
โ โ - Verify credentials (bcrypt hash) โ โ
โ โ - Create Claims โ โ
โ โ - Issue JWT Token โ โ
โ โ - Return token to client โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Authentication Middleware โ โ
โ โ - Extract token from header โ โ
โ โ - Verify signature (HMAC) โ โ
โ โ - Check expiration โ โ
โ โ - Extract Claims โ โ
โ โ - Return 401 if invalid โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Protected Route Handler โ โ
โ โ - Access claims from request โ โ
โ โ - Validate permissions โ โ
โ โ - Execute business logic โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ JWT Config โ โ
โ โ - Secret (from env variable) โ โ
โ โ - Algorithm (HS256) โ โ
โ โ - Key for signing/verifying โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโ
โ Database (PostgreSQL) โ
โ - User credentials โ
โ - User profiles โ
โ - Role mappings โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Part 9: JWT Comparison with Alternatives
| Approach | JWT | OAuth 2.0 | Session Cookies | API Keys |
|---|---|---|---|---|
| Stateless | Yes | Not always | No | No |
| Scalability | Excellent | Good | Poor | Good |
| Security | Good if HTTPS | Excellent | Good if HTTPS | Limited |
| Mobile-friendly | Yes | Yes | Poor | Yes |
| Complexity | Simple | Complex | Simple | Simple |
| Use Case | Internal APIs | Third-party | Web apps | Simple APIs |
When to Use JWT
- Microservices architecture
- Mobile/SPA applications
- Cross-domain requests
- Internal APIs with controlled clients
When to Use OAuth 2.0
- Third-party integrations
- Delegated authorization
- Social login
- Complex permission models
When to Use Session Cookies
- Traditional server-rendered web apps
- Tight security requirements
- Session-dependent features
Part 10: Testing Authentication
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_creation() {
let claims = Claims::new(
"user-123".to_string(),
"[email protected]".to_string(),
UserRole::User,
);
let config = JwtConfig::new("test-secret-key".to_string());
let token = issue_token(&claims, &config).unwrap();
assert!(!token.is_empty());
assert_eq!(token.matches('.').count(), 2); // JWT has 3 parts
}
#[test]
fn test_token_verification() {
let claims = Claims::new(
"user-123".to_string(),
"[email protected]".to_string(),
UserRole::User,
);
let config = JwtConfig::new("test-secret-key".to_string());
let token = issue_token(&claims, &config).unwrap();
let verified = verify_token(&token, &config).unwrap();
assert_eq!(verified.sub, "user-123");
assert_eq!(verified.email, "[email protected]");
}
#[test]
fn test_token_expiration() {
let mut claims = Claims::new(
"user-123".to_string(),
"[email protected]".to_string(),
UserRole::User,
);
// Set expiration to past
claims.exp = (Utc::now() - Duration::seconds(1)).timestamp();
assert!(claims.is_expired());
}
#[test]
fn test_invalid_signature() {
let claims = Claims::new(
"user-123".to_string(),
"[email protected]".to_string(),
UserRole::User,
);
let config1 = JwtConfig::new("secret-1".to_string());
let token = issue_token(&claims, &config1).unwrap();
// Try to verify with different secret
let config2 = JwtConfig::new("secret-2".to_string());
let result = verify_token(&token, &config2);
assert!(result.is_err());
}
}
Part 11: Resources & Further Reading
Official Documentation
- jsonwebtoken Crate
- JWT.io - Debugger & Libraries
- Axum Authentication Patterns
- OWASP Authentication Cheat Sheet
Articles & Guides
- JWT Security Best Practices
- Implementing JWT in Rust - Shuttle Blog
- Authentication in Axum Web Framework
- Stateless Authentication in Microservices
Books
- The Rust Programming Language - Chapter on lifetimes and ownership
- Building Microservices with Rust - Security chapter
- OAuth 2.0 in Action - Authentication patterns
Tools & Libraries
- jsonwebtoken - JWT encoding/decoding
- bcrypt - Password hashing
- argon2 - Modern password hashing (alternative to bcrypt)
- ring - Cryptographic primitives
- openssl - SSL/TLS support
Part 12: Security Hardening
Environment Variables
# .env (never commit this!)
JWT_SECRET=your-random-secret-here-min-32-chars
JWT_EXPIRATION_HOURS=24
REFRESH_TOKEN_EXPIRATION_DAYS=7
BCRYPT_COST=12
HTTPS Configuration
use rustls_pemfile::{certs, rsa_private_keys};
use std::fs::File;
use std::io::BufReader;
pub async fn run_secure(app: Router) -> Result<()> {
let config = rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(certs(cert_reader)?, key)?;
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
// Listen on HTTPS
let listener = tokio::net::TcpListener::bind("0.0.0.0:443").await?;
axum::serve(
listener,
app.layer(axum::middleware::from_fn(
move |req, next| {
// Ensure HTTPS
Ok(next.run(req).await)
},
)),
)
.await?;
Ok(())
}
Rate Limiting
use tower_governor::governor::RateLimiter;
pub fn rate_limiting_layer() -> tower_governor::key_extractor::KeyExtractor {
tower_governor::governor::RateLimiter::direct(
std::num::NonZeroU32::new(100).unwrap(), // 100 requests
)
}
// Apply to sensitive endpoints
let app = Router::new()
.route("/login", post(login).layer(rate_limiting_layer()))
.route("/refresh", post(refresh_token).layer(rate_limiting_layer()));
Part 13: Alternative Approaches
Token Alternatives to JWT
| Token Type | Pros | Cons |
|---|---|---|
| JWT | Stateless, compact, self-contained | Difficult to revoke immediately |
| Opaque Tokens | Easy to revoke, smaller | Requires backend lookup |
| PASETO | More secure defaults than JWT | Less ecosystem |
| Structured Tokens | Best of both | Complex implementation |
When to Use Opaque Tokens
// Generate random token, store in Redis
pub fn issue_opaque_token(user_id: &str) -> String {
let token = uuid::Uuid::new_v4().to_string();
// Store mapping: token -> user_id (expires in 24 hours)
redis_client.setex(&token, 86400, user_id).unwrap();
token
}
// Verify requires backend lookup
pub fn verify_opaque_token(token: &str) -> Result<String, AuthError> {
let user_id = redis_client.get(token)
.map_err(|_| AuthError("Invalid token".to_string()))?;
Ok(user_id)
}
Conclusion
JWT authentication in Rust combines type safety, performance, and security to create robust API authentication systems. Key takeaways:
- JWTs are stateless - No server storage required
- Use bcrypt for passwords - Never store plaintext
- Always verify expiration - Tokens have lifetime limits
- Implement refresh tokens - Shorten access token lifetime
- Use HTTPS always - Prevent token interception
- Follow best practices - Rotate tokens, handle revocation, rate limit
By leveraging Rust’s type system through frameworks like Axum, you create authentication systems that are not just secure but also impossible to misuse at compile time.
Comments