A lightweight, production-ready Rust library for Identity and Access Management (IAM) with PostgreSQL backend. Provides secure authentication, token management, and email verification for modern applications.
-
Multiple Authentication Methods
- Email and password authentication with Argon2 password hashing
- Google OAuth integration via ID token verification
- Configurable password policies with complexity requirements
-
Token Management
- JWT-based access and refresh tokens
- Token storage in PostgreSQL for revocation and validation
- Automatic token expiration and refresh flow
- Protection against token reuse attacks
-
Email Verification
- Time-limited verification codes
- Automatic email sending via configurable
EmailSendertrait - Built-in support for Lettre SMTP transport
-
Database Integration
- PostgreSQL storage with SQLx
- Programmatic schema creation
- Support for master-slave database setups
-
Distributed System Support
- PostgreSQL advisory locks for distributed deployments
- Prevents race conditions in token refresh, email verification, and password reset
- Supports master-slave PostgreSQL setups
-
Production Ready
- Automatic retry logic for transient database errors
- Comprehensive error handling
- Structured logging with tracing
- Configurable TTLs and security policies
Add to your Cargo.toml:
[dependencies]
nano-iam = { path = "../nano-iam" }
# Or from crates.io when published
# nano-iam = "0.1.0"The recommended way to set up the database schema is using SQLx migrations:
use nano_iam::repo::Repo;
use sqlx::postgres::PgPoolOptions;
async fn setup_database(database_url: &str) -> anyhow::Result<()> {
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(database_url)
.await?;
let repo = Repo::new(pool);
repo.migrate().await?;
Ok(())
}Migrations are located in the src/migrations/ directory and are automatically applied when you call migrate(). The migration system uses PostgreSQL advisory locks to ensure only one instance runs migrations at a time.
Alternatively, you can create the schema manually with SQL:
create type token_type as enum ('access', 'refresh');
create table accounts (
id uuid primary key,
email text not null unique,
password_hash text not null,
email_verified boolean not null default false,
auth_type text not null default 'email',
created_at timestamptz not null,
updated_at timestamptz not null,
deleted_at timestamptz
);
create table tokens (
id uuid primary key,
account_id uuid not null references accounts(id) on delete cascade,
token text not null unique,
token_type token_type not null,
expires_at timestamptz not null,
created_at timestamptz not null,
revoked_at timestamptz,
root_token text,
usage bigint not null default 0
);
create table email_verifications (
id uuid primary key,
account_id uuid not null references accounts(id) on delete cascade,
code text not null,
expires_at timestamptz not null,
consumed_at timestamptz,
created_at timestamptz not null
);Note: The migrate() method is idempotent and can be called multiple times safely.
Main types to use:
AuthService: main entry point (takesEmailSenderinstance as parameter)AuthConfig,TokenConfig,EmailVerificationConfig,PasswordPolicy: configurationLoginResult,RefreshResultAccount,AccountId
Construct the service (example with SQLx and lettre):
use std::sync::Arc;
use nano_iam::{
AuthConfig,
AuthService,
EmailVerificationConfig,
TokenConfig,
PasswordPolicy,
};
use nano_iam::email::LettreEmailSender;
use nano_iam::repo::Repo;
use chrono::Duration;
use lettre::message::Mailbox;
use lettre::AsyncSmtpTransport;
use lettre::Tokio1Executor;
use sqlx::postgres::PgPoolOptions;
async fn build_auth_service(database_url: &str) -> anyhow::Result<AuthService> {
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(database_url)
.await?;
let repo = Repo::new(pool.clone());
// Create lock for distributed deployment (required)
let lock = nano_iam::locks::LeaseLock::new(pool);
let transport = AsyncSmtpTransport::<Tokio1Executor>::relay("smtp.example.com")?
.build();
let from: Mailbox = "Auth <no-reply@example.com>".parse()?;
let email_sender = Arc::new(LettreEmailSender::new(transport, from));
let cfg = AuthConfig {
token: TokenConfig {
access_ttl: Duration::minutes(15),
refresh_ttl: Duration::days(30),
},
email_verification: EmailVerificationConfig {
code_ttl: Duration::minutes(10),
code_length: 6,
},
password_policy: PasswordPolicy::default(), // Or customize password requirements
service_name: None, // Optional service name for email templates
};
Ok(AuthService::new(repo, email_sender, cfg, lock))
}Note: Locks are required for distributed PostgreSQL deployments to prevent race conditions in critical operations like token refresh, email verification, and password reset.
let account = auth_service
.register("user@example.com", "password123")
.await?;This will:
- create the account row
- generate an email verification code and row
- send an email with that code using
EmailSender
use nano_iam::models::AuthType;
// Verify Google ID token first
let email = nano_iam::google_oauth::verify_google_id_token(id_token).await?;
// Register with Google auth type
let account = auth_service
.register_with_auth_type(&email, "", AuthType::Google)
.await?;Note: Set the GOOGLE_OAUTH_CLIENT_ID environment variable for OAuth verification.
auth_service
.verify_email(account.id, "123456")
.await?;let LoginResult { account, tokens } = auth_service
.login("user@example.com", "password123")
.await?;
// Use tokens.access_token as bearer token in API responses
// Store tokens.refresh_token in http-only cookie or secure storage on clientlet account = auth_service
.authenticate_access_token(access_token_from_header)
.await?;
// account.id identifies the current userlet RefreshResult { account, tokens } = auth_service
.refresh(refresh_token_from_cookie)
.await?;This will:
- validate the refresh token
- revoke the old refresh token
- issue new access and refresh tokens
Customize password requirements:
let password_policy = PasswordPolicy {
min_length: 12,
require_uppercase: true,
require_lowercase: true,
require_digit: true,
require_special: true,
block_common_passwords: true,
};Adjust token lifetimes:
let token_config = TokenConfig {
access_ttl: Duration::minutes(15), // Short-lived access tokens
refresh_ttl: Duration::days(30), // Longer-lived refresh tokens
};Configure verification code settings:
let email_verification_config = EmailVerificationConfig {
code_ttl: Duration::minutes(10), // Code expiration time
code_length: 6, // Code length (numeric)
};This library is designed for distributed PostgreSQL deployments and uses advisory locks to prevent race conditions:
- Token Refresh: Prevents double-spending the same refresh token
- Email Verification: Prevents using the same verification code twice
- Password Reset: Prevents using the same reset code twice
- Google OAuth Account Creation: Prevents duplicate account creation
- Change Password: Prevents concurrent password changes causing inconsistent state
- Cleanup Expired Objects: Prevents multiple cleanup jobs running simultaneously
When using master-slave PostgreSQL:
- Always use the master database connection for locks
- Read replicas cannot acquire advisory locks
- All lock operations must go through the master
- Use the same database pool for both
RepoandLeaseLock
The library uses IamError for all error cases:
use nano_iam::IamError;
match auth_service.login(email, password).await {
Ok(result) => { /* success */ }
Err(IamError::InvalidCredentials) => { /* wrong password */ }
Err(IamError::AccountNotFound) => { /* email not found */ }
Err(IamError::WeakPassword(msg)) => { /* password doesn't meet policy */ }
Err(e) => { /* other error */ }
}MIT