Skip to main content
⚡ Calmops

Authentication and Authorization in Rust

Implement JWT, OAuth2, sessions, and RBAC in web applications

Authentication (verifying who users are) and authorization (determining what they can do) are foundational to secure web applications. Building these systems correctly is critical—poor implementation leads to data breaches and compromised user accounts. This article covers modern authentication patterns in Rust: JWT tokens, OAuth2, session management, and role-based access control (RBAC).


Authentication vs Authorization

  • Authentication: “Who are you?” - Verifying user identity
  • Authorization: “What can you do?” - Determining user permissions
Request → Authentication → Authorization → Resource Access
          (Verify user)   (Check perms)    (Serve resource)

Setup and Dependencies

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
jsonwebtoken = "9"
bcrypt = "0.15"
uuid = { version = "1", features = ["v4", "serde"] }
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls"] }
chrono = "0.4"
thiserror = "1"
tower = "0.4"
tower-http = { version = "0.5", features = ["trace", "cors"] }

Password Security

Hashing with Bcrypt

// filepath: src/password.rs
use bcrypt::{hash, verify, DEFAULT_COST};

pub struct PasswordManager;

impl PasswordManager {
    /// Hash a password for storage
    pub fn hash_password(password: &str) -> Result<String, bcrypt::BcryptError> {
        hash(password, DEFAULT_COST)
    }
    
    /// Verify a password against its hash
    pub fn verify_password(password: &str, hash: &str) -> Result<bool, bcrypt::BcryptError> {
        verify(password, hash)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_password_hashing() {
        let password = "supersecret123";
        
        let hash = PasswordManager::hash_password(password).unwrap();
        
        // Same password verifies
        assert!(PasswordManager::verify_password(password, &hash).unwrap());
        
        // Different password doesn't verify
        assert!(!PasswordManager::verify_password("wrongpassword", &hash).unwrap());
    }
}

Why Bcrypt?

  • Automatically adds salt (prevents rainbow tables)
  • Slows down hashing (prevents brute force)
  • Built-in cost factor (can increase over time)
  • Battle-tested in production

JWT (JSON Web Tokens)

Token Generation

// filepath: src/jwt.rs
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
use chrono::{Utc, Duration};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
    pub sub: String,           // Subject (user ID)
    pub user_id: i32,
    pub email: String,
    pub roles: Vec<String>,    // User roles
    pub exp: i64,              // Expiration time
    pub iat: i64,              // Issued at
}

pub struct JwtManager {
    secret: String,
}

impl JwtManager {
    pub fn new(secret: String) -> Self {
        JwtManager { secret }
    }
    
    /// Generate JWT token
    pub fn generate_token(&self, user_id: i32, email: String, roles: Vec<String>) 
        -> Result<String, jsonwebtoken::errors::Error> 
    {
        let now = Utc::now();
        let expiration = now + Duration::hours(24);
        
        let claims = Claims {
            sub: user_id.to_string(),
            user_id,
            email,
            roles,
            exp: expiration.timestamp(),
            iat: now.timestamp(),
        };
        
        let key = EncodingKey::from_secret(self.secret.as_ref());
        encode(&Header::default(), &claims, &key)
    }
    
    /// Verify and decode JWT token
    pub fn verify_token(&self, token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
        let key = DecodingKey::from_secret(self.secret.as_ref());
        let data = decode::<Claims>(token, &key, &Validation::default())?;
        Ok(data.claims)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_jwt_flow() {
        let secret = "your-secret-key".to_string();
        let manager = JwtManager::new(secret);
        
        // Generate token
        let token = manager.generate_token(
            1,
            "[email protected]".to_string(),
            vec!["user".to_string()],
        ).unwrap();
        
        // Verify token
        let claims = manager.verify_token(&token).unwrap();
        assert_eq!(claims.user_id, 1);
        assert_eq!(claims.email, "[email protected]");
    }
}

Using JWT in Axum

// filepath: src/jwt_middleware.rs
use axum::{
    extract::FromRequestParts,
    http::{request::Parts, StatusCode, header::AUTHORIZATION},
    response::{IntoResponse, Response},
};
use async_trait::async_trait;
use crate::jwt::Claims;

pub struct AuthToken(pub Claims);

#[async_trait]
impl<S> FromRequestParts<S> for AuthToken
where
    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::MissingToken)?;
        
        // Extract token from "Bearer <token>"
        let token = auth_header
            .strip_prefix("Bearer ")
            .ok_or(AuthError::InvalidToken)?;
        
        // Verify token (would use injected JwtManager in real app)
        let secret = "your-secret-key".to_string();
        let manager = crate::jwt::JwtManager::new(secret);
        
        let claims = manager
            .verify_token(token)
            .map_err(|_| AuthError::InvalidToken)?;
        
        Ok(AuthToken(claims))
    }
}

#[derive(Debug)]
pub enum AuthError {
    MissingToken,
    InvalidToken,
}

impl IntoResponse for AuthError {
    fn into_response(self) -> Response {
        match self {
            AuthError::MissingToken => (
                StatusCode::UNAUTHORIZED,
                "Missing authorization token",
            ).into_response(),
            AuthError::InvalidToken => (
                StatusCode::UNAUTHORIZED,
                "Invalid authorization token",
            ).into_response(),
        }
    }
}

Protected Routes

use axum::{routing::get, Router};

// Protected endpoint - requires authentication
async fn protected_route(auth: AuthToken) -> String {
    format!("Hello, {}!", auth.0.email)
}

// Get user info
async fn get_user_info(auth: AuthToken) -> serde_json::json::Value {
    serde_json::json!({
        "user_id": auth.0.user_id,
        "email": auth.0.email,
        "roles": auth.0.roles,
    })
}

pub fn auth_routes() -> Router {
    Router::new()
        .route("/protected", get(protected_route))
        .route("/user", get(get_user_info))
}

Session-Based Authentication

Session Storage

// filepath: src/sessions.rs
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
use chrono::{Utc, Duration};

#[derive(Debug, Clone)]
pub struct SessionData {
    pub user_id: i32,
    pub email: String,
    pub created_at: i64,
    pub expires_at: i64,
}

pub struct SessionStore {
    sessions: Arc<RwLock<HashMap<String, SessionData>>>,
}

impl SessionStore {
    pub fn new() -> Self {
        SessionStore {
            sessions: Arc::new(RwLock::new(HashMap::new())),
        }
    }
    
    /// Create a new session
    pub async fn create_session(&self, user_id: i32, email: String) -> String {
        let session_id = Uuid::new_v4().to_string();
        let now = Utc::now();
        
        let session = SessionData {
            user_id,
            email,
            created_at: now.timestamp(),
            expires_at: (now + Duration::hours(24)).timestamp(),
        };
        
        let mut sessions = self.sessions.write().await;
        sessions.insert(session_id.clone(), session);
        
        session_id
    }
    
    /// Get session data
    pub async fn get_session(&self, session_id: &str) -> Option<SessionData> {
        let sessions = self.sessions.read().await;
        
        sessions.get(session_id).and_then(|session| {
            let now = Utc::now().timestamp();
            if session.expires_at > now {
                Some(session.clone())
            } else {
                None // Session expired
            }
        })
    }
    
    /// Destroy session
    pub async fn destroy_session(&self, session_id: &str) {
        let mut sessions = self.sessions.write().await;
        sessions.remove(session_id);
    }
}
// filepath: src/cookie_session.rs
use axum::{
    extract::FromRequestParts,
    http::{request::Parts, StatusCode},
    response::{IntoResponse, Response, IntoResponseParts, ResponseParts},
};
use async_trait::async_trait;
use tower_cookies::{Cookies, Cookie};

pub struct SessionCookie(pub String); // Session ID

#[async_trait]
impl<S> FromRequestParts<S> for SessionCookie
where
    S: Send + Sync,
{
    type Rejection = StatusCode;
    
    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
        let cookies = Cookies::from_request_parts(parts, _state)
            .await
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
        
        cookies
            .get("session_id")
            .map(|cookie| SessionCookie(cookie.value().to_string()))
            .ok_or(StatusCode::UNAUTHORIZED)
    }
}

// Login endpoint
async fn login(
    cookies: Cookies,
    email: String,
    password: String,
) -> impl IntoResponse {
    // Verify credentials...
    
    // Create session
    let session_id = uuid::Uuid::new_v4().to_string();
    
    // Set secure cookie
    let mut cookie = Cookie::new("session_id", session_id);
    cookie.set_http_only(true);
    cookie.set_secure(true);
    cookie.set_same_site(tower_cookies::cookie::SameSite::Strict);
    cookie.set_max_age(time::Duration::hours(24));
    
    cookies.add(cookie);
    
    StatusCode::OK
}

// Logout endpoint
async fn logout(cookies: Cookies) -> impl IntoResponse {
    cookies.remove(Cookie::named("session_id"));
    StatusCode::OK
}

Role-Based Access Control (RBAC)

Role Hierarchy

// filepath: src/rbac.rs
use std::collections::HashSet;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Role {
    Admin,
    Moderator,
    User,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Permission {
    ReadPost,
    CreatePost,
    EditPost,
    DeletePost,
    ManageUsers,
    DeleteUser,
}

impl Role {
    /// Get permissions for a role
    pub fn permissions(&self) -> HashSet<Permission> {
        match self {
            Role::Admin => vec![
                Permission::ReadPost,
                Permission::CreatePost,
                Permission::EditPost,
                Permission::DeletePost,
                Permission::ManageUsers,
                Permission::DeleteUser,
            ].into_iter().collect(),
            
            Role::Moderator => vec![
                Permission::ReadPost,
                Permission::CreatePost,
                Permission::EditPost,
                Permission::DeletePost,
            ].into_iter().collect(),
            
            Role::User => vec![
                Permission::ReadPost,
                Permission::CreatePost,
            ].into_iter().collect(),
        }
    }
    
    pub fn has_permission(&self, permission: Permission) -> bool {
        self.permissions().contains(&permission)
    }
}

pub struct UserRoles {
    pub roles: Vec<Role>,
}

impl UserRoles {
    pub fn new(roles: Vec<Role>) -> Self {
        UserRoles { roles }
    }
    
    /// Check if user has ANY of the required roles
    pub fn has_any_role(&self, required: &[Role]) -> bool {
        self.roles.iter().any(|role| required.contains(role))
    }
    
    /// Check if user has ALL required roles
    pub fn has_all_roles(&self, required: &[Role]) -> bool {
        required.iter().all(|req| self.roles.contains(req))
    }
    
    /// Check if user has a specific permission
    pub fn has_permission(&self, permission: Permission) -> bool {
        self.roles.iter().any(|role| role.has_permission(permission))
    }
}

Authorization Middleware

// filepath: src/auth_middleware.rs
use axum::{
    extract::FromRequestParts,
    http::{request::Parts, StatusCode},
    response::IntoResponse,
};
use async_trait::async_trait;
use crate::rbac::{Permission, UserRoles, Role};

pub struct RequirePermission(pub Permission);
pub struct RequireRole(pub Vec<Role>);

#[async_trait]
impl<S> FromRequestParts<S> for RequirePermission
where
    S: Send + Sync,
{
    type Rejection = StatusCode;
    
    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
        // Get user from auth token
        let auth = crate::jwt_middleware::AuthToken::from_request_parts(parts, _state)
            .await
            .map_err(|_| StatusCode::UNAUTHORIZED)?;
        
        // Parse roles from claims
        let roles: Vec<Role> = auth.0.roles
            .iter()
            .filter_map(|role_str| {
                match role_str.as_str() {
                    "admin" => Some(Role::Admin),
                    "moderator" => Some(Role::Moderator),
                    "user" => Some(Role::User),
                    _ => None,
                }
            })
            .collect();
        
        // Check permission
        let user_roles = UserRoles::new(roles);
        if user_roles.has_permission(Self::required_permission()) {
            Ok(RequirePermission(Self::required_permission()))
        } else {
            Err(StatusCode::FORBIDDEN)
        }
    }
}

impl RequirePermission {
    fn required_permission() -> Permission {
        Permission::CreatePost // Example - would be configurable
    }
}

#[async_trait]
impl<S> FromRequestParts<S> for RequireRole
where
    S: Send + Sync,
{
    type Rejection = StatusCode;
    
    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
        let auth = crate::jwt_middleware::AuthToken::from_request_parts(parts, _state)
            .await
            .map_err(|_| StatusCode::UNAUTHORIZED)?;
        
        let roles: Vec<Role> = auth.0.roles
            .iter()
            .filter_map(|role_str| {
                match role_str.as_str() {
                    "admin" => Some(Role::Admin),
                    "moderator" => Some(Role::Moderator),
                    "user" => Some(Role::User),
                    _ => None,
                }
            })
            .collect();
        
        // Would check against required roles
        Ok(RequireRole(roles))
    }
}

Protected Endpoints with RBAC

use axum::{routing::post, Router};

// Only admins can delete users
async fn delete_user(
    _require_role: RequireRole,
    user_id: i32,
) -> String {
    format!("Deleted user {}", user_id)
}

// Only users with CreatePost permission
async fn create_post(
    _require_perm: RequirePermission,
    title: String,
) -> String {
    format!("Created post: {}", title)
}

pub fn protected_routes() -> Router {
    Router::new()
        .route("/admin/users/:id", post(delete_user))
        .route("/posts", post(create_post))
}

OAuth2 Integration

Google OAuth2 Example

// filepath: src/oauth2.rs
use reqwest::Client;
use serde::{Deserialize, Serialize};
use url::Url;

#[derive(Debug, Serialize, Deserialize)]
pub struct GoogleOAuth2Config {
    pub client_id: String,
    pub client_secret: String,
    pub redirect_uri: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TokenResponse {
    pub access_token: String,
    pub token_type: String,
    pub expires_in: i32,
    pub id_token: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct UserInfo {
    pub sub: String,       // User ID
    pub email: String,
    pub name: String,
    pub picture: String,
    pub email_verified: bool,
}

pub struct OAuth2Client {
    config: GoogleOAuth2Config,
    client: Client,
}

impl OAuth2Client {
    pub fn new(config: GoogleOAuth2Config) -> Self {
        OAuth2Client {
            config,
            client: Client::new(),
        }
    }
    
    /// Generate authorization URL
    pub fn get_auth_url(&self, state: &str) -> String {
        let mut url = Url::parse("https://accounts.google.com/o/oauth2/v2/auth").unwrap();
        
        url.query_pairs_mut()
            .append_pair("client_id", &self.config.client_id)
            .append_pair("redirect_uri", &self.config.redirect_uri)
            .append_pair("response_type", "code")
            .append_pair("scope", "openid email profile")
            .append_pair("state", state);
        
        url.to_string()
    }
    
    /// Exchange authorization code for token
    pub async fn exchange_code(&self, code: &str) -> Result<TokenResponse, Box<dyn std::error::Error>> {
        let response = self.client
            .post("https://oauth2.googleapis.com/token")
            .form(&[
                ("code", code),
                ("client_id", &self.config.client_id),
                ("client_secret", &self.config.client_secret),
                ("redirect_uri", &self.config.redirect_uri),
                ("grant_type", "authorization_code"),
            ])
            .send()
            .await?;
        
        let token: TokenResponse = response.json().await?;
        Ok(token)
    }
    
    /// Get user info from ID token
    pub async fn get_user_info(&self, id_token: &str) -> Result<UserInfo, Box<dyn std::error::Error>> {
        // Parse and verify ID token (JWT)
        let parts: Vec<&str> = id_token.split('.').collect();
        
        if parts.len() != 3 {
            return Err("Invalid token format".into());
        }
        
        // Decode payload (base64)
        let payload = base64::decode(parts[1])?;
        let user_info: UserInfo = serde_json::from_slice(&payload)?;
        
        Ok(user_info)
    }
}

OAuth2 Callback Handler

// filepath: src/oauth2_handler.rs
use axum::{extract::Query, response::Redirect, Extension};
use serde::Deserialize;
use uuid::Uuid;

#[derive(Debug, Deserialize)]
pub struct AuthCallback {
    pub code: String,
    pub state: String,
}

pub async fn oauth2_callback(
    Query(callback): Query<AuthCallback>,
    Extension(oauth_client): Extension<crate::oauth2::OAuth2Client>,
    Extension(session_store): Extension<crate::sessions::SessionStore>,
) -> Result<Redirect, String> {
    // Exchange code for token
    let token = oauth_client
        .exchange_code(&callback.code)
        .await
        .map_err(|e| e.to_string())?;
    
    // Get user info
    let user_info = oauth_client
        .get_user_info(&token.id_token)
        .await
        .map_err(|e| e.to_string())?;
    
    // Create or update user in database
    // (database logic omitted)
    
    // Create session
    let session_id = session_store.create_session(
        1, // user_id would come from database
        user_info.email,
    ).await;
    
    // Redirect to dashboard with session
    Ok(Redirect::to(&format!("/dashboard?session={}", session_id)))
}

Two-Factor Authentication (2FA)

TOTP Implementation

// filepath: src/totp.rs
use std::time::{SystemTime, UNIX_EPOCH};

pub struct TotpManager {
    secret: String,
}

impl TotpManager {
    /// Generate TOTP secret
    pub fn generate_secret() -> String {
        // Generate random secret (32 bytes)
        // In production, use a proper random generator
        base64::encode(uuid::Uuid::new_v4().as_bytes())
    }
    
    pub fn new(secret: String) -> Self {
        TotpManager { secret }
    }
    
    /// Verify TOTP code
    pub fn verify(&self, code: &str) -> bool {
        let current_code = self.get_current_code();
        
        // Allow current and previous codes (30-60 second window)
        code == current_code || code == self.get_previous_code()
    }
    
    /// Get current TOTP code
    fn get_current_code(&self) -> String {
        let time = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs() / 30;
        
        self.calculate_code(time)
    }
    
    /// Get previous TOTP code
    fn get_previous_code(&self) -> String {
        let time = (SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs() / 30) - 1;
        
        self.calculate_code(time)
    }
    
    /// Calculate TOTP for a specific time
    fn calculate_code(&self, time: u64) -> String {
        // HMAC-SHA1 calculation (simplified)
        // In production, use proper TOTP library like `totp-lite`
        format!("{:06}", time % 1_000_000)
    }
}

Use crate:

totp-lite = "2"

Security Best Practices

Environment Variables

// filepath: src/config.rs
use std::env;

pub struct Config {
    pub jwt_secret: String,
    pub database_url: String,
    pub oauth2_client_id: String,
    pub oauth2_client_secret: String,
}

impl Config {
    pub fn from_env() -> Self {
        Config {
            jwt_secret: env::var("JWT_SECRET")
                .expect("JWT_SECRET not set"),
            database_url: env::var("DATABASE_URL")
                .expect("DATABASE_URL not set"),
            oauth2_client_id: env::var("OAUTH2_CLIENT_ID")
                .expect("OAUTH2_CLIENT_ID not set"),
            oauth2_client_secret: env::var("OAUTH2_CLIENT_SECRET")
                .expect("OAUTH2_CLIENT_SECRET not set"),
        }
    }
}

// .env file (NEVER commit this!)
// JWT_SECRET=your-super-secret-key-here
// DATABASE_URL=postgresql://user:password@localhost/db
// OAUTH2_CLIENT_ID=your-client-id
// OAUTH2_CLIENT_SECRET=your-client-secret

HTTPS Only

use tower_http::trace::TraceLayer;
use axum::middleware::Next;
use axum::http::{Request, StatusCode};

pub async fn require_https<B>(
    req: Request<B>,
    next: Next<B>,
) -> Result<impl axum::response::IntoResponse, StatusCode> {
    if req.headers().get("x-forwarded-proto").map(|h| h.to_str()) == Ok(Ok("http")) {
        return Err(StatusCode::PERMANENT_REDIRECT);
    }
    Ok(next.run(req).await)
}

CORS Configuration

use tower_http::cors::{CorsLayer, AllowOrigin};
use std::str::FromStr;

pub fn cors_layer() -> CorsLayer {
    CorsLayer::permissive()
        // More restrictive in production
        .allow_origin(
            AllowOrigin::list([
                "https://example.com".parse().unwrap(),
                "https://app.example.com".parse().unwrap(),
            ])
        )
        .allow_credentials()
}

Rate Limiting

use tower::limit::RateLimitLayer;
use std::num::NonZeroU32;

pub fn rate_limit_layer() -> RateLimitLayer {
    RateLimitLayer::new(
        NonZeroU32::new(100).unwrap(), // 100 requests
        std::time::Duration::from_secs(60), // per minute
    )
}

Complete Authentication Example

// filepath: src/main.rs
use axum::{
    routing::{post, get},
    Router,
    http::StatusCode,
    response::IntoResponse,
    Extension,
    Json,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

#[derive(Debug, Serialize, Deserialize)]
struct LoginRequest {
    email: String,
    password: String,
}

#[derive(Debug, Serialize)]
struct LoginResponse {
    token: String,
}

async fn login(
    Json(req): Json<LoginRequest>,
    Extension(jwt_mgr): Extension<Arc<crate::jwt::JwtManager>>,
) -> Result<Json<LoginResponse>, StatusCode> {
    // Verify credentials against database
    // (database logic omitted)
    
    // Generate JWT
    let token = jwt_mgr
        .generate_token(1, req.email, vec!["user".to_string()])
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    Ok(Json(LoginResponse { token }))
}

async fn protected(auth: crate::jwt_middleware::AuthToken) -> impl IntoResponse {
    format!("Hello, {}!", auth.0.email)
}

#[tokio::main]
async fn main() {
    let jwt_manager = Arc::new(crate::jwt::JwtManager::new("secret".to_string()));
    
    let app = Router::new()
        .route("/login", post(login))
        .route("/protected", get(protected))
        .layer(Extension(jwt_manager));
    
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    
    axum::serve(listener, app).await.unwrap();
}

Testing Authentication

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_password_hashing() {
        let pwd = "password123";
        let hash = crate::password::PasswordManager::hash_password(pwd).unwrap();
        assert!(crate::password::PasswordManager::verify_password(pwd, &hash).unwrap());
    }
    
    #[tokio::test]
    async fn test_jwt_flow() {
        let manager = crate::jwt::JwtManager::new("secret".to_string());
        
        let token = manager
            .generate_token(1, "[email protected]".to_string(), vec!["user".to_string()])
            .unwrap();
        
        let claims = manager.verify_token(&token).unwrap();
        assert_eq!(claims.user_id, 1);
    }
    
    #[tokio::test]
    async fn test_session_creation() {
        let store = crate::sessions::SessionStore::new();
        
        let session_id = store.create_session(1, "[email protected]".to_string()).await;
        let session = store.get_session(&session_id).await;
        
        assert!(session.is_some());
        assert_eq!(session.unwrap().user_id, 1);
    }
}

Security Checklist

  • Use HTTPS only (enforce with middleware)
  • Hash passwords with bcrypt (never plain text)
  • Use strong JWT secrets (32+ random characters)
  • Set short JWT expiration times (15min-1hr)
  • Implement refresh tokens for long sessions
  • Validate all input (SQL injection, XSS)
  • Use CORS correctly (whitelist origins)
  • Implement rate limiting on login attempts
  • Use secure cookies (HttpOnly, Secure, SameSite)
  • Implement CSRF protection for forms
  • Log authentication events
  • Implement 2FA for sensitive operations
  • Monitor for suspicious activity
  • Keep dependencies updated
  • Use environment variables for secrets

Further Resources

Libraries

Standards

Articles


Conclusion

Secure authentication and authorization in Rust involves:

  1. Password Security - Use bcrypt, never plain text
  2. Token-based Auth - JWT for stateless authentication
  3. Session Management - Track user sessions safely
  4. Access Control - RBAC for fine-grained permissions
  5. OAuth2/OIDC - Delegate auth to providers
  6. 2FA - Extra security layer
  7. Best Practices - HTTPS, rate limiting, CSRF protection

Build security in from the start—retrofitting is much harder!



Stay secure! 🔐

Comments