Comprehensive security guidelines and best practices for RustAuth.
- Authentication Security
- Password Security
- JWT Token Security
- API Security
- Database Security
- Deployment Security
- Data Protection
- Security Checklist
Minimum Standards:
- Length: 12+ characters recommended
- Complexity: Uppercase, lowercase, numbers, special characters
- No common patterns or dictionary words
- No personal information (name, email, etc.)
Example Validation:
pub fn validate_password(password: &str) -> Result<(), String> {
if password.len() < 12 {
return Err("Password must be at least 12 characters".to_string());
}
if !password.chars().any(|c| c.is_uppercase()) {
return Err("Password must contain uppercase letter".to_string());
}
if !password.chars().any(|c| c.is_lowercase()) {
return Err("Password must contain lowercase letter".to_string());
}
if !password.chars().any(|c| c.is_numeric()) {
return Err("Password must contain number".to_string());
}
if !password.chars().any(|c| !c.is_alphanumeric()) {
return Err("Password must contain special character".to_string());
}
Ok(())
}Algorithm: Argon2
- Resistant to GPU/ASIC attacks
- Memory-hard function
- Configurable time/memory cost
use argon2::{Argon2, PasswordHasher, PasswordHash, PasswordVerifier};
use argon2::password_hash::SaltString;
use rand_core::OsRng;
// Hash password
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)?
.to_string();
// Verify password
let parsed_hash = PasswordHash::new(&stored_hash)?;
argon2.verify_password(password.as_bytes(), &parsed_hash)?;Implement account lockout after failed login attempts:
pub struct LoginAttempt {
pub user_id: Uuid,
pub failed_attempts: i32,
pub locked_until: Option<DateTime<Utc>>,
}
// Lock account after 5 failed attempts for 15 minutes
const MAX_LOGIN_ATTEMPTS: i32 = 5;
const LOCKOUT_DURATION_MINUTES: i64 = 15;
pub async fn check_login_attempts(
pool: &PgPool,
email: &str,
) -> Result<bool, Error> {
let user = sqlx::query!("
SELECT failed_attempts, locked_until FROM user.users
WHERE email = $1
", email)
.fetch_optional(pool)
.await?;
if let Some(user) = user {
if let Some(locked_until) = user.locked_until {
if locked_until > Utc::now() {
return Ok(false); // Account locked
}
}
}
Ok(true)
}Security Measures:
- One-time use tokens
- 24-hour expiration
- Secure token generation (96-bit entropy)
- Invalid tokens don't reveal user existence
use rand::Rng;
pub fn generate_reset_token() -> String {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
abcdefghijklmnopqrstuvwxyz\
0123456789";
let mut rng = rand::thread_rng();
(0..32)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect()
}
pub async fn reset_password(
pool: &PgPool,
token: &str,
new_password: &str,
) -> Result<(), Error> {
// Validate password
validate_password(new_password)?;
// Find unexpired, unused token
let token_record = sqlx::query!(
"SELECT user_id FROM user.password_reset_tokens
WHERE token = $1
AND expires_at > NOW()
AND used = false",
token
)
.fetch_optional(pool)
.await?
.ok_or(ApiError::InvalidToken)?;
let password_hash = hash_password(new_password).await?;
// Hash password
sqlx::query!(
"UPDATE user.users SET password = $1, updated_at = NOW()
WHERE id = $2",
password_hash,
token_record.user_id
)
.execute(pool)
.await?;
// Mark token as used
sqlx::query!(
"UPDATE user.password_reset_tokens SET used = true
WHERE token = $1",
token
)
.execute(pool)
.await?;
Ok(())
}❌ Don't:
- Store passwords in plain text
- Use simple hashing (MD5, SHA1, SHA256)
- Implement custom crypto
- Log passwords
- Send passwords via email
- Allow unlimited password reset attempts
✅ Do:
- Use Argon2 for hashing
- Use industry-standard libraries
- Rate limit password reset
- Implement account lockout
- Notify via email on resets
- Regenerate sessions after password change
Token Components:
#[derive(Debug, Serialize, Deserialize)]
pub struct JwtClaims {
pub sub: String, // User ID
pub iat: i64, // Issued at
pub exp: i64, // Expiration
pub roles: Vec<String>, // User roles
pub permissions: Vec<String>, // User permissions
}
pub fn generate_token(
user_id: &Uuid,
roles: Vec<String>,
secret: &str,
expires_in_hours: u64,
) -> Result<String, JwtError> {
let now = Utc::now();
let expiry = now + Duration::hours(expires_in_hours as i64);
let claims = JwtClaims {
sub: user_id.to_string(),
iat: now.timestamp(),
exp: expiry.timestamp(),
roles,
permissions: vec![],
};
let token = jsonwebtoken::encode(
&jsonwebtoken::Header::default(),
&claims,
&jsonwebtoken::EncodingKey::from_secret(secret.as_ref()),
)?;
Ok(token)
}pub fn validate_token(
token: &str,
secret: &str,
) -> Result<JwtClaims, JwtError> {
let token_data = jsonwebtoken::decode::<JwtClaims>(
token,
&jsonwebtoken::DecodingKey::from_secret(secret.as_ref()),
&jsonwebtoken::Validation::default(),
)?;
Ok(token_data.claims)
}Security Measures:
- Short expiration: 24 hours for access tokens
- Refresh token rotation: Issue new refresh token on each use
- Token blacklisting: Invalidate tokens on logout
- Secure storage: Use httpOnly, secure cookies
- HTTPS only: Never transmit over HTTP
- No sensitive data: Don't include passwords or PII in claims
Token Storage:
// Configuration for secure token storage
pub struct TokenConfig {
// JWT Secret (min 64 chars)
pub jwt_secret: String,
// Token expiration
pub access_token_expiry_hours: u64,
pub refresh_token_expiry_days: u64,
// Storage
pub use_secure_cookies: bool,
pub use_same_site: String,
}
// Recommended .env values
// JWT_SECRET=your-very-long-secure-random-key-min-64-chars
// JWT_EXPIRY_HOURS=24
// JWT_REFRESH_EXPIRY_DAYS=7use tower_http::cors::CorsLayer;
use axum::http::{Method, header};
let cors = CorsLayer::new()
// Specify allowed origins (not wildcard in production)
.allow_origin("https://example.com".parse()?)
.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::PATCH,
])
.allow_headers([
header::CONTENT_TYPE,
header::AUTHORIZATION,
])
.allow_credentials()
.max_age(Duration::from_secs(3600));Implement rate limiting to prevent abuse:
use tower_governor::Governor;
let auth_limiter = Governor::builder()
.per_second(10)
.burst_size(20)
.finish()
.unwrap();
// Apply to sensitive endpoints
let auth_routes = Router::new()
.route("/auth/login", post(login))
.route("/auth/register", post(register))
.layer(GovernorLayer {
state: Box::new(auth_limiter),
key_extractor: Box::new(|req| {
req.headers()
.get("x-forwarded-for")
.and_then(|h| h.to_str().ok())
.unwrap_or("127.0.0.1")
.to_string()
}),
});Always validate user input:
use validator::{Validate, ValidationError};
#[derive(Validate, Deserialize)]
pub struct LoginRequest {
#[validate(email)]
pub email: String,
#[validate(length(min = 8, max = 255))]
pub password: String,
}
pub async fn login(
Json(req): Json<LoginRequest>,
) -> Result<Json<AuthResponse>> {
req.validate()?; // Returns error if validation fails
// ... rest of handler
}Use parameterized queries with SQLx:
// ✅ GOOD - Parameterized query
let user = sqlx::query_as::<_, User>(
"SELECT * FROM user.users WHERE email = $1"
)
.bind(email)
.fetch_optional(&pool)
.await?;
// ❌ BAD - String interpolation (vulnerable)
let query = format!(
"SELECT * FROM user.users WHERE email = '{}'",
email // SQL injection vulnerability!
);// Use SSL/TLS for database connections
pub async fn init_pool(url: &str) -> Result<PgPool> {
let connect_options = PgConnectOptions::from_str(url)?
.ssl_mode(sqlx::postgres::PgSslMode::Require);
let pool = PgPoolOptions::new()
.max_connections(10)
.acquire_timeout(Duration::from_secs(30))
.connect_with(connect_options)
.await?;
Ok(pool)
}Apply principle of least privilege:
-- Create role for application
CREATE ROLE rustauth WITH PASSWORD 'strong_password';
-- Grant schema permissions
GRANT USAGE ON SCHEMA user, blogs TO rustauth;
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA user TO rustauth;
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA blogs TO rustauth;
-- Deny sensitive operations
REVOKE DELETE ON user.users FROM rustauth;
REVOKE ALL ON postgres FROM rustauth;At Rest:
// Use database encryption
// PostgreSQL: pgcrypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto;
// Store sensitive data encrypted
ALTER TABLE user.users
ADD COLUMN ssn_encrypted bytea;
INSERT INTO user.users (ssn_encrypted)
VALUES (pgp_sym_encrypt('123-45-6789', 'encryption_key'));
SELECT pgp_sym_decrypt(ssn_encrypted, 'encryption_key') FROM user.users;In Transit:
- Always use HTTPS
- Use SSL/TLS for database connections
- Use VPN for administrative access
Never commit secrets:
# .env (local development - use .env.example for git)
DATABASE_URL=postgres://user:password@localhost:5432/auth_dev
JWT_SECRET=your-secret-key # Min 64 characters
REDIS_URL=redis://127.0.0.1:6379
# Production (set via environment)
export DATABASE_URL=postgres://...
export JWT_SECRET=...FROM rust:1.70 as builder
WORKDIR /app
COPY . .
RUN cargo build --release
# Production image
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
# Don't run as root
RUN useradd -m -u 1001 appuser
USER appuser
COPY --from=builder /app/target/release/rustauth /usr/local/bin/
EXPOSE 8000
CMD ["rustauth"]Use secrets management tools:
# Kubernetes Secrets
kubectl create secret generic rustauth \
--from-literal=database-url="postgres://..." \
--from-literal=jwt-secret="..."
# AWS Secrets Manager
aws secretsmanager create-secret \
--name rustauth/prod \
--secret-string file://secrets.json
# HashiCorp Vault
vault write secret/rustauth/prod \
database_url="..." \
jwt_secret="..."Sensitive Fields:
- Email addresses
- Phone numbers
- Passwords
- IP addresses
- Location data
Protection Measures:
- Encrypt at rest
- Hash where possible (emails)
- Minimize collection
- Secure deletion
- Access logging
// Right to be forgotten
pub async fn delete_user_data(
pool: &PgPool,
user_id: Uuid,
) -> Result<()> {
// Delete user personal data
sqlx::query!("
UPDATE user.users
SET
email = 'deleted-' || gen_random_uuid(),
password = '',
full_name = NULL,
phone_number = NULL,
avatar_url = NULL,
is_active = false
WHERE id = $1
", user_id)
.execute(pool)
.await?;
// Delete related data
sqlx::query!("DELETE FROM user.refresh_tokens WHERE user_id = $1", user_id)
.execute(pool)
.await?;
Ok(())
}- All secrets in environment variables (not code)
- JWT_SECRET is strong (min 64 chars)
- HTTPS/TLS enabled
- Database uses SSL/TLS
- Firewall configured
- Rate limiting enabled
- Input validation on all endpoints
- CORS properly configured (not wildcard)
- Logging doesn't contain passwords/tokens
- Secrets not in Git history
- Database backups encrypted
- Admin credentials changed
- Security headers configured
- Tests include security checks
- Log all authentication attempts
- Monitor failed login attempts
- Alert on suspicious activity
- Track API endpoint usage
- Monitor database queries
- Regular security audits
For more information:
- DEPLOYMENT.md - Deployment guide
- API.md - API security
- DATABASE.md - Database security