rust-auth (0.1.0)
Installation
[registries.forgejo]
index = "sparse+ " # Sparse index
# index = " " # Git
[net]
git-fetch-with-cli = truecargo add rust-auth@0.1.0 --registry forgejoAbout 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 |