{
  "feature": "login",
  "version": "1.0.0",
  "description": "Authenticate a user with email and password",
  "category": "auth",
  "tags": [
    "authentication",
    "session",
    "security",
    "identity",
    "saas"
  ],
  "aliases": [
    "sign-in",
    "signin",
    "sign in",
    "log-in",
    "log in",
    "authenticate",
    "user-authentication",
    "email-password-login",
    "credential-login",
    "password-login"
  ],
  "uses": [
    "ui-design-system"
  ],
  "api": {
    "http": {
      "method": "POST",
      "path": "/auth/login"
    },
    "request": {
      "content_type": "application/json",
      "schema": {
        "type": "object",
        "required": [
          "email",
          "password"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 255
          },
          "password": {
            "type": "string",
            "minLength": 8,
            "maxLength": 64
          },
          "remember_me": {
            "type": "boolean",
            "default": false
          }
        }
      }
    },
    "response": {
      "success": {
        "status": 200,
        "content_type": "application/json",
        "schema": {
          "type": "object",
          "required": [
            "access_token",
            "refresh_token",
            "token_type",
            "expires_in",
            "user"
          ],
          "properties": {
            "access_token": {
              "type": "string",
              "description": "JWT, 15-minute TTL"
            },
            "refresh_token": {
              "type": "string",
              "description": "Opaque token, 7-day TTL (30-day if remember_me)"
            },
            "token_type": {
              "type": "string",
              "enum": [
                "Bearer"
              ]
            },
            "expires_in": {
              "type": "number",
              "description": "Access token TTL in seconds"
            },
            "user": {
              "type": "object",
              "required": [
                "id",
                "email"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "email": {
                  "type": "string",
                  "format": "email"
                }
              }
            }
          }
        }
      },
      "errors": [
        {
          "status": 401,
          "error_code": "LOGIN_INVALID_CREDENTIALS"
        },
        {
          "status": 403,
          "error_code": "LOGIN_EMAIL_NOT_VERIFIED"
        },
        {
          "status": 403,
          "error_code": "LOGIN_ACCOUNT_DISABLED"
        },
        {
          "status": 422,
          "error_code": "LOGIN_VALIDATION_ERROR"
        },
        {
          "status": 423,
          "error_code": "LOGIN_ACCOUNT_LOCKED"
        },
        {
          "status": 429,
          "error_code": "LOGIN_RATE_LIMITED"
        }
      ]
    }
  },
  "anti_patterns": [
    {
      "rule": "do not create /signin, /sign-in, /authenticate, or any alternate path — /auth/login is canonical",
      "why": "Duplicate endpoints fragment the auth surface, break session/token assumptions, and split monitoring. Aliases route to one path."
    },
    {
      "rule": "do not return distinct messages, status codes, or response times for 'user not found' vs 'wrong password'",
      "why": "Any difference enables user enumeration (OWASP ASVS 2.2.1). Both must produce identical 401 + LOGIN_INVALID_CREDENTIALS + similar latency."
    },
    {
      "rule": "do not use ==, ===, or string equality for password comparison",
      "why": "Plain comparison leaks information through timing. Use bcrypt.compare() / argon2.verify() or the language's constant-time primitive."
    },
    {
      "rule": "do not return password_hash, salt, refresh_token rotation history, or any credential material in the response body or logs",
      "why": "Credentials must never leave the auth boundary. Hash exposure enables offline brute force; rotation history enables replay."
    },
    {
      "rule": "do not skip the rate_limited or account_locked outcomes, and do not reorder them — rate_limited (priority 1) and account_locked (priority 2) MUST be checked before password comparison",
      "why": "Ordering is a security guarantee, not an optimization. Checking password first leaks DB lookup timing and burns CPU on attackers."
    },
    {
      "rule": "do not log the password field, even on validation error or in stack traces",
      "why": "POPIA / GDPR / OWASP — credentials in logs are a breach trigger. Sanitize request bodies before any logger sees them."
    },
    {
      "rule": "do not emit a single login.success event without first resetting failed_login_attempts to 0",
      "why": "Stale counters cause false lockouts on the next failed attempt — a surprising support burden traceable to this exact omission."
    },
    {
      "rule": "do not invent additional fields in the response (display_name, avatar_url, roles, etc.) — fetch those from a separate /me endpoint",
      "why": "Coupling profile data to login forces the auth path to query unrelated tables, increases attack surface, and breaks token rotation independence."
    },
    {
      "rule": "do not implement 'remember me' as a longer access_token TTL — extend the refresh_token TTL instead",
      "why": "Long-lived access tokens cannot be revoked between rotations. Refresh tokens can be invalidated server-side on logout/compromise."
    }
  ],
  "fields": [
    {
      "name": "email",
      "type": "email",
      "required": true,
      "label": "Email Address",
      "placeholder": "you@example.com",
      "sensitive": false,
      "validation": [
        {
          "type": "required",
          "message": "Email is required"
        },
        {
          "type": "email",
          "message": "Please enter a valid email address"
        },
        {
          "type": "maxLength",
          "value": 255,
          "message": "Email is too long"
        }
      ]
    },
    {
      "name": "password",
      "type": "password",
      "required": true,
      "label": "Password",
      "sensitive": true,
      "validation": [
        {
          "type": "required",
          "message": "Password is required"
        },
        {
          "type": "minLength",
          "value": 8,
          "message": "Password must be at least 8 characters"
        },
        {
          "type": "maxLength",
          "value": 64,
          "message": "Password must be less than 64 characters"
        }
      ]
    },
    {
      "name": "remember_me",
      "type": "boolean",
      "required": false,
      "label": "Remember me",
      "default": false
    }
  ],
  "rules": {
    "security": {
      "max_attempts": 5,
      "lockout_duration_minutes": 15,
      "lockout_scope": "per_email",
      "rate_limit": {
        "window_seconds": 60,
        "max_requests": 10,
        "scope": "per_ip"
      },
      "password_comparison": {
        "constant_time": true
      },
      "credential_error_handling": {
        "generic_message": true
      }
    },
    "session": {
      "type": "jwt",
      "access_token": {
        "expiry_minutes": 15
      },
      "refresh_token": {
        "expiry_days": 7,
        "rotate_on_use": true
      },
      "remember_me_expiry_days": 30,
      "extend_on_activity": true,
      "secure_flags": {
        "http_only": true,
        "secure": true,
        "same_site": "strict"
      }
    },
    "email": {
      "require_verified": true,
      "case_sensitive": false,
      "trim_whitespace": true
    }
  },
  "outcomes": {
    "rate_limited": {
      "priority": 1,
      "error": "LOGIN_RATE_LIMITED",
      "description": "Defends against credential-stuffing and brute-force probes by short-circuiting before any DB lookup, so attackers cannot use response timing to enumerate accounts.",
      "given": [
        {
          "field": "request_count",
          "source": "computed",
          "operator": "gt",
          "value": 10,
          "description": "More than 10 requests in 60 seconds from this IP"
        }
      ],
      "result": "show \"Too many login attempts. Please wait a moment.\""
    },
    "account_locked": {
      "priority": 2,
      "error": "LOGIN_ACCOUNT_LOCKED",
      "description": "Stops repeated failed-login attempts on a single account. Distinct from rate_limited: this is per-account state (sticky), not per-IP rate (rolling).",
      "given": [
        {
          "field": "failed_login_attempts",
          "source": "db",
          "operator": "gte",
          "value": 5,
          "description": "Max attempts exceeded"
        },
        {
          "field": "locked_until",
          "source": "db",
          "operator": "gt",
          "value": "now",
          "description": "Lockout period has not expired"
        }
      ],
      "then": [
        {
          "action": "emit_event",
          "event": "login.locked",
          "payload": [
            "email",
            "user_id",
            "timestamp",
            "lockout_until",
            "attempt_count"
          ]
        }
      ],
      "result": "show \"Account temporarily locked. Please try again later.\""
    },
    "account_disabled": {
      "priority": 3,
      "error": "LOGIN_ACCOUNT_DISABLED",
      "description": "Honors admin/user deactivation as an explicit, non-recoverable state. Checked before credentials so a disabled user can never observe a 'wrong password' response.",
      "given": [
        {
          "field": "status",
          "source": "db",
          "operator": "eq",
          "value": "disabled",
          "description": "Account deactivated by admin or user"
        }
      ],
      "result": "show \"This account has been disabled. Please contact support.\""
    },
    "invalid_credentials": {
      "priority": 4,
      "error": "LOGIN_INVALID_CREDENTIALS",
      "transaction": true,
      "description": "Collapses 'user not found' and 'wrong password' into one indistinguishable response. Identical message + status + similar latency are mandatory to prevent user enumeration (OWASP ASVS 2.2.1).",
      "given": [
        {
          "any": [
            {
              "field": "user",
              "source": "db",
              "operator": "not_exists",
              "description": "User not found in database"
            },
            {
              "field": "password",
              "source": "input",
              "operator": "neq",
              "value": "stored_hash",
              "description": "Password does not match (constant-time comparison)"
            }
          ]
        }
      ],
      "then": [
        {
          "action": "set_field",
          "target": "failed_login_attempts",
          "value": "increment",
          "description": "Increment failed attempt counter"
        },
        {
          "action": "emit_event",
          "event": "login.failed",
          "payload": [
            "email",
            "timestamp",
            "ip_address",
            "user_agent",
            "attempt_count",
            "reason"
          ]
        },
        {
          "action": "set_field",
          "target": "locked_until",
          "value": "now + 15m",
          "description": "Lock account if attempts reach 5",
          "when": "failed_login_attempts >= 5"
        },
        {
          "action": "emit_event",
          "event": "login.locked",
          "payload": [
            "email",
            "user_id",
            "timestamp",
            "lockout_until",
            "attempt_count"
          ],
          "when": "failed_login_attempts >= 5"
        }
      ],
      "result": "show \"Invalid email or password\" (SAME message for both cases — enumeration prevention)"
    },
    "email_not_verified": {
      "priority": 5,
      "error": "LOGIN_EMAIL_NOT_VERIFIED",
      "description": "Blocks session creation until the user proves email ownership. Prevents unverified accounts from using protected features or receiving sensitive notifications.",
      "given": [
        {
          "field": "email_verified",
          "source": "db",
          "operator": "eq",
          "value": false,
          "description": "User exists, password correct, but email not verified"
        }
      ],
      "then": [
        {
          "action": "emit_event",
          "event": "login.unverified",
          "payload": [
            "user_id",
            "email",
            "timestamp"
          ]
        }
      ],
      "result": "redirect to /verify-email with message \"Please verify your email before logging in\""
    },
    "successful_login": {
      "priority": 10,
      "transaction": true,
      "description": "The only path that creates a session. Atomic so the counter reset, session issuance, and success event all persist together — a half-succeeded login must roll back to avoid stale lockouts or orphaned sessions.",
      "given": [
        {
          "field": "email",
          "source": "input",
          "operator": "matches",
          "value": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
          "description": "Email is valid format"
        },
        {
          "field": "user",
          "source": "db",
          "operator": "exists",
          "description": "User found in database (after lowercase + trim normalization)"
        },
        {
          "field": "password",
          "source": "input",
          "operator": "eq",
          "value": "stored_hash",
          "description": "Password matches stored hash (constant-time via bcrypt)"
        },
        {
          "field": "status",
          "source": "db",
          "operator": "neq",
          "value": "disabled"
        },
        {
          "field": "email_verified",
          "source": "db",
          "operator": "eq",
          "value": true
        }
      ],
      "then": [
        {
          "action": "set_field",
          "target": "failed_login_attempts",
          "value": 0,
          "description": "Reset attempt counter on success"
        },
        {
          "action": "create_record",
          "type": "session",
          "target": "session",
          "description": "Create JWT access token (15-min) + refresh token (7-day, or 30-day if remember_me)"
        },
        {
          "action": "emit_event",
          "event": "login.success",
          "payload": [
            "user_id",
            "email",
            "timestamp",
            "ip_address",
            "user_agent",
            "session_id"
          ]
        }
      ],
      "result": "redirect to /dashboard"
    }
  },
  "errors": [
    {
      "code": "LOGIN_INVALID_CREDENTIALS",
      "status": 401,
      "message": "Invalid email or password",
      "retry": true
    },
    {
      "code": "LOGIN_ACCOUNT_LOCKED",
      "status": 423,
      "message": "Account temporarily locked. Please try again later.",
      "retry": false
    },
    {
      "code": "LOGIN_EMAIL_NOT_VERIFIED",
      "status": 403,
      "message": "Please verify your email address to continue",
      "retry": false,
      "redirect": "email-verification"
    },
    {
      "code": "LOGIN_ACCOUNT_DISABLED",
      "status": 403,
      "message": "This account has been disabled. Please contact support.",
      "retry": false
    },
    {
      "code": "LOGIN_RATE_LIMITED",
      "status": 429,
      "message": "Too many login attempts. Please wait a moment.",
      "retry": true
    },
    {
      "code": "LOGIN_VALIDATION_ERROR",
      "status": 422,
      "message": "Please check your input and try again",
      "retry": true
    }
  ],
  "events": [
    {
      "name": "login.success",
      "description": "User successfully authenticated",
      "payload": [
        "user_id",
        "email",
        "timestamp",
        "ip_address",
        "user_agent",
        "session_id"
      ]
    },
    {
      "name": "login.failed",
      "description": "Authentication attempt failed",
      "payload": [
        "email",
        "timestamp",
        "ip_address",
        "user_agent",
        "attempt_count",
        "reason"
      ]
    },
    {
      "name": "login.locked",
      "description": "Account locked due to too many failures",
      "payload": [
        "email",
        "user_id",
        "timestamp",
        "lockout_until",
        "attempt_count"
      ]
    },
    {
      "name": "login.unverified",
      "description": "Login blocked — email not verified",
      "payload": [
        "user_id",
        "email",
        "timestamp"
      ]
    }
  ],
  "related": [
    {
      "feature": "signup",
      "type": "required",
      "reason": "User must exist before they can log in",
      "ui_link": "Don't have an account? Sign up",
      "ui_link_position": "below_form"
    },
    {
      "feature": "password-reset",
      "type": "recommended",
      "reason": "Users will forget passwords",
      "ui_link": "Forgot password?",
      "ui_link_position": "below_password_field"
    },
    {
      "feature": "email-verification",
      "type": "recommended",
      "reason": "Required when rules.email.require_verified is true"
    },
    {
      "feature": "logout",
      "type": "required",
      "reason": "Every login needs a logout"
    },
    {
      "feature": "biometric-auth",
      "type": "optional",
      "reason": "Palm vein scan as an alternative to password login",
      "ui_link": "Log in with palm vein"
    }
  ],
  "agi": {
    "goals": [
      {
        "id": "secure_authentication",
        "description": "Authenticate users securely while preventing credential-based attacks",
        "success_metrics": [
          {
            "metric": "unauthorized_access_rate",
            "target": "0%",
            "measurement": "Successful logins with invalid credentials / total login attempts"
          },
          {
            "metric": "legitimate_user_friction",
            "target": "< 2% lockout rate for valid users",
            "measurement": "Valid users locked out / total valid users attempting login"
          }
        ],
        "constraints": [
          {
            "type": "security",
            "description": "OWASP ASVS Level 2 compliance required",
            "negotiable": false
          },
          {
            "type": "performance",
            "description": "Login response time under 500ms p95",
            "negotiable": true
          }
        ]
      }
    ],
    "autonomy": {
      "level": "supervised",
      "human_checkpoints": [
        "before disabling an account permanently",
        "before changing lockout policy thresholds"
      ],
      "escalation_triggers": [
        "failed_login_attempts > 100",
        "lockout_rate > 5"
      ]
    },
    "verification": {
      "invariants": [
        "no plaintext passwords stored or logged",
        "error messages never reveal whether email exists",
        "all password comparisons use constant-time algorithm",
        "rate limiting checked before any database lookup"
      ],
      "acceptance_tests": [
        {
          "scenario": "brute force prevention",
          "given": "5 failed login attempts for the same email",
          "when": "6th attempt is made",
          "expect": "account locked for 15 minutes, LOGIN_ACCOUNT_LOCKED returned"
        },
        {
          "scenario": "credential stuffing defense",
          "given": "10 requests in 60 seconds from same IP",
          "when": "11th request arrives",
          "expect": "rate limited with 429 status"
        },
        {
          "scenario": "enumeration prevention",
          "given": "login attempted with non-existent email",
          "when": "response is returned",
          "expect": "same error message and similar response time as wrong password"
        },
        {
          "scenario": "successful login resets counters",
          "given": "user has 3 failed attempts",
          "when": "valid credentials submitted",
          "expect": "failed_login_attempts reset to 0, session created"
        }
      ],
      "monitoring": [
        {
          "metric": "error_rate",
          "threshold": "< 0.1%",
          "action": "alert and investigate"
        },
        {
          "metric": "login_latency_p95",
          "threshold": "< 500ms",
          "action": "scale infrastructure"
        },
        {
          "metric": "lockout_rate",
          "threshold": "< 2%",
          "action": "review lockout thresholds"
        }
      ]
    },
    "capabilities": [
      {
        "id": "credential_auth",
        "description": "Authenticate via email and password"
      },
      {
        "id": "session_creation",
        "description": "Create JWT access and refresh tokens",
        "requires": [
          "credential_auth"
        ]
      },
      {
        "id": "lockout_protection",
        "description": "Lock accounts after repeated failures"
      },
      {
        "id": "rate_limiting",
        "description": "Throttle login attempts per IP"
      }
    ],
    "boundaries": [
      "authentication must complete before session creation",
      "rate limiting must be checked before any database lookup",
      "audit trail of login attempts cannot be disabled",
      "password comparison must use constant-time algorithm"
    ],
    "tradeoffs": [
      {
        "prefer": "security",
        "over": "performance",
        "reason": "constant-time comparison adds latency but prevents timing attacks"
      },
      {
        "prefer": "user_privacy",
        "over": "debugging",
        "reason": "generic error messages make debugging harder but prevent enumeration"
      }
    ],
    "evolution": {
      "triggers": [
        {
          "condition": "failed_login_attempts > 1000",
          "action": "add CAPTCHA challenge before password check"
        },
        {
          "condition": "login_latency_p95 > 500",
          "action": "add connection pooling or read replicas"
        },
        {
          "condition": "lockout_rate > 5",
          "action": "implement progressive delays instead of hard lockout"
        }
      ],
      "deprecation": [
        {
          "field": "remember_me",
          "remove_after": "2027-01-01",
          "migration": "use refresh token rotation with configurable TTL instead"
        }
      ]
    }
  },
  "ui_hints": {
    "layout": "single_column_centered",
    "max_width": "420px",
    "show_logo": true,
    "fields_order": [
      "email",
      "password",
      "remember_me"
    ],
    "actions": {
      "primary": {
        "label": "Sign in",
        "type": "submit",
        "full_width": true
      }
    },
    "links": [
      {
        "label": "Forgot password?",
        "target": "password-reset",
        "position": "below_password_field"
      },
      {
        "label": "Don't have an account? Sign up",
        "target": "signup",
        "position": "below_form"
      }
    ],
    "accessibility": {
      "autofocus": "email",
      "autocomplete": {
        "email": "username",
        "password": "current-password"
      },
      "aria_live_region": true
    },
    "loading": {
      "disable_button": true,
      "show_spinner": true,
      "prevent_double_submit": true
    }
  },
  "extensions": {
    "nextjs": {
      "route": "/login",
      "layout": "(auth)",
      "server_action": true,
      "middleware_redirect": "/dashboard"
    },
    "express": {
      "route": "/api/auth/login",
      "middleware": [
        "rate-limit",
        "cors"
      ]
    },
    "laravel": {
      "guard": "web",
      "middleware": [
        "guest"
      ],
      "throttle": "5,1"
    }
  }
}