Skip to content

🦀 Exploring Rust Through a Real Authentication Project

Aug 18, 2025 • Rust •9 min

Share on:

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:

These resources provide further reading and official documentation for the tools and libraries used in this project.

Other notes

🦀 Exploring Rust Through a Real Authentication Project | Iure