# Authentication System Architecture

**Last Updated:** December 2025
**Status:** Current State Documentation + Analysis Findings

---

## Overview

BuyerKiosk uses a **hybrid authentication system** combining:

- Session-based authentication (PHP `$_SESSION`)
- "Remember Me" persistent tokens (Birke\Rememberme)
- JWT tokens (partially implemented)
- API key authentication (for mobile/programmatic access)

---

## Authentication Methods

### 1. Session Authentication (Primary)

**Used by:** Web browser users

**Flow:**
1. User submits login form
2. System validates credentials
3. Session created with user object
4. Session cookie set in browser
5. Subsequent requests validated via session

**Key Files:**
- `userfrosting/src/BuyerKiosk/Core/Controllers/AccountController.php`
- `userfrosting/middleware/UserSession.php`

### 2. Remember Me Tokens

**Used by:** Users who check "Remember me" on login

**Flow:**
1. On login with "remember me" checked
2. System generates persistent token pair
3. Tokens stored in `uf_user_rememberme` table
4. Cookie set with token
5. On return visit, token validated and session restored

**Library:** `Birke\Rememberme\Authenticator`

**Database Table:**
```sql
CREATE TABLE uf_user_rememberme (
    user_id INT,
    token VARCHAR(255),
    persistent_token VARCHAR(255),
    expires TIMESTAMP
);
```

### 3. JWT Tokens (Incomplete)

**Status:** Partially implemented, not actively used

**Current Implementation:**
- JWT created on login ("pineapple" cookie)
- Token expires in 60 seconds (effectively useless)
- Token is **never validated** on subsequent requests

**Key Issue:** This appears to be abandoned/incomplete code.

### 4. API Key Authentication

**Used by:** Mobile apps, external integrations

**Flow:**
1. Client includes API key in header
2. System looks up key in `uf_apiKey_user`
3. If valid and active, request authenticated
4. Associated user loaded for authorization

**Headers Supported:**
```http
Authorization: ApiKey <key>
X-Api-Key: <key>
```

**Database Tables:**
```sql
-- API Keys
CREATE TABLE uf_apiKey (
    id INT PRIMARY KEY,
    key VARCHAR(255),
    active TINYINT(1)
);

-- User-Key Association
CREATE TABLE uf_apiKey_user (
    user_id INT,
    key_id INT
);
```

---

## Login Flow Details

### Entry Point

```
POST /account/login
Content-Type: application/x-www-form-urlencoded

user_name=jsmith&password=secret123&rememberme=1
```

### Validation Steps

```
┌─────────────────────────────────────────────────────────────────┐
│                      LOGIN FLOW                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. CSRF Validation (CURRENTLY DISABLED!)                       │
│     └── Should check csrf_token in POST data                    │
│                                                                 │
│  2. User Lookup                                                 │
│     ├── By username: SELECT * FROM uf_user WHERE user_name = ?  │
│     └── By email (if enabled): WHERE email = ?                  │
│                                                                 │
│  3. Account Status Check                                        │
│     ├── enabled == 1 (not disabled by admin)                   │
│     └── active == 1 (email verified)                           │
│                                                                 │
│  4. Password Verification                                       │
│     ├── Detect hash type by length (FRAGILE!)                  │
│     ├── SHA1 (65 chars): salt + sha1(salt + password)          │
│     ├── BCrypt: password_verify() or crypt()                   │
│     └── Upgrade hash if legacy format (BROKEN!)                │
│                                                                 │
│  5. Session Creation                                            │
│     ├── session_regenerate_id() (prevent fixation)             │
│     └── $_SESSION["userfrosting"]["user"] = $user              │
│                                                                 │
│  6. Remember Me (if checked)                                    │
│     └── $app->remember_me->createCookie($user->id)             │
│                                                                 │
│  7. JWT Cookie (always)                                         │
│     └── setcookie("pineapple", base64_encode($jwt))            │
│                                                                 │
│  8. Update Last Login                                           │
│     └── UPDATE uf_user SET last_sign_in_stamp = NOW()          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

### Code Location

**File:** `userfrosting/src/BuyerKiosk/Core/Controllers/AccountController.php`
**Method:** `login()` (lines 205-331)

---

## Password Hashing

### Supported Formats

The system supports three password hash formats for backward compatibility:

| Format | Detection | Algorithm | Security |
|--------|-----------|-----------|----------|
| SHA1 (Legacy) | 65 characters | salt + SHA1(salt + password) | **INSECURE** |
| Homegrown BCrypt | Other | crypt() with cost 12 | Adequate |
| Modern | Other | password_hash(PASSWORD_BCRYPT) | **Recommended** |

### Hash Type Detection

**File:** `userfrosting/auth/Authentication.php`

```php
public static function getPasswordHashType($password) {
    if (strlen($password) == 65) {
        return "sha1";
    }
    return "modern";
}
```

**Problem:** Detection is based solely on string length, which is fragile.

### Password Verification

**File:** `userfrosting/models/mysql/MySqlUser.php:357-379`

```php
public function verifyPassword($password) {
    if (Authentication::getPasswordHashType($this->password) == "sha1") {
        // Legacy SHA1 verification
        $salt = substr($this->password, 0, 25);
        if ($salt . sha1($salt . $password) == $this->password) {
            return true;
        }
    }

    // Modern verification
    return password_verify($password, $this->password);
}
```

### Password Hashing (New Passwords)

**File:** `userfrosting/auth/Authentication.php:17-19`

```php
public static function hashPassword($password) {
    return password_hash($password, PASSWORD_BCRYPT);
}
```

Uses BCrypt with default cost factor (10).

### Hash Upgrade on Login

**Intended Behavior:** Upgrade legacy hashes to modern format on successful login.

**File:** `userfrosting/models/mysql/MySqlUser.php:401-410`

```php
// BUG: Calls wrong method!
if (Authentication::getPasswordHashType($this->password) != "modern") {
    $password_hash = Authentication::getPasswordHashType($password);  // WRONG!
    // Should be: Authentication::hashPassword($password)
    if ($password_hash !== null) {
        $this->password = $password_hash;
    }
}
```

**Status:** This code is broken and passwords are never actually upgraded.

---

## Session Management

### Configuration

**File:** `userfrosting/config-userfrosting.php`

```php
ini_set('session.gc_maxlifetime', 60*60*24);  // 24 hours
session_cache_limiter(false);
session_name("UserFrosting");
```

### Session Middleware

**File:** `userfrosting/middleware/UserSession.php`

Runs before every request via Slim's `slim.before` hook:

1. Starts PHP session
2. Checks for existing user in session
3. Validates RememberMe cookie if present
4. Refreshes user data from database
5. Creates guest user if not authenticated

### Session Data Structure

```php
$_SESSION["userfrosting"]["user"] = MySqlUser object
$_SESSION["userfrosting"]["alerts"] = MessageStream object
```

---

## JWT Implementation Details

### Token Creation

**File:** `userfrosting/src/BuyerKiosk/Core/Controllers/AccountController.php:296-322`

```php
$tokenId = base64_encode(openssl_random_pseudo_bytes(128));
$issuedAt = time();
$notBefore = $issuedAt + 10;     // Valid 10 seconds from now
$expire = $notBefore + 60;        // Expires after 60 seconds total

$data = [
    'iat' => $issuedAt,
    'jti' => $tokenId,
    'iss' => $serverName,         // UNDEFINED VARIABLE!
    'nbf' => $notBefore,
    'exp' => $expire,
    'data' => ['userName' => $data['user_name']]
];

$jwt = \JWT::encode($data, $secretKey, 'HS256');
setcookie("pineapple", base64_encode($jwt), time()+259200, '/');
```

### Issues with Current JWT

| Issue | Impact |
|-------|--------|
| 60-second expiration | Token useless for API calls |
| Never validated | No code checks JWT on requests |
| `$serverName` undefined | PHP warning on every login |
| Double base64 encoding | Unnecessary, adds confusion |
| No HttpOnly flag | XSS can steal token |
| No Secure flag | Sent over HTTP |
| HS256 with shared secret | Less secure than RS256 |

---

## API Authentication Flow

### Endpoint

```
POST /api/authenticate-user
```

### Request

```json
{
    "userName": "jsmith",
    "password": "secret123",
    "typeNum": "ou00",
    "accessLevel": "uri_queue"
}
```

### Validation

**File:** `userfrosting/routes/api.php:85-147`

```php
// 1. HTTPS required
if (!isSecure()) {
    return error("HTTPS required");
}

// 2. User lookup
$user = UserLoader::fetch($userName, "user_name");

// 3. Password verification (modern only!)
if (!password_verify($password, $user->password)) {
    return error("Invalid credentials");
}

// 4. Store access check
if (!$user->checkStoreGroup($typeNum)) {
    return error("No store access");
}

// 5. Permission check
if (!$user->checkAccess($accessLevel)) {
    return error("Insufficient permissions");
}
```

### Response

```json
{
    "userId": 123,
    "error": false
}
```

**Note:** This endpoint does NOT return a token. Mobile apps use separate API key authentication.

---

## API Key Authentication

### Header Format

```http
Authorization: ApiKey abc123def456...
# or
X-Api-Key: abc123def456...
```

### Verification

**File:** `userfrosting/models/mysql/MySqlUser.php:381-395`

```php
public function verifyAPIKey($apiKey, \Klogger $log) {
    $stmt = $db->prepare("
        SELECT `key`, active
        FROM uf_apiKey_user
        JOIN uf_apiKey ON uf_apiKey_user.key_id = uf_apiKey.id
        WHERE uf_apiKey_user.user_id = :user_id
    ");

    // VULNERABILITY: Uses strcmp() - timing attack possible
    if (strcmp($row['key'], $apiKey) == 0 && (int)$row['active'] == 1) {
        return true;
    }
    return false;
}
```

### Security Issue

`strcmp()` is vulnerable to timing attacks. Should use `hash_equals()`:

```php
// INSECURE
if (strcmp($row['key'], $apiKey) == 0) { ... }

// SECURE
if (hash_equals($row['key'], $apiKey)) { ... }
```

---

## Password Reset Flow

### Request Reset

```
POST /account/forgot-password
{ "email": "user@example.com" }
```

**Flow:**
1. Look up user by email
2. Generate activation token
3. Store token in `uf_user.lost_password_request`
4. Store timestamp in `uf_user.lost_password_timestamp`
5. Send email with reset link

### Complete Reset

```
GET /account/reset-password/:token
POST /account/reset-password/:token
{ "password": "newpassword", "confirm": "newpassword" }
```

**Flow:**
1. Validate token matches stored value
2. Check timestamp not expired (no explicit timeout!)
3. Hash new password
4. Update user record
5. Clear reset token

---

## CSRF Protection

### Library

**File:** `userfrosting/lib/NoCSRF.php`

### Current Status

**CSRF is DISABLED on login form!**

**File:** `AccountController.php:217-221`

```php
// CSRF token check temporarily disabled
// if (!\NoCSRF::check('csrf_token', $this->_app->request->post())) {
//     $ms->addMessageTranslated("danger", "Invalid or missing CSRF token.");
//     $this->_app->halt(403);
// }
```

This is a **critical security vulnerability**.

---

## Key Configuration

### Session

| Setting | Value | File |
|---------|-------|------|
| `session.gc_maxlifetime` | 86400 (24h) | config-userfrosting.php |
| `session.name` | "UserFrosting" | config-userfrosting.php |
| `session.cookie_httponly` | Not set | - |
| `session.cookie_secure` | Not set | - |
| `session.cookie_samesite` | Not set | - |

### Remember Me

| Setting | Value |
|---------|-------|
| Token expiry | Database-defined |
| Cookie name | Configurable |
| Storage | `uf_user_rememberme` table |

### JWT

| Setting | Value |
|---------|-------|
| Algorithm | HS256 |
| Expiration | 60 seconds |
| Cookie name | "pineapple" |
| Cookie duration | 3 days |

---

## Code File Reference

### Core Authentication

| File | Purpose |
|------|---------|
| `userfrosting/src/BuyerKiosk/Core/Controllers/AccountController.php` | Login, logout, registration, password reset |
| `userfrosting/middleware/UserSession.php` | Session initialization and validation |
| `userfrosting/auth/Authentication.php` | Password hashing utilities |
| `userfrosting/models/mysql/MySqlUser.php` | User model with auth methods |
| `userfrosting/models/mysql/MySqlUserLoader.php` | User loading/factory |

### Configuration

| File | Purpose |
|------|---------|
| `userfrosting/config-userfrosting.php` | Session configuration |
| `userfrosting/initialize.php` | RememberMe setup |

### Libraries

| File | Purpose |
|------|---------|
| `userfrosting/lib/NoCSRF.php` | CSRF token generation/validation |
| `vendor/birke/rememberme/` | Remember Me token management |
| `vendor/firebase/php-jwt/` | JWT encoding |

---

## Related Documentation

- [User & Employee Architecture](./user-employee-architecture.md)
- [Unified Users Specification](../specs/unified-users-modern-auth-spec.md)
- [Security Vulnerabilities Report](./security-vulnerabilities.md)
