Skip to main content

Authentication

TrailBase provides a complete authentication system with JWT tokens, OAuth integration, email verification, and flexible access control.

Overview

The authentication system supports multiple flows:
  • Email/Password Authentication: Traditional username and password login
  • OAuth Providers: Google, GitHub, Discord, Microsoft, and more
  • JWT Tokens: Stateless authentication with EdDSA signatures
  • Refresh Tokens: Long-lived tokens stored in secure cookies
  • Email Verification: Account verification workflow
  • Password Reset: Secure password recovery

Auth Module

All authentication logic is implemented in crates/core/src/auth/.

User Model

Database Schema

Users are stored in the _user table:
// From auth/user.rs:14-42
pub struct DbUser {
  pub id: [u8; 16],                          // UUIDv7 as bytes
  pub email: String,
  pub password_hash: String,                 // Argon2 hash
  pub verified: bool,
  pub admin: bool,
  
  pub created: i64,
  pub updated: i64,
  
  // Email verification
  pub email_verification_code: Option<String>,
  pub email_verification_code_sent_at: Option<i64>,
  pub pending_email: Option<String>,
  
  // Password reset
  pub password_reset_code: Option<String>,
  pub password_reset_code_sent_at: Option<i64>,
  
  // OAuth authorization
  pub authorization_code: Option<String>,
  pub authorization_code_sent_at: Option<i64>,
  pub pkce_code_challenge: Option<String>,
  
  // OAuth provider data
  pub provider_id: i64,
  pub provider_user_id: Option<String>,
  pub provider_avatar_url: Option<String>,
}

Authenticated User

Once authenticated, users are represented by the User struct:
// From auth/user.rs:86-96
pub struct User {
  pub id: String,           // Base64-encoded UUID
  pub email: String,
  pub uuid: Uuid,           // Convenience UUID representation
  pub csrf_token: String,   // CSRF protection token
}
The User type can be extracted from Axum request handlers using FromRequestParts.

JWT Tokens

JWT Helper

JWT token generation and validation using EdDSA (Ed25519) signatures.

Token Structure

// From auth/jwt.rs:29-49
pub struct TokenClaims {
  pub sub: String,         // Base64 encoded user ID
  pub iat: i64,           // Issued at (Unix timestamp)
  pub exp: i64,           // Expiration (Unix timestamp)
  pub email: String,       // User's email address
  pub csrf_token: String,  // Random CSRF token
  pub admin: bool,        // Is admin user
}

Key Management

JWT keys are automatically generated on first startup:
  • Private Key: traildepot/secrets/key/private_key.pem (EdDSA)
  • Public Key: traildepot/secrets/key/public_key.pem
// From auth/jwt.rs:89-98
pub struct JwtHelper {
  header: Header,
  validation: Validation,
  encoding_key: EncodingKey,  // For creating tokens
  decoding_key: DecodingKey,  // For validating tokens
  public_key: String,
}
Never commit your private key to version control. It’s automatically excluded via .gitignore.

Token Lifetimes

From constants.rs:
pub const DEFAULT_AUTH_TOKEN_TTL: Duration = Duration::minutes(60);
pub const DEFAULT_REFRESH_TOKEN_TTL: Duration = Duration::days(30);
  • Auth Token: Short-lived (60 minutes), sent in headers
  • Refresh Token: Long-lived (30 days), stored in secure HTTP-only cookie

Authentication Flows

Email/Password Registration

1. POST /api/auth/v1/register
   Body: { "email": "[email protected]", "password": "secure123" }
   
2. Server creates unverified user
3. Email verification code sent
4. Response: { auth_token, refresh_token, user_id }
Users can log in even without email verification, but some features may require verified=true.

Email/Password Login

1. POST /api/auth/v1/login
   Body: { "email": "[email protected]", "password": "secure123" }
   
2. Server validates credentials with Argon2
3. Lookup user sessions
4. Generate JWT tokens
5. Response: { auth_token, refresh_token, user_id }
Implemented in auth/api/login.rs using login_with_password() from auth/util.rs.

OAuth Flow

OAuth Integration

Supports multiple OAuth providers with PKCE for security.
Supported providers:
  • Google
  • GitHub
  • GitLab
  • Discord
  • Microsoft
  • Facebook
  • Twitch
  • Yandex
  • Generic OIDC (OpenID Connect)
OAuth Flow:
1. GET /api/auth/v1/oauth/login/{provider}
   Query: ?redirect_uri=<app_callback>&code_challenge=<pkce>
   
2. Server redirects to provider's authorization page

3. User authorizes on provider site

4. Provider redirects to: /api/auth/v1/oauth/callback/{provider}
   Query: ?code=<auth_code>&state=<state>
   
5. Server exchanges code for tokens
6. Fetch user profile from provider
7. Create or update user in _user table
8. Redirect to app with authorization code

9. Client: POST /api/auth/v1/token
   Body: { "code": "<auth_code>", "code_verifier": "<pkce_verifier>" }
   
10. Response: { auth_token, refresh_token, user_id }

Token Refresh

1. POST /api/auth/v1/refresh
   Cookie: refresh_token=<token>
   
2. Server validates refresh token
3. Check session validity
4. Generate new auth token
5. Response: { auth_token, refresh_token }
Implemented in auth/api/refresh.rs.

Password Security

Password Hashing

Passwords are hashed using Argon2id with secure parameters.
// Password hashing uses Argon2id
pub fn hash_password(password: &str) -> Result<String, AuthError> {
  let salt = SaltString::generate(&mut OsRng);
  let argon2 = Argon2::default();
  let hash = argon2
    .hash_password(password.as_bytes(), &salt)
    .map_err(|_| AuthError::Internal("password hashing"))?;
  Ok(hash.to_string())
}
Password Requirements (configurable):
  • Minimum length
  • Complexity rules
  • Common password prevention

Email Verification

Verification Flow

1. User registers account
2. Server generates random verification code
3. Email sent with verification link
4. GET /api/auth/v1/verify_email/confirm/{code}
5. Server marks user as verified
6. Redirect to application

Re-send Verification

GET /api/auth/v1/verify_email/trigger
Authorization: Bearer <auth_token>

Password Reset

Reset Flow

1. POST /api/auth/v1/reset_password/request
   Body: { "email": "[email protected]" }
   
2. Server generates reset code
3. Email sent with reset link

4. POST /api/auth/v1/reset_password/update
   Body: { 
     "email": "[email protected]",
     "code": "<reset_code>",
     "new_password": "new_secure123"
   }
   
5. Password updated, user can log in
Reset codes expire after a configured time period (default: 1 hour).

Sessions

Sessions are stored in the _session table:
CREATE TABLE _session (
  id              BLOB PRIMARY KEY NOT NULL,
  user_id         BLOB NOT NULL REFERENCES _user(id),
  refresh_token   TEXT NOT NULL,
  csrf_token      TEXT NOT NULL,
  created         INTEGER NOT NULL,
  expires         INTEGER NOT NULL
) STRICT;

Session Management

  • Creation: New session created on login
  • Validation: Checked on token refresh
  • Logout: Deletes session and invalidates refresh token
  • Expiration: Sessions auto-expire based on expires timestamp

Access Control

Table-Level ACLs

Access Control Lists define base permissions for unauthenticated and authenticated users.
record_apis {
  name: "posts"
  table_name: "post"
  
  # Unauthenticated users can READ
  acl_world: [READ]
  
  # Authenticated users can do everything
  acl_authenticated: [CREATE, READ, UPDATE, DELETE, SCHEMA]
}
Permissions from records/mod.rs:103-110:
pub enum Permission {
  Create = 1,   // Create new records
  Read = 2,     // Read records
  Update = 4,   // Update records
  Delete = 8,   // Delete records
  Schema = 16,  // View JSON schema
}

Row-Level Access Rules

Access Rules

SQL expressions that control access to individual records.
Access rules are SQL expressions with special variables:
  • _USER_: Current authenticated user (or NULL)
  • _ROW_: The record being accessed
  • _REQ_: The request payload (for CREATE/UPDATE)
Example Rules:
# Anyone can read all posts
read_access_rule: "TRUE"

# Only authenticated users can create
create_access_rule: "_USER_.id IS NOT NULL"

# Only post owner can update
update_access_rule: "_ROW_.user_id = _USER_.id"

# Only owner or admin can delete
delete_access_rule: "_ROW_.user_id = _USER_.id OR _USER_.admin = 1"
Complex Example:
# Can only publish own posts, admins can publish any
update_access_rule: |
  _ROW_.user_id = _USER_.id AND (
    _REQ_.published IS NULL OR 
    _REQ_.published = _ROW_.published OR
    _USER_.admin = 1
  )

CSRF Protection

Admin API endpoints require CSRF token validation to prevent cross-site request forgery.
CSRF protection flow:
  1. Auth token includes csrf_token in claims
  2. Client extracts token from auth token
  3. Client sends token in x-csrf-token header
  4. Server validates token matches user’s token
// From server/mod.rs:572-597
async fn assert_admin_api_access(
  State(state): State<AppState>,
  mut req: Request,
  next: Next,
) -> Result<Response, AuthError> {
  let user = req.extract_parts_with_state::<User, _>(&state).await?;
  
  if !is_admin(&state, &user.uuid).await {
    return Err(AuthError::Forbidden);
  }
  
  // Validate CSRF token
  let Some(received_csrf_token) = req
    .headers()
    .get(HEADER_CSRF_TOKEN)
    .and_then(|header| header.to_str().ok())
  else {
    return Err(AuthError::BadRequest("admin APIs require csrf header"));
  };
  
  if user.csrf_token != received_csrf_token {
    return Err(AuthError::BadRequest("invalid CSRF token"));
  }
  
  Ok(next.run(req).await)
}

Rate Limiting

Rate Limiter

IP-based rate limiting prevents authentication abuse.
Auth endpoints have rate limiting:
  • Burst: 5 requests (50 in debug mode)
  • Replenish: 1 request every 2 seconds
  • Scope: POST methods only (login, register, etc.)
  • Key: Client IP address from headers or connection
// Rate limiter configuration
let governor_conf = GovernorConfigBuilder::default()
  .burst_size(5)
  .per_second(2)
  .key_extractor(RealIpKeyExtractor)
  .use_headers()
  .methods(vec![Method::POST])
  .finish()?;

Configuration

Auth Options

auth {
  # Disable password authentication (OAuth only)
  disable_password_auth: false
  
  # Custom URI schemes for mobile deep links
  custom_uri_schemes: ["myapp"]
  
  # OAuth providers
  oauth_providers {
    key: "google"
    value {
      provider_id: GOOGLE
      client_id: "<google_client_id>"
      client_secret: "<google_client_secret>"
    }
  }
}

OAuth Provider Setup

Google OAuth

  1. Create project in Google Cloud Console
  2. Enable Google+ API
  3. Create OAuth 2.0 credentials
  4. Set redirect URI: <site_url>/api/auth/v1/oauth/callback/google

GitHub OAuth

  1. Go to Settings > Developer settings > OAuth Apps
  2. Register new application
  3. Set callback URL: <site_url>/api/auth/v1/oauth/callback/github
  4. Copy client ID and secret

Best Practices

Use HTTPS

Always use TLS in production to protect tokens in transit

Short Auth Tokens

Keep auth token TTL short (minutes), rely on refresh tokens

Validate Email

Require email verification for sensitive operations

Implement Logout

Always provide logout to invalidate sessions

Security Considerations

  • Token Storage: Store refresh tokens in HTTP-only, secure cookies
  • Token Rotation: Refresh tokens are rotated on each use
  • Session Tracking: All sessions are tracked in database
  • IP Validation: Consider validating IP for sensitive operations
  • Admin Separation: Admin endpoints require both auth + CSRF

Next Steps

APIs

Learn how to make authenticated API requests

Data Model

Understand how access control integrates with data

Build docs developers (and LLMs) love