Skip to content

Authentication

FORGE generates a complete JWT-based authentication system with support for multiple login methods, token refresh, and secure password handling. The system uses short-lived access tokens paired with longer-lived refresh tokens, following industry best practices for stateless API authentication.

Token Strategy

The authentication system uses a dual-token approach:

TokenLifetimePurposeStorage
Access Token15 minAuthorize API requestsAuthorization header or HttpOnly cookie
Refresh Token7 daysObtain new access tokensHttpOnly cookie
┌──────────┐     Login      ┌──────────┐
│  Client  │ ──────────────▶│  Server  │
│          │◀────────────── │          │
│          │  Access Token  │          │
│          │  Refresh Token │          │
│          │                │          │
│          │  API Request   │          │
│          │ ──────────────▶│          │
│          │  (Access Token)│          │
│          │                │          │
│          │  Token Expired │          │
│          │ ──────────────▶│          │
│          │ (Refresh Token)│          │
│          │◀────────────── │          │
│          │  New Tokens    │          │
└──────────┘                └──────────┘

TIP

Access tokens are intentionally short-lived. When an access token expires, the client should call the refresh endpoint to obtain a new pair of tokens without requiring the user to log in again.

Login Methods

FORGE supports three login methods out of the box:

Email + Password

The standard login flow using email and password credentials.

rust
// POST /api/auth/login
#[derive(Deserialize, Validate)]
pub struct LoginRequest {
    #[validate(email)]
    pub email: String,
    #[validate(length(min = 8))]
    pub password: String,
}

Mobile + Password

For applications that use phone numbers as the primary identifier.

rust
// POST /api/auth/login
#[derive(Deserialize, Validate)]
pub struct MobileLoginRequest {
    #[validate(length(min = 8, max = 15))]
    pub mobile: String,
    #[validate(length(min = 8))]
    pub password: String,
}

Mobile + OTP

Passwordless authentication using one-time passwords sent via SMS.

rust
// Step 1: Request OTP
// POST /api/auth/send-otp
#[derive(Deserialize, Validate)]
pub struct SendOtpRequest {
    #[validate(length(min = 8, max = 15))]
    pub mobile: String,
}

// Step 2: Verify OTP and authenticate
// POST /api/auth/verify-otp
#[derive(Deserialize, Validate)]
pub struct VerifyOtpRequest {
    #[validate(length(min = 8, max = 15))]
    pub mobile: String,
    #[validate(length(equal = 6))]
    pub otp: String,
}

Admin Two-Factor Authentication (2FA)

When both email_password and mobile_otp are enabled in your project configuration, FORGE automatically implements Two-Factor Authentication for admin panel access. This provides an additional security layer for administrative accounts.

2FA Flow

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  Email/Password │ ──▶ │   Send OTP to   │ ──▶ │   Verify OTP    │ ──▶ Admin Panel
│     Login       │     │  Admin's Mobile │     │   (6 digits)    │
└─────────────────┘     └─────────────────┘     └─────────────────┘

How It Works

  1. User submits email/password → Server validates credentials
  2. If user has mobile number → Server generates OTP, sends to mobile, returns requires_otp: true with a temporary token
  3. User enters OTP → Server verifies OTP and temporary token, returns access tokens
  4. If user has no mobile → 2FA is skipped, normal login proceeds

Login Response Types

When 2FA is enabled, the login endpoint returns a unified response:

Standard login (no 2FA required):

json
{
  "success": true,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
    "user": { "id": "...", "name": "...", "email": "..." }
  }
}

2FA required:

json
{
  "success": true,
  "data": {
    "requires_otp": true,
    "temp_token": "dXNlcl9pZDoxNzA2MTIzNDU2...",
    "mobile_masked": "+971*****567"
  }
}

Verify 2FA Endpoint

Complete the 2FA flow by verifying the OTP:

bash
POST /api/auth/otp/verify-2fa
Content-Type: application/json

{
  "temp_token": "dXNlcl9pZDoxNzA2MTIzNDU2...",
  "code": "123456"
}

Response (200):

json
{
  "success": true,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
    "user": { "id": "...", "name": "...", "email": "..." }
  }
}

Security Features

FeatureDescription
Temporary TokenHMAC-SHA256 signed, 5-minute expiry
Max Attempts3 OTP attempts before requiring re-login
Mobile MaskingDisplay +971*****567 for privacy
OTP Expiry5 minutes (configurable via OTP_EXPIRY)

Development Mode

For easier testing, when APP_ENV=development, the OTP code is always 123456:

rust
fn generate_otp_code(length: usize) -> String {
    if std::env::var("APP_ENV").unwrap_or_default() == "development" {
        return "123456".to_string();
    }
    // Production: generate random OTP
    // ...
}

Seed Users

When mobile_otp is enabled, seed users include mobile numbers:

UserEmailMobilePassword
Adminadmin@+971501234567password
Useruser@+971509876543password

Testing 2FA

  1. Run forge new myapp with both email_password and mobile_otp enabled
  2. Start the API with APP_ENV=development
  3. Login at /admin/login with admin@{domain} / password
  4. Enter static OTP 123456 on the verification screen

API Endpoints

Login

Authenticate a user and receive tokens.

bash
POST /api/auth/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "securepassword"
}

Response (200):

json
{
  "success": true,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
    "token_type": "Bearer",
    "expires_in": 900,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "John Doe",
      "email": "user@example.com"
    }
  }
}

Register

Create a new user account.

bash
POST /api/auth/register
Content-Type: application/json

{
  "name": "Jane Doe",
  "email": "jane@example.com",
  "password": "securepassword",
  "password_confirmation": "securepassword"
}

Refresh Token

Exchange a valid refresh token for a new token pair.

bash
POST /api/auth/refresh
Content-Type: application/json

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}

WARNING

Refresh tokens are single-use. Each call to the refresh endpoint invalidates the previous refresh token and issues a new pair. If a refresh token is used twice, both tokens are revoked as a security precaution (refresh token rotation).

Logout

Invalidate the current session tokens.

bash
POST /api/auth/logout
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Forgot Password

Initiate the password reset flow by sending a reset link to the user's email.

bash
POST /api/auth/forgot-password
Content-Type: application/json

{
  "email": "user@example.com"
}

Reset Password

Complete the password reset using the token from the email.

bash
POST /api/auth/reset-password
Content-Type: application/json

{
  "token": "reset-token-from-email",
  "password": "newsecurepassword",
  "password_confirmation": "newsecurepassword"
}

Send OTP

Request a one-time password for mobile authentication.

bash
POST /api/auth/send-otp
Content-Type: application/json

{
  "mobile": "+966500000000"
}

Verify OTP

Verify the OTP and authenticate the user.

bash
POST /api/auth/verify-otp
Content-Type: application/json

{
  "mobile": "+966500000000",
  "otp": "123456"
}

Password Hashing

All passwords are hashed using Argon2id, the winner of the Password Hashing Competition and the recommended algorithm for new applications.

rust
use argon2::{
    password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
    Argon2,
};

/// Hash a plaintext password using Argon2id
pub fn hash_password(password: &str) -> Result<String, AppError> {
    let salt = SaltString::generate(&mut OsRng);
    let argon2 = Argon2::default();
    let hash = argon2
        .hash_password(password.as_bytes(), &salt)
        .map_err(|_| AppError::InternalError("Failed to hash password".into()))?;
    Ok(hash.to_string())
}

/// Verify a password against a stored hash
pub fn verify_password(password: &str, hash: &str) -> Result<bool, AppError> {
    let parsed_hash = PasswordHash::new(hash)
        .map_err(|_| AppError::InternalError("Invalid password hash".into()))?;
    Ok(Argon2::default()
        .verify_password(password.as_bytes(), &parsed_hash)
        .is_ok())
}

TIP

FORGE uses Argon2id with default parameters (19 MiB memory, 2 iterations, 1 degree of parallelism). These settings provide strong security while keeping login response times under 500ms on modern hardware.

JWT Token Structure

Access tokens carry the user's identity and are signed with HMAC-SHA256:

rust
#[derive(Serialize, Deserialize)]
pub struct Claims {
    pub sub: String,        // User ID (UUID)
    pub email: String,      // User email
    pub exp: usize,         // Expiration timestamp
    pub iat: usize,         // Issued at timestamp
    pub token_type: String, // "access" or "refresh"
}

Token generation:

rust
use jsonwebtoken::{encode, Header, EncodingKey};

pub fn generate_access_token(user: &User, secret: &str) -> Result<String, AppError> {
    let now = chrono::Utc::now();
    let claims = Claims {
        sub: user.id.to_string(),
        email: user.email.clone(),
        exp: (now + chrono::Duration::minutes(15)).timestamp() as usize,
        iat: now.timestamp() as usize,
        token_type: "access".to_string(),
    };

    encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(secret.as_bytes()),
    )
    .map_err(|_| AppError::InternalError("Token generation failed".into()))
}

Token Delivery

Tokens can be delivered in two ways depending on the client type:

HTTP-Only Cookie (recommended for web apps):

rust
let cookie = format!(
    "access_token={}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900",
    access_token
);

Authorization Header (for mobile/SPA clients):

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

WARNING

Always prefer HttpOnly cookies for browser-based applications. They are immune to XSS attacks because JavaScript cannot access the token value. Use the Authorization header only for non-browser clients (mobile apps, CLI tools).

Environment Variables

Configure authentication behavior through environment variables:

bash
# JWT signing secret (required, use a strong random value)
JWT_SECRET=your-256-bit-secret-key-here

# Token lifetimes
ACCESS_TOKEN_EXPIRY=900      # 15 minutes in seconds
REFRESH_TOKEN_EXPIRY=604800  # 7 days in seconds

# Password reset
RESET_TOKEN_EXPIRY=3600      # 1 hour in seconds

# OTP settings
OTP_LENGTH=6
OTP_EXPIRY=300               # 5 minutes in seconds

See Also

Released under the MIT License.