rust-auth
SKILL.md
Authentication Strategies
| Method | Use Case | Security | Complexity |
|---|---|---|---|
| JWT Token | API access, SPA | High | Medium |
| API Key | Service-to-service, automation | Medium | Low |
| OAuth 2.0 | Third-party login | High | High |
| Session Cookie | Traditional web apps | Medium | Medium |
| mTLS | Service mesh, microservices | Very High | High |
Solution Patterns
Pattern 1: JWT Authentication
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String, // Subject (user ID)
exp: u64, // Expiration
iat: u64, // Issued at
roles: Vec<String>, // User roles
}
struct JwtService {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
algorithm: Algorithm,
expiry_seconds: u64,
}
impl JwtService {
// Recommended: EdDSA (Ed25519)
pub fn new_ed25519(private_pem: &[u8], public_pem: &[u8]) -> Result<Self, Error> {
Ok(Self {
encoding_key: EncodingKey::from_ed_pem(private_pem)?,
decoding_key: DecodingKey::from_ed_pem(public_pem)?,
algorithm: Algorithm::EdDSA,
expiry_seconds: 3600, // 1 hour
})
}
// Alternative: RS256 (RSA)
pub fn new_rs256(private_pem: &[u8], public_pem: &[u8]) -> Result<Self, Error> {
Ok(Self {
encoding_key: EncodingKey::from_rsa_pem(private_pem)?,
decoding_key: DecodingKey::from_rsa_pem(public_pem)?,
algorithm: Algorithm::RS256,
expiry_seconds: 3600,
})
}
pub fn generate_token(&self, user_id: &str, roles: Vec<String>) -> Result<String, Error> {
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let claims = Claims {
sub: user_id.to_string(),
exp: now + self.expiry_seconds,
iat: now,
roles,
};
encode(&Header::new(self.algorithm), &claims, &self.encoding_key)
.map_err(Into::into)
}
pub fn verify_token(&self, token: &str) -> Result<Claims, Error> {
let mut validation = Validation::new(self.algorithm);
validation.validate_exp = true;
decode::<Claims>(token, &self.decoding_key, &validation)
.map(|data| data.claims)
.map_err(Into::into)
}
}
Algorithm Selection:
- EdDSA (Ed25519): Recommended default (fast, secure, small keys)
- RS256: Use when ecosystem already standardized on RSA
- HS256: Only for single-service, tightly controlled scenarios
Pattern 2: API Key Authentication
use sha2::{Digest, Sha256};
use rand::{thread_rng, Rng};
struct ApiKey {
key_id: String,
secret_hash: String,
owner_id: String,
scopes: Vec<String>,
expires_at: i64,
disabled: bool,
}
struct ApiKeyService {
secret: String,
prefix: String,
}
impl ApiKeyService {
pub fn generate(&self, owner_id: &str, scopes: Vec<String>) -> (String, String) {
let key_id = self.generate_id();
let secret = self.generate_secret();
let signature = self.compute_signature(&key_id, &secret);
let api_key = format!("{}_{}_{}", self.prefix, key_id, signature);
let secret_hash = self.hash_secret(&secret);
(api_key, secret_hash)
}
pub fn verify(&self, api_key: &str, stored: &ApiKey) -> Result<(), AuthError> {
if stored.disabled {
return Err(AuthError::KeyRevoked);
}
let now = chrono::Utc::now().timestamp();
if stored.expires_at < now {
return Err(AuthError::KeyExpired);
}
let parts: Vec<&str> = api_key.split('_').collect();
if parts.len() != 3 {
return Err(AuthError::InvalidFormat);
}
let expected_sig = self.compute_signature(parts[1], "");
if parts[2] != expected_sig {
return Err(AuthError::InvalidSignature);
}
Ok(())
}
fn generate_id(&self) -> String {
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
let mut rng = thread_rng();
(0..16).map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char).collect()
}
fn generate_secret(&self) -> String {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let mut rng = thread_rng();
(0..32).map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char).collect()
}
fn compute_signature(&self, key_id: &str, secret: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(format!("{}{}{}", key_id, secret, self.secret));
format!("{:x}", hasher.finalize())[..16].to_string()
}
fn hash_secret(&self, secret: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(format!("{}{}", secret, self.secret));
format!("{:x}", hasher.finalize())
}
}
Pattern 3: Password Hashing (Argon2)
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use argon2::password_hash::{rand_core::OsRng, SaltString};
struct PasswordService;
impl PasswordService {
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2.hash_password(password.as_bytes(), &salt)?;
Ok(hash.to_string())
}
pub fn verify_password(password: &str, hash: &str) -> Result<bool, argon2::password_hash::Error> {
let parsed_hash = PasswordHash::new(hash)?;
let argon2 = Argon2::default();
Ok(argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok())
}
}
Pattern 4: Distributed Token Storage (Redis)
use redis::AsyncCommands;
use serde_json;
struct TokenStore {
redis: redis::aio::ConnectionManager,
prefix: String,
}
impl TokenStore {
pub async fn store_token(
&mut self,
user_id: &str,
token_id: &str,
claims: &Claims,
max_concurrent: usize,
) -> Result<(), Error> {
let user_tokens_key = format!("{}:user:{}:tokens", self.prefix, user_id);
// Check concurrent session limit
let count: usize = self.redis.scard(&user_tokens_key).await?;
if count >= max_concurrent {
// Remove oldest token
if let Some(old_token) = self.redis.spop::<_, Option<String>>(&user_tokens_key).await? {
self.redis.del(format!("{}:token:{}", self.prefix, old_token)).await?;
}
}
// Store new token
let token_key = format!("{}:token:{}", self.prefix, token_id);
let data = serde_json::to_string(claims)?;
let ttl = claims.exp - chrono::Utc::now().timestamp() as u64;
self.redis.set_ex(&token_key, data, ttl as usize).await?;
self.redis.sadd(&user_tokens_key, token_id).await?;
Ok(())
}
pub async fn revoke_token(&mut self, user_id: &str, token_id: &str) -> Result<(), Error> {
self.redis.del(format!("{}:token:{}", self.prefix, token_id)).await?;
self.redis.srem(format!("{}:user:{}:tokens", self.prefix, user_id), token_id).await?;
Ok(())
}
pub async fn revoke_all_tokens(&mut self, user_id: &str) -> Result<(), Error> {
let user_tokens_key = format!("{}:user:{}:tokens", self.prefix, user_id);
let tokens: Vec<String> = self.redis.smembers(&user_tokens_key).await?;
for token_id in tokens {
self.redis.del(format!("{}:token:{}", self.prefix, token_id)).await?;
}
self.redis.del(&user_tokens_key).await?;
Ok(())
}
}
Pattern 5: Auth Middleware (Axum)
use axum::{
extract::Request,
middleware::Next,
response::Response,
http::StatusCode,
};
pub async fn jwt_auth_middleware(
mut req: Request,
next: Next,
) -> Result<Response, StatusCode> {
let auth_header = req.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
let token = auth_header
.strip_prefix("Bearer ")
.ok_or(StatusCode::UNAUTHORIZED)?;
let jwt_service = req.extensions()
.get::<JwtService>()
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
let claims = jwt_service
.verify_token(token)
.map_err(|_| StatusCode::UNAUTHORIZED)?;
req.extensions_mut().insert(claims);
Ok(next.run(req).await)
}
RBAC (Role-Based Access Control)
#[derive(Debug, Clone)]
pub enum Permission {
Read,
Write,
Delete,
Admin,
}
#[derive(Debug, Clone)]
pub struct Role {
name: String,
permissions: Vec<Permission>,
}
pub struct RbacService {
roles: HashMap<String, Role>,
}
impl RbacService {
pub fn check_permission(&self, user_roles: &[String], required: Permission) -> bool {
user_roles.iter().any(|role_name| {
self.roles
.get(role_name)
.map(|role| role.permissions.contains(&required))
.unwrap_or(false)
})
}
}
Workflow
Step 1: Choose Auth Strategy
Use case:
→ API/SPA? JWT
→ Service-to-service? API Key or mTLS
→ Traditional web? Session Cookie
→ Third-party login? OAuth 2.0
→ Microservices? mTLS + JWT
Step 2: Implement Securely
Security checklist:
→ Strong password hashing (Argon2)
→ Secure token generation (crypto random)
→ HTTPS only in production
→ Validate all inputs
→ Rate limiting on auth endpoints
→ Audit logging
Step 3: Handle Token Lifecycle
Token management:
→ Issue: Generate with expiry
→ Refresh: Before expiry
→ Revoke: Blacklist or store state
→ Rotate: Periodic key rotation
Review Checklist
When implementing auth:
- Passwords hashed with Argon2 (not bcrypt/MD5)
- JWT tokens have expiration
- API keys have scope limitations
- HTTPS enforced in production
- Rate limiting on login endpoints
- Audit logging for auth events
- Token refresh mechanism
- Secure session storage (Redis with encryption)
- IP whitelisting for API keys
- Concurrent session limits
Verification Commands
# Test JWT generation
cargo test jwt_generation
# Verify password hashing
cargo test password_hashing
# Check token expiration
cargo test token_expiry
# Load test auth endpoints
wrk -t4 -c100 -d30s http://localhost:3000/auth/login
Common Pitfalls
1. Weak Password Hashing
Symptom: Fast brute-force attacks
// ❌ Bad: MD5/SHA256 (too fast)
let hash = format!("{:x}", md5::compute(password));
// ✅ Good: Argon2 (designed for passwords)
let hash = Argon2::default().hash_password(password.as_bytes(), &salt)?;
2. No Token Expiration
Symptom: Stolen tokens valid forever
// ❌ Bad: no expiration
let claims = Claims { sub: user_id, exp: None };
// ✅ Good: reasonable expiry
let claims = Claims {
sub: user_id,
exp: now + 3600, // 1 hour
};
3. Storing Tokens in LocalStorage
Symptom: XSS vulnerability
// ❌ Bad: accessible to XSS
localStorage.setItem('token', jwt);
// ✅ Good: HTTP-only cookie
// Set-Cookie: token=...; HttpOnly; Secure; SameSite=Strict
Related Skills
- rust-web - Web framework integration
- rust-middleware - Middleware patterns
- rust-cache - Token storage with Redis
- rust-error - Error handling
- rust-database - User storage
Localized Reference
- Chinese version: SKILL_ZH.md - 完整中文版本,包含所有内容
Weekly Installs
7
Repository
huiali/rust-skillsGitHub Stars
20
First Seen
Jan 28, 2026
Security Audits
Installed on
gemini-cli6
claude-code4
github-copilot4
amp4
cline4
codex4