🦀 Exploring Rust Through a Real Authentication Project
Aug 18, 2025 • Rust •9 min
Creating a project in Rust can feel intimidating, especially for beginners.
But hands-on experience is the best way to learn.
In this post, I'll share my experience building an authentication service in Rust from scratch, explaining architecture decisions, technical challenges, and key learnings.
Why an Authentication Service?
After considering several project ideas, I realized most were very basic (CLI apps, simple CRUDs, small apps).
I wanted a project that could actually be used in production, where I could apply my Rust knowledge and learn at the same time.
An authentication system fits perfectly: every product or system needs one. Bingo! 🎯
Project Goal
The goal was to create something modular, scalable, secure, and maintainable, while deepening my knowledge in both Rust and software engineering best practices:
- Rust ownership and borrowing
- Lifetimes
- Async programming with Tokio
- Proper error handling
- Writing unit and integration tests
- Custom errors
- Clean architecture design
Challenges
- Lifetime, ownership, and borrowing: Rust's strict memory rules forced me to design the architecture carefully from day one.
- Implementing RBAC: Role-Based Access Control required careful planning to ensure that permissions were applied correctly without duplicating code.
- Rate limiting and token blocklist: Ensuring fair usage and secure logout while maintaining performance, using Redis for distributed state management.
- Security: Protecting sensitive data, implementing secure password hashing with Argon2, and preventing common authentication vulnerabilities.
Example approach for RBAC:
1enum Role { 2 Admin, 3 User, 4} 5 6fn can_access(user_role: &Role, required_role: &Role) -> bool { 7 matches!(user_role, Role::Admin) || user_role == required_role 8}
Architecture Overview
The project follows a modular design based on practical needs:
src/
├── auth/ # JWT logic (token generation/validation)
├── config.rs # Configuration management
├── db/ # Database operations and repositories
├── docs.rs # OpenAPI documentation generation
├── errors/ # Custom error types with Thiserror
├── handlers/ # HTTP request handlers (API endpoints)
├── middleware/ # Middleware (auth, CORS, rate limiting)
├── models/ # Data structures and database models
├── routes/ # Route definitions and API organization
├── services/ # Utility services (password hashing, JWT)
├── bin/ # Binary utilities (seed script)
├── lib.rs # Library entry point
└── main.rs # Application entry point
Flow of a request:
Client → routes→ handlers→ middleware → services→ db → Database → Response
Key architectural decisions:
- Handlers: Handle HTTP requests and coordinate business logic
- Middleware: Authentication, CORS, and rate limiting
- DB layer: Direct database operations with SQLx
- Services: Utility functions (password hashing, JWT operations)
- Models: Data structures shared across layers
Key Features Implemented
- 🔑 JWT-based authentication (Access & Refresh Tokens)
- 👥 Role-Based Access Control (RBAC) with middleware
- ⚡ Rate limiting using Redis (10 requests/min per IP)
- 🚪 Logout with token blocklist via Redis
- 🔒 Password hashing with Argon2 (enterprise-grade security)
- 🧪 Unit and integration tests with Tokio + SQLx
- 📚 Auto-generated API documentation with Utoipa + SwaggerUI
- 🐳 Docker setup with automated configuration
- 🔧 Automated setup script for easy deployment
Tech Stack
- Framework: Axum (async Rust web framework)
- Database: PostgreSQL + SQLx (type-safe ORM)
- Cache: Redis (rate limiting + token blacklisting)
- Auth: JWT + Argon2 (password hashing)
- Logging: Tracing + Tracing Subscriber
- Testing: Tokio + SQLx test utilities
- DevOps: Docker + Docker Compose
- Documentation: Utoipa (OpenAPI generation)
Development Environment
Automated Setup Script (``):
1#!/bin/bash 2echo "🚀 Setup basic Rust Auth Service..." 3 4if [ ! -f .env ]; then 5 echo "📝 Creating .env..." 6fi 7 8echo "🐳 Init Docker Compose" 9docker compose up -d 10 11echo "⏳ Waiting for services..." 12sleep 15 13 14echo "🔧 Installing SQLx CLI..." 15if ! command -v sqlx &> /dev/null; then 16 cargo install sqlx-cli --no-default-features --features postgres 17fi 18 19echo "📦 Running migrations..." 20source .env && sqlx migrate run 21 22echo "🌱 Creating initial data..." 23cargo run --bin seed -- --nocapture 24 25echo "✅ Setup complete!"
Docker Compose Configuration:
1services: 2 postgres: 3 image: postgres:latest 4 environment: 5 POSTGRES_USER: ${POSTGRES_USER} 6 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 7 POSTGRES_DB: ${POSTGRES_DB} 8 ports: 9 - "5432:5432" 10 volumes: 11 - pgdata:/var/lib/postgresql/data 12 13 redis: 14 image: redis:latest 15 ports: 16 - "6379:6379" 17 volumes: 18 - redisdata:/data 19 20 redis_commander: 21 image: rediscommander/redis-commander:latest 22 ports: 23 - "8081:8081" 24 environment: 25 - REDIS_HOSTS=local:redis:6379
Code Examples
Creating a new user:
1pub async fn create_user_handler( 2 State(app_state): State<AppState>, 3 Json(payload): Json<UserRegister>, 4) -> Result<Json<UserOutput>, MyError> { 5 if payload.name.is_none() || payload.email.is_none() || payload.password.is_none() { 6 return Err(MyError::Validation("Name, email and password are required".to_string())); 7 } 8 9 let exist_user = get_user_by_email(&app_state.pool, payload.email.clone().unwrap()).await?; 10 if exist_user.is_some() { 11 return Err(MyError::Validation("User already exists".to_string())); 12 } 13 14 let result = create_user(&app_state.pool, payload).await?; 15 Ok(Json(result)) 16}
RBAC middleware example:
1pub fn require_role( 2 required_roles: Vec<String>, 3) -> impl Clone + Send + Sync + 'static + Fn(Request<Body>, Next) -> Pin<Box<dyn Future<Output = Result<Response<Body>, MyError>> + Send>> 4{ 5 move | req: Request<Body>, next: Next| { 6 let required_roles = required_roles.clone(); 7 8 Box::pin(async move { 9 let claims = req.extensions().get::<Claims>().cloned().ok_or(MyError::Unauthorized)?; 10 let has_role = claims.roles.iter().any(|role| required_roles.contains(role)); 11 12 if !has_role { 13 return Err(MyError::Unauthorized); 14 } 15 16 Ok(next.run(req).await) 17 }) 18 } 19} 20 21// 4. Usage in Routes (src/routes/routes.rs) 22let admin_router = Router::new() 23 .route("/admin", get(|| async { "Route only for Admin" })) 24 .layer(from_fn(require_role(vec!["Admin".to_string()]))) // Require Admin role 25 .layer(from_fn_with_state(state.clone(), auth_middleware)); 26 27// 5. Multiple Role Requirements 28let moderator_router = Router::new() 29 .route("/moderate", get(|| async { "Route for Mods and Admins" })) 30 .layer(from_fn(require_role(vec!["Admin".to_string(), "Moderator".to_string()]))) // Either Admin OR Moderator 31 .layer(from_fn_with_state(state.clone(), auth_middleware));
Custom error handling:
1#[derive(Debug, Error)] 2pub enum MyError { 3 #[error("Database: {0}")] 4 DatabaseError(#[from] SqlxError), 5 #[error("Validation: {0}")] 6 Validation(String), 7 #[error("Unauthorized")] 8 Unauthorized, 9 #[error("Login error: {0}")] 10 LoginError(String), 11 #[error("Too many requests")] 12 TooManyRequests, 13}
Key Learnings
- Rust's ownership system forces careful architecture design.
- Async/await with Tokio simplifies concurrency but requires attention to error handling.
- Writing tests from day one improves reliability.
- Clear separation of concerns (handlers, middleware, db) makes the system maintainable.
- RBAC implementation taught the importance of centralized permission logic.
- Structured logging with tracing improves production debugging.
- DevOps automation from the start saves setup time and reduces errors.
Next Steps
- Add social login (OAuth)
- Implement email sending
- Implement notifications
- Implement audit logging
- Add GraphQL API as an alternative to REST
- Improve separation of business logic
- Production Docker images and Kubernetes manifests
- CI/CD pipelines
- Monitoring, logging, and auto-scaling configurations
Conclusion
Building this authentication service was challenging but extremely rewarding.
I improved my Rust skills and gained practical experience with software architecture, async programming, and clean code principles.
Even if Rust seems intimidating, hands-on practice is the fastest way to learn. The compiler becomes your best friend in building robust, production-ready software.
Project Repository: GitHub - Rust Auth Service
External Links and Resources:
- Rust Official Documentation
- Tokio Async Runtime
- Axum Web Framework
- SQLx ORM
- Redis
- Argon2 Password Hashing
- Utoipa (OpenAPI generation)
- Thiserror (Custom errors)
- Tracing for Logging
These resources provide further reading and official documentation for the tools and libraries used in this project.