# Validation Report: URS-001

**Title:** Secure Login with Unique User Credentials
**Date:** 2026-04-21T05:55:47.219Z
**Duration:** 88.9s
**Overall Status:** ✅ PASS

## Environment

- **Inbox URL:** http://localhost:60677
- **Database:** localhost:60678/cc_repinbox_dev

## Setup

Status: ✅ PASS

## Test Steps

Each step below corresponds to one Playwright test that ran sequentially. Screenshots and video recordings provide visual evidence of the UI behaviour.

### 1. Step 1: Login page loads correctly — ✅ PASS

**What this step proves:**

The system presents a login form with email and password fields and a password reset link. This confirms the authentication entry point is functional and that all users are directed to credential-based login rather than any unauthenticated route.

**Screenshots:**

![step 01 login page](screenshots/step-01-login-page.png)

---

### 2. Step 2: Valid login - Distributor user — ✅ PASS

**What this step proves:**

A distributor user authenticates successfully with correct credentials. The system validates the credentials, creates a session, and redirects the user to the authenticated application. This demonstrates that valid credentials grant access as required by URS-001.

**Audit events generated by this step:**

| Time | Type | Action | User | Org | Performed |
|------|------|--------|------|-----|-----------|
| 2026-04-21 05:55:56Z | user_log | user:login | dan.distributor@stellartech.com | StellarTech Medical Solutions | — |
| 2026-04-21 05:56:03Z | user_log | user:login | mark.manufacturer@zurimed.com | ZuriMED | — |
| 2026-04-21 05:56:10Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — |
| 2026-04-21 05:56:36Z | user_log | user:login_deactivated | demo.user@zurimed.com | ZuriMED | — |
| 2026-04-21 05:56:53Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — |
| 2026-04-21 05:57:00Z | user_log | user:login | dan.distributor@stellartech.com | StellarTech Medical Solutions | — |
| 2026-04-21 05:57:01Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — |
| 2026-04-21 05:57:08Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — |

**Screenshots:**

![step 02 distributor logged in](screenshots/step-02-distributor-logged-in.png)

---

### 3. Step 3: Valid login - Manufacturer user — ✅ PASS

**What this step proves:**

A manufacturer-type user authenticates successfully, confirming that the secure credential flow handles multiple organization types uniformly. No special path exists for different org types.

**Audit events generated by this step:**

| Time | Type | Action | User | Org | Performed |
|------|------|--------|------|-----|-----------|
| 2026-04-21 05:55:56Z | user_log | user:login | dan.distributor@stellartech.com | StellarTech Medical Solutions | — |
| 2026-04-21 05:56:03Z | user_log | user:login | mark.manufacturer@zurimed.com | ZuriMED | — |
| 2026-04-21 05:56:10Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — |
| 2026-04-21 05:56:36Z | user_log | user:login_deactivated | demo.user@zurimed.com | ZuriMED | — |
| 2026-04-21 05:56:53Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — |
| 2026-04-21 05:57:00Z | user_log | user:login | dan.distributor@stellartech.com | StellarTech Medical Solutions | — |
| 2026-04-21 05:57:01Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — |
| 2026-04-21 05:57:08Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — |

**Screenshots:**

![step 03 manufacturer logged in](screenshots/step-03-manufacturer-logged-in.png)

---

### 4. Step 4: Valid login - Admin user — ✅ PASS

**What this step proves:**

An admin user authenticates successfully, confirming that all system roles use the same credential-based authentication mechanism.

**Audit events generated by this step:**

| Time | Type | Action | User | Org | Performed |
|------|------|--------|------|-----|-----------|
| 2026-04-21 05:55:56Z | user_log | user:login | dan.distributor@stellartech.com | StellarTech Medical Solutions | — |
| 2026-04-21 05:56:03Z | user_log | user:login | mark.manufacturer@zurimed.com | ZuriMED | — |
| 2026-04-21 05:56:10Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — |
| 2026-04-21 05:56:36Z | user_log | user:login_deactivated | demo.user@zurimed.com | ZuriMED | — |
| 2026-04-21 05:56:53Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — |
| 2026-04-21 05:57:00Z | user_log | user:login | dan.distributor@stellartech.com | StellarTech Medical Solutions | — |
| 2026-04-21 05:57:01Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — |
| 2026-04-21 05:57:08Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — |

**Screenshots:**

![step 04 admin logged in](screenshots/step-04-admin-logged-in.png)

---

### 5. Step 5: Invalid credentials - Wrong password — ✅ PASS

**What this step proves:**

Attempting to log in with a valid registered email but an incorrect password is rejected. The system returns a generic error message and keeps the user on the login page. The exact error text is captured for comparison in Step 6 to verify protection against email enumeration.

**Screenshots:**

![step 05 wrong password error](screenshots/step-05-wrong-password-error.png)

---

### 6. Step 6: Invalid credentials - Non-existent email — ✅ PASS

**What this step proves:**

Attempting to log in with an email address that has never been registered returns the exact same error message as Step 5. Identical responses for wrong-password and unknown-email requests prevent attackers from determining whether a given email address exists in the system.

**Screenshots:**

![step 06 nonexistent email error](screenshots/step-06-nonexistent-email-error.png)

---

### 7. Step 6b: Locked account - inactive user cannot login — ✅ PASS

**What this step proves:**

A user whose organization membership has been deactivated is blocked from logging in even when submitting the correct credentials. This confirms that deactivating a user account takes effect immediately and that credential validity alone is insufficient for access.

**Screenshots:**

![step 06b locked account error](screenshots/step-06b-locked-account-error.png)

---

### 8. Step 7: Empty field validation — ✅ PASS

**What this step proves:**

Submitting the login form with empty fields triggers validation errors before any server request is made, displaying field-level messages such as "Invalid email" and "Password is required". This confirms that required-field enforcement provides clear feedback and prevents malformed requests.

**Screenshots:**

![step 07 empty both fields](screenshots/step-07-empty-both-fields.png)

![step 07 empty password](screenshots/step-07-empty-password.png)

---

### 9. Step 8: Session security - Cookie verification — ✅ PASS

**What this step proves:**

After successful login the session cookie attributes are inspected. The httpOnly flag confirms that JavaScript cannot read the token, mitigating XSS-based session theft. The SameSite=Lax setting blocks cross-site request forgery. Cookie evidence is written to session-cookie-evidence.json.

**Screenshots:**

![step 08 session security](screenshots/step-08-session-security.png)

---

### 10. Step 9: Session isolation - Multiple users — ✅ PASS

**What this step proves:**

Two users log in simultaneously in separate browser contexts (equivalent to separate incognito windows) and each receives a distinct, non-overlapping session token. This confirms that sessions are fully isolated and one user's session cannot be used to access another user's account.

**Screenshots:**

![step 09 session a distributor](screenshots/step-09-session-a-distributor.png)

![step 09 session b admin](screenshots/step-09-session-b-admin.png)

---

### 11. Step 10: Logout and session termination — ✅ PASS

**What this step proves:**

A logged-in user opens the sidebar user menu and clicks Logout. The server invalidates the session, clears the session cookie, and redirects the browser. A subsequent attempt to access /inbox confirms that the session can no longer authenticate: the user is redirected to login.

**Screenshots:**

![step 10 before logout](screenshots/step-10-before-logout.png)

![step 10 after logout](screenshots/step-10-after-logout.png)

![step 10 protected route redirect](screenshots/step-10-protected-route-redirect.png)

---

## Database Validations

The following SQL queries ran against the application database after the Playwright scenarios completed. Each query asserts a specific condition that proves the feature under test persisted its data correctly.

### Demo users exist — ✅ PASS

**Assertion:** All 4 demo users should exist in the users table

```sql
SELECT id, email, name, created_at
      FROM users
      WHERE email IN (
        'alex.admin@zurimed.com',
        'mark.manufacturer@zurimed.com',
        'dan.distributor@stellartech.com',
        'demo.user@zurimed.com'
      )
      ORDER BY email
```

| id | email | name | created_at |
| --- | --- | --- | --- |
| f6a7b8c9-d0e1-2345-f123-456789012345 | alex.admin@zurimed.com | Alex Admin | Mon Apr 20 2026 22:36:14 GMT-0500 (Central Daylight Time) |
| c3d4e5f6-a7b8-9012-cdef-123456789012 | dan.distributor@stellartech.com | Dan Distributor | Mon Apr 20 2026 22:36:14 GMT-0500 (Central Daylight Time) |
| e5f6a7b8-c9d0-1234-ef12-345678901234 | demo.user@zurimed.com | Demo User | Mon Apr 20 2026 22:36:14 GMT-0500 (Central Daylight Time) |
| d4e5f6a7-b8c9-0123-def1-234567890123 | mark.manufacturer@zurimed.com | Mark Manufacturer | Mon Apr 20 2026 22:36:14 GMT-0500 (Central Daylight Time) |

### No duplicate emails — ✅ PASS

**Assertion:** No email address should appear more than once in the users table

```sql
SELECT email, COUNT(*) as count
      FROM users
      GROUP BY email
      HAVING COUNT(*) > 1
```

*No rows returned*

### Passwords are hashed — ✅ PASS

**Assertion:** All password hashes should use Argon2id algorithm (not plaintext)

```sql
SELECT id, email,
        LEFT(password_hash, 10) as hash_prefix,
        password_hash LIKE '$argon2id$%' as is_argon2
      FROM users
      WHERE email IN (
        'alex.admin@zurimed.com',
        'mark.manufacturer@zurimed.com',
        'dan.distributor@stellartech.com',
        'demo.user@zurimed.com'
      )
      ORDER BY email
```

| id | email | hash_prefix | is_argon2 |
| --- | --- | --- | --- |
| f6a7b8c9-d0e1-2345-f123-456789012345 | alex.admin@zurimed.com | $argon2id$ | true |
| c3d4e5f6-a7b8-9012-cdef-123456789012 | dan.distributor@stellartech.com | $argon2id$ | true |
| e5f6a7b8-c9d0-1234-ef12-345678901234 | demo.user@zurimed.com | $argon2id$ | true |
| d4e5f6a7-b8c9-0123-def1-234567890123 | mark.manufacturer@zurimed.com | $argon2id$ | true |

### Recent sessions created — ✅ PASS

**Assertion:** Sessions should have been created within the last 10 minutes for users who logged in during the test

```sql
SELECT s.id as session_id, u.email, s.created_at, s.expires_at
      FROM session s
      JOIN users u ON s.user_id = u.id
      WHERE u.email IN (
        'alex.admin@zurimed.com',
        'mark.manufacturer@zurimed.com',
        'dan.distributor@stellartech.com'
      )
      AND s.created_at > NOW() - INTERVAL '10 minutes'
      ORDER BY s.created_at DESC
      LIMIT 10
```

| session_id | email | created_at | expires_at |
| --- | --- | --- | --- |
| ef993f23ccc4fc7388ed5fdb8d9a240be989be2e51c672ac3f91905f2f2ad664 | alex.admin@zurimed.com | Tue Apr 21 2026 00:57:01 GMT-0500 (Central Daylight Time) | Thu May 21 2026 00:57:01 GMT-0500 (Central Daylight Time) |
| 5fdd8f4dd4559e6c32352e841d56d505328018441bf1fed8497d0c851ab75b9a | dan.distributor@stellartech.com | Tue Apr 21 2026 00:57:00 GMT-0500 (Central Daylight Time) | Thu May 21 2026 00:57:00 GMT-0500 (Central Daylight Time) |
| fddfaf9216b071eaf03f800640dc181098c6d6ae0b5c19b2abf468e7e84db816 | alex.admin@zurimed.com | Tue Apr 21 2026 00:56:53 GMT-0500 (Central Daylight Time) | Thu May 21 2026 00:56:53 GMT-0500 (Central Daylight Time) |
| c3364fd44f7445ac9ebbe3384e78e1f766b96a90e8b287a15859c17d6b1d10d5 | alex.admin@zurimed.com | Tue Apr 21 2026 00:56:10 GMT-0500 (Central Daylight Time) | Thu May 21 2026 00:56:10 GMT-0500 (Central Daylight Time) |
| de32ec1c03bca6eae56a28decca6ed88a8ab083479d45246d2d6997542f26ca8 | mark.manufacturer@zurimed.com | Tue Apr 21 2026 00:56:03 GMT-0500 (Central Daylight Time) | Thu May 21 2026 00:56:03 GMT-0500 (Central Daylight Time) |
| f020a4191c6ed13273bd9d24df24065f86b1433d8ef736f57806fe99791e1f30 | dan.distributor@stellartech.com | Tue Apr 21 2026 00:55:56 GMT-0500 (Central Daylight Time) | Thu May 21 2026 00:55:56 GMT-0500 (Central Daylight Time) |

### Locked user is inactive — ✅ PASS

**Assertion:** demo.user@zurimed.com should have inactive org membership (locked account)

```sql
SELECT u.email, om.active, om.organization_id
      FROM organization_members om
      JOIN users u ON om.user_id = u.id
      WHERE u.email = 'demo.user@zurimed.com'
```

| email | active | organization_id |
| --- | --- | --- |
| demo.user@zurimed.com | false | a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
| demo.user@zurimed.com | false | b2c3d4e5-f6a7-8901-bcde-f12345678901 |
| demo.user@zurimed.com | false | 6763fc17-7da1-47e3-851e-8f4fac570dc6 |

### Global email uniqueness — ✅ PASS

**Assertion:** Every user should have a unique email address

```sql
SELECT COUNT(DISTINCT email) as unique_emails, COUNT(*) as total_users
      FROM users
      WHERE email IS NOT NULL
```

| unique_emails | total_users |
| --- | --- |
| 12 | 12 |

## Audit Log Events

Every row written to `audit_events` while this test was running (scoped to the demo organizations). Provides compliance evidence that user actions are traced end-to-end (URS-003).

**Capture window start:** 2026-04-21T05:55:45.235Z

<details><summary>Query used to capture events</summary>

```sql
SELECT
    ae.created_at,
    ae.event_type,
    ae.action,
    ae.user_id,
    u.email AS user_email,
    ae.organization_id,
    o.name AS organization_name,
    ae.object_id,
    ae.secondary_object_id,
    ae.payload,
    ae.route,
    ae.trace_id
  FROM audit_events ae
  LEFT JOIN users u ON u.id = ae.user_id
  LEFT JOIN organizations o ON o.id = ae.organization_id
  WHERE ae.created_at >= $1
    AND ae.organization_id = ANY($2::uuid[])
  ORDER BY ae.created_at ASC
```
</details>

8 event(s) captured:

| Time | Type | Action | User | Org | Object ID | Performed | Reason |
|------|------|--------|------|-----|-----------|-----------|--------|
| 2026-04-21 05:55:56Z | user_log | user:login | dan.distributor@stellartech.com | StellarTech Medical Solutions | — | — |  |
| 2026-04-21 05:56:03Z | user_log | user:login | mark.manufacturer@zurimed.com | ZuriMED | — | — |  |
| 2026-04-21 05:56:10Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — | — |  |
| 2026-04-21 05:56:36Z | user_log | user:login_deactivated | demo.user@zurimed.com | ZuriMED | — | — |  |
| 2026-04-21 05:56:53Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — | — |  |
| 2026-04-21 05:57:00Z | user_log | user:login | dan.distributor@stellartech.com | StellarTech Medical Solutions | — | — |  |
| 2026-04-21 05:57:01Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — | — |  |
| 2026-04-21 05:57:08Z | user_log | user:login | alex.admin@zurimed.com | ZuriMED | — | — |  |

## Video Evidence

Screencast recordings capture the full browser session for each test step, including annotated chapter overlays that identify what is being tested.

- [videos/step-01-login-page.webm](videos/step-01-login-page.webm)
- [videos/step-02-valid-login-distributor.webm](videos/step-02-valid-login-distributor.webm)
- [videos/step-03-valid-login-manufacturer.webm](videos/step-03-valid-login-manufacturer.webm)
- [videos/step-04-valid-login-admin.webm](videos/step-04-valid-login-admin.webm)
- [videos/step-05-wrong-password.webm](videos/step-05-wrong-password.webm)
- [videos/step-06-nonexistent-email.webm](videos/step-06-nonexistent-email.webm)
- [videos/step-06b-locked-account.webm](videos/step-06b-locked-account.webm)
- [videos/step-07-empty-fields.webm](videos/step-07-empty-fields.webm)
- [videos/step-08-session-security.webm](videos/step-08-session-security.webm)
- [videos/step-09-session-isolation-a.webm](videos/step-09-session-isolation-a.webm)
- [videos/step-10-logout.webm](videos/step-10-logout.webm)
