rust-auth (0.1.0)

Published 2026-05-31 21:23:30 +02:00 by bagro

Installation

[registries.forgejo]
index = "sparse+" # Sparse index
# index = "" # Git

[net]
git-fetch-with-cli = true
cargo add rust-auth@0.1.0 --registry forgejo

About this package

rust-auth

A framework-agnostic authentication and authorization library for Rust.

Handles JWT (ES256), refresh token rotation with family-based theft detection, bcrypt password hashing, email verification, password reset, rate limiting, OAuth (Google), and role/attribute-based authorization.

The library owns no HTTP layer and has no opinion on your web framework. You implement two traits — AuthStore and AuthHooks — and the library does the rest.


Design

You own the database. The library communicates with your database exclusively through the AuthStore trait. Implement it against any database layer you prefer — sqlx, diesel, or an in-memory store for tests.

You own the hooks. AuthHooks lets your application react to auth events (user created, login, token theft detected) without coupling the library to your application logic. All hook methods have default no-op implementations — implement only what you need.

Tokens are opaque to the database. Refresh tokens are 32 random bytes hex-encoded. Only a SHA-256 hash is stored — never the token itself. Access tokens are short-lived JWTs signed with ES256.

Authorization is claims-based. Roles and attributes are embedded in the JWT at signing time via the on_build_claims hook. No extra database calls are needed on protected routes — the token carries everything the application needs to make authorization decisions.


Quick start

1. Generate EC keys

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out private.pem
openssl pkey -in private.pem -pubout -out public.pem

2. Implement AuthStore

use rust_auth::store::AuthStore;
use rust_auth::error::AuthError;
use rust_auth::models::*;
use uuid::Uuid;

struct PgStore {
    pool: sqlx::PgPool,
}

impl AuthStore for PgStore {
    async fn find_user_by_email(&self, email: &str) -> Result<Option<AuthUser>, AuthError> {
        // your sqlx query here
    }

    // ... remaining methods
}

3. Implement AuthHooks (optional)

use rust_auth::hooks::AuthHooks;
use rust_auth::error::AuthError;
use rust_auth::models::AuthUser;
use std::collections::HashMap;

struct AppHooks {
    db: Arc<PgStore>,
    mailer: Arc<Mailer>,
}

impl AuthHooks for AppHooks {
    async fn on_user_created(&self, user: &AuthUser) -> Result<(), AuthError> {
        self.mailer.send_welcome(user.email.clone()).await
            .map_err(|e| AuthError::Store(Box::new(e)))?;
        Ok(())
    }

    async fn on_token_theft_detected(&self, user_id: Uuid) -> Result<(), AuthError> {
        // log, alert, notify user
        Ok(())
    }

    async fn on_build_claims(
        &self,
        user: &AuthUser,
        roles: &mut Vec<String>,
        attributes: &mut HashMap<String, String>,
    ) -> Result<(), AuthError> {
        let subscription = self.db.get_subscription(user.id).await
            .map_err(|e| AuthError::Store(Box::new(e)))?;

        roles.push(subscription.plan.clone());
        attributes.insert("plan".to_string(), subscription.plan);
        attributes.insert("subscription_status".to_string(), subscription.status);

        Ok(())
    }
}

4. Create the service

use rust_auth::service::AuthService;
use rust_auth::token::TokenConfig;

let token_config = TokenConfig {
    private_key_pem: std::fs::read("private.pem")?,
    public_key_pem: std::fs::read("public.pem")?,
    access_token_lifetime_secs: 900, // 15 minutes
};

let service = AuthService::new(PgStore { pool }, AppHooks { db, mailer }, token_config)
    .with_bcrypt_cost(12);

5. Use in your handlers

// Register
let (user, tokens) = service
    .register(&email, &password, user_agent, ip_address)
    .await?;

// Login
let (user, tokens) = service
    .login(&email, &password, user_agent, ip_address)
    .await?;

// Refresh
let (user, tokens) = service
    .refresh(&refresh_token, user_agent, ip_address)
    .await?;

// Verify access token (on protected routes)
let claims = service.verify_access_token(&token).await?;

// Logout
service.logout(&refresh_token).await?;

Token flow

Register / Login
  → on_build_claims fires, application injects roles and attributes
  → creates access token (JWT, 15 min) with roles and attributes embedded
  → creates refresh token (opaque, 30 days)
  → saves refresh token hash + family_id to store

Refresh
  → looks up token hash in store
  → if revoked: revokes entire family, fires on_token_theft_detected → TokenFamilyRevoked
  → if expired: → TokenExpired
  → revokes old token, on_build_claims fires, issues new token pair (same family_id)

Logout
  → revokes the presented refresh token

Authorization

Roles and attributes are embedded in the JWT at signing time. Use the functions in rust_auth::authorization to enforce access on protected routes:

use rust_auth::authorization;

// Require all of the specified roles
authorization::require_roles(&claims, &["admin"])?;

// Require at least one of the specified roles
authorization::require_any_role(&claims, &["basic", "pro"])?;

// Require an attribute with an exact value
authorization::require_attribute(&claims, "subscription_status", "active")?;

// Require an attribute matching any of the specified values
authorization::require_any_attribute(&claims, "plan", &["basic", "pro"])?;

RBAC — assign roles in on_build_claims and check them with require_roles or require_any_role.

ABAC — assign attributes in on_build_claims and check them with require_attribute or require_any_attribute. Attributes are arbitrary key-value pairs, making them suitable for dynamic conditions like subscription status, organization membership, or feature flags.

Combining both — roles and attributes can be checked together in the same handler, giving full flexibility without coupling the library to any particular authorization model.

Since roles and attributes live in the JWT, authorization requires no database calls on protected routes. Access tokens expire after 15 minutes, which bounds the window of stale claims.


Error handling

All operations return Result<T, AuthError>. Map these to HTTP responses in your handler layer:

Error Suggested HTTP status
InvalidCredentials 401
TokenExpired 401
InvalidToken 401
TokenFamilyRevoked 401
AccountLocked 429
EmailNotVerified 403
Unauthorized(_) 403
Validation(_) 422
Store(_) 500

OAuth

Implement OAuthProvider or use the built-in GoogleOAuthProvider:

use rust_auth::oauth::google::GoogleOAuthProvider;

let provider = GoogleOAuthProvider::new(client_id, client_secret);
let user_info = provider.exchange_code(&code, &redirect_uri).await?;

let (user, tokens) = service
    .oauth_login(
        &user_info.provider,
        &user_info.provider_id,
        &user_info.email,
        user_agent,
        ip_address,
    )
    .await?;

Testing

The library is designed to be testable without a real database. Implement AuthStore with an in-memory store backed by Mutex<Vec<T>>:

#[derive(Default)]
struct FakeStore {
    users: Mutex<Vec<AuthUser>>,
    refresh_tokens: Mutex<Vec<RefreshTokenRecord>>,
    // ...
}

Use a low bcrypt cost in tests to keep them fast:

let service = AuthService::new(FakeStore::default(), NoopHooks, token_config)
    .with_bcrypt_cost(4);

Security notes

  • Refresh tokens are rotated on every use — a reused token revokes the entire family and fires on_token_theft_detected
  • Refresh tokens are stored as SHA-256 hashes — a database leak does not expose usable tokens
  • Passwords are hashed with bcrypt (default cost 12)
  • Account lockout after 5 failed login attempts (15 minute lockout)
  • Password reset and email verification tokens expire after 1 hour and 24 hours respectively and are single-use
  • All communication with the database goes through AuthStore — the library never holds a database connection
  • Authorization claims are embedded in short-lived JWTs — stale claims are bounded by the 15-minute access token lifetime

Crate features

No optional features. All functionality is included by default.


License

MIT

Dependencies

ID Version
bcrypt ^0.19
jsonwebtoken ^10
rand ^0.10
reqwest ^0.13
serde ^1
serde_json ^1
sha2 ^0.11
thiserror ^2
uuid ^1
tokio ^1
Details
Cargo
2026-05-31 21:23:30 +02:00
3
30 KiB
Assets (1)
Versions (1) View all
0.1.0 2026-05-31