Password Policy

UniAuth enforces a comprehensive password policy designed to protect user accounts from brute-force attacks, credential stuffing, and password reuse. This guide explains each layer of protection, the password reset flow, email verification, and account lockout behavior.

Password Requirements

All passwords must meet the following baseline requirements:

  • Minimum length: 8 characters
  • Strength score: At least 2 ("fair") on a 0–4 scale

UniAuth does not enforce arbitrary complexity rules such as "must contain an uppercase letter, a number, and a symbol." Research shows that these rules lead to predictable substitution patterns (e.g., P@ssw0rd!) without meaningfully increasing security. Instead, UniAuth uses intelligent strength scoring that evaluates the actual difficulty of guessing a password.

Strength Scoring

Every password is evaluated against a comprehensive scoring algorithm that considers dictionary words, common patterns (dates, sequences, keyboard walks), repetition, and estimated crack time. The result is a score from 0 to 4:

ScoreRatingDescription
0Too guessableRisky password — easily guessed
1Very guessableProtection from throttled online attacks only
2Somewhat guessable (minimum)Protection from unthrottled online attacks
3Safely unguessableModerate protection from offline attacks
4Very unguessableStrong protection from offline attacks

The scoring is context-aware: passwords that contain the user's email address, first name, last name, or username are penalized. For example, a user named "John Smith" choosing johnsmith2024 would receive a lower score than a random string of the same length.

The API response includes actionable feedback when a password is too weak:

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

{
  "email": "[email protected]",
  "password": "john2024",
  "firstName": "John"
}

// Response (400):
{
  "error": "Password is too weak",
  "score": 1,
  "feedback": {
    "warning": "This is similar to a commonly used password",
    "suggestions": [
      "Add another word or two — uncommon words are better",
      "Avoid sequences or repeated characters"
    ]
  }
}
Tip: Passphrases like correct-horse-battery-staple typically score 4/4 and are easier to remember than short, complex passwords.

Breach Detection

During registration and password changes, UniAuth checks whether the chosen password has appeared in known data breaches. This check uses the k-anonymity protocol: only the first 5 characters of the password's SHA-1 hash are sent to the breach database. The full password and its complete hash never leave UniAuth's servers.

If a password is found in breaches, UniAuth returns a non-blocking warning. The password is still accepted (assuming it meets the strength requirement), but the user is informed so they can make a better choice:

// Registration response with breach warning (200):
{
  "success": true,
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "[email protected]"
  },
  "breachWarning": "This password has appeared in data breaches. Consider choosing a different password."
}

The breach check has a 3-second timeout to avoid delaying registration if the external service is slow or unavailable. If the check times out, registration proceeds normally without a warning.

Password Reset Flow

UniAuth provides a secure, token-based password reset flow. The process involves four steps:

Step 1: Request a Reset

The user submits their email address to request a password reset link:

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

{
  "email": "[email protected]"
}

// Response (200) — always returns success:
{
  "success": true,
  "message": "If an account exists with that email, a reset link has been sent."
}
Security note: This endpoint always returns a success response, regardless of whether the email exists in the system. This prevents attackers from using the endpoint to discover which email addresses are registered. See the Email Enumeration Prevention section below.

Step 2: Email Delivery

If the account exists, UniAuth sends an email containing a unique, time-limited reset link. The link points to your application's password reset page with a cryptographic token as a query parameter. The token is hashed before storage, so even if the database were compromised, the token could not be used.

Step 3: Validate the Token

Before showing the password reset form, validate that the token is still valid:

POST /api/auth/validate-reset-token
Content-Type: application/json

{
  "token": "abc123def456..."
}

// Valid token (200):
{
  "valid": true
}

// Expired or invalid token (400):
{
  "error": "Invalid or expired reset token"
}

Step 4: Set the New Password

Submit the token along with the new password. The new password must pass the same strength requirements as during registration:

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

{
  "token": "abc123def456...",
  "password": "newSecurePassword!2024"
}

// Success (200):
{
  "success": true,
  "message": "Password has been reset successfully"
}

// Weak password (400):
{
  "error": "Password is too weak",
  "score": 1,
  "feedback": {
    "warning": "This is a top-100 common password",
    "suggestions": ["Add another word or two — uncommon words are better"]
  }
}

Upon successful reset, all existing sessions for the user are invalidated, requiring the user to sign in again with their new password. The reset token is also consumed and cannot be reused.

Email Verification

After registration, users must verify their email address before signing in. UniAuth automatically sends a verification email containing a unique link. When the user clicks the link, your application should call the verification endpoint:

POST /api/auth/verify-email
Content-Type: application/json

{
  "token": "verification-token-from-email-link"
}

// Success (200):
{
  "success": true,
  "message": "Email verified successfully"
}

// Invalid or expired token (400):
{
  "error": "Invalid or expired verification token"
}

Resending Verification Email

If the verification email was lost or expired, users can request a new one:

POST /api/auth/resend-verification
Content-Type: application/json

{
  "email": "[email protected]"
}

// Response (200):
{
  "success": true,
  "message": "If your email is registered and unverified, a new verification email has been sent."
}

Like the password reset endpoint, the resend endpoint returns a generic success message regardless of whether the email exists, preventing enumeration attacks.

Account Lockout

UniAuth implements progressive account lockout to protect against brute-force and credential-stuffing attacks. After a threshold of failed login attempts, the account is temporarily locked for an increasing duration:

Failed AttemptsLockout Duration
51 minute
105 minutes
1515 minutes
20+1 hour

When a locked account attempts to sign in, the API returns HTTP 423 with the remaining lockout time:

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

{
  "email": "[email protected]",
  "password": "wrongpassword"
}

// Response (423 Locked):
{
  "error": "Account is temporarily locked",
  "remainingTime": 245,
  "message": "Too many failed attempts. Please try again in 4 minutes and 5 seconds."
}

The lockout counter resets automatically when:

  • The user signs in successfully with the correct password
  • The user authenticates using a passkey
  • An administrator manually unlocks the account from the Admin Dashboard
Rate limiting: In addition to account lockout, the login endpoint is rate-limited to 10 requests per 15-minute window per IP address. This applies before the account lockout logic, providing defense-in-depth against distributed attacks.

Email Enumeration Prevention

Email enumeration is an attack where an adversary probes your authentication endpoints to determine which email addresses are registered. UniAuth prevents this by returning identical responses regardless of whether an email exists:

  • Forgot password: Always returns "If an account exists with that email, a reset link has been sent."
  • Resend verification: Always returns "If your email is registered and unverified, a new verification email has been sent."
  • Login: Returns a generic "Invalid email or password" for both non-existent accounts and incorrect passwords.

Response timing is also consistent across both cases to prevent timing-based enumeration. This design means your users' email addresses remain private even if an attacker targets these endpoints.

Best Practices

To maximize account security, we recommend the following practices for your users:

  • Use a password manager. Tools like 1Password, Bitwarden, or the built-in browser password manager generate and store strong, unique passwords for every account.
  • Enable multi-factor authentication (MFA). Even the strongest password can be compromised through phishing or data breaches. Adding a second factor (TOTP, email, or SMS) provides an additional layer of protection. See the MFA documentation.
  • Never reuse passwords. If one service is breached, reused credentials can be used to access other accounts through credential stuffing attacks.
  • Check for breaches regularly. If you receive a breach warning during registration or password change, choose a different password immediately. You can also check your email at haveibeenpwned.com.
  • Consider using passkeys. Passkeys provide phishing-resistant authentication that eliminates passwords entirely. See the Authentication documentation for setup instructions.
  • Prefer passphrases over complex passwords. A phrase like meadow-sunset-bicycle-thunder is both stronger and easier to remember than P@55w0rd!.

For Developers: Implementing Password Validation

If you are building a registration or password change form that connects to UniAuth, here is how to handle the strength validation response:

// JavaScript / TypeScript example
async function registerUser(email: string, password: string, firstName: string) {
  const response = await fetch('https://uniauth.id/api/auth/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password, firstName }),
  });

  const data = await response.json();

  if (!response.ok) {
    if (data.score !== undefined) {
      // Password too weak — show feedback to the user
      console.log('Score:', data.score, '/ 4');
      console.log('Warning:', data.feedback?.warning);
      console.log('Suggestions:', data.feedback?.suggestions);
    } else {
      // Other error (email taken, validation error, etc.)
      console.error(data.error);
    }
    return null;
  }

  // Registration succeeded
  if (data.breachWarning) {
    // Show non-blocking warning about breach
    console.warn(data.breachWarning);
  }

  return data.user;
}
# Python example
import requests

def register_user(email: str, password: str, first_name: str) -> dict | None:
    response = requests.post(
        "https://uniauth.id/api/auth/register",
        json={"email": email, "password": password, "firstName": first_name},
    )
    data = response.json()

    if response.status_code != 200:
        if "score" in data:
            print(f"Password score: {data['score']} / 4")
            print(f"Warning: {data.get('feedback', {}).get('warning')}")
            print(f"Suggestions: {data.get('feedback', {}).get('suggestions')}")
        else:
            print(f"Error: {data.get('error')}")
        return None

    if "breachWarning" in data:
        print(f"Warning: {data['breachWarning']}")

    return data["user"]
<?php
// PHP example
function registerUser(string $email, string $password, string $firstName): ?array {
    $ch = curl_init('https://uniauth.id/api/auth/register');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS => json_encode([
            'email' => $email,
            'password' => $password,
            'firstName' => $firstName,
        ]),
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    $data = json_decode($response, true);

    if ($httpCode !== 200) {
        if (isset($data['score'])) {
            echo "Password score: {$data['score']} / 4\n";
            echo "Warning: " . ($data['feedback']['warning'] ?? '') . "\n";
            echo "Suggestions: " . implode(', ', $data['feedback']['suggestions'] ?? []) . "\n";
        } else {
            echo "Error: {$data['error']}\n";
        }
        return null;
    }

    if (isset($data['breachWarning'])) {
        echo "Warning: {$data['breachWarning']}\n";
    }

    return $data['user'];
}

API Reference Summary

EndpointMethodDescription
/api/auth/registerPOSTRegister a new account (validates password strength and checks breaches)
/api/auth/loginPOSTSign in (enforces lockout policy)
/api/auth/forgot-passwordPOSTRequest a password reset email
/api/auth/validate-reset-tokenPOSTCheck if a reset token is still valid
/api/auth/reset-passwordPOSTSet a new password using a valid reset token
/api/auth/verify-emailPOSTVerify an email address with a token
/api/auth/resend-verificationPOSTResend the email verification link