Login, sessions, password — M00 Auth
Authentication foundation. Email + password login. Bcrypt password hashing. Session regeneration on login + privilege change. Active-sessions screen. Forgot-password flow. Failed-login rate limiting. Audit log of every auth event.
The login screen
URL: /login. Fields:
- Email — must match
users.email(case-insensitive) - Password — verified against bcrypt hash in
users.password - Remember me — extends session cookie to 30 days (default 8 hours)
- CSRF token — auto-included; rejects POSTs without valid token
On success: session ID regenerated (per session_regenerate_id(true)), users.last_login_at + last_login_ip stamped, redirect to dashboard. Audit log entry: m00.login.success.
Failed login
- Generic error "Invalid credentials" — never reveals whether email exists
- Stamps
users.failed_login_count+users.last_failed_login_at - After 5 failed attempts within 15 min → account auto-locked for 30 min
- Audit log entry:
m00.login.fail - Super_admin can manually unlock via Users → unlock action
Forgot password
Click "Forgot password" on login screen
Enter email
System always responds "if the email exists, a reset link has been sent" — never reveals existence.
If email exists
Generates 64-char random token + 1-hour expiry. Stores in
password_resets. M17 emails the link.Click link → set new password
Token validated. Password bcrypt-hashed + saved. Token deleted. Audit log + automatic logout of all sessions.
Sessions
| Aspect | Behaviour |
|---|---|
| Storage | PHP session files in storage/sessions/ (gitignored) |
| Default TTL | 8 hours of inactivity |
| Remember-me TTL | 30 days |
| Cookie name | ACWMS_SESS (HttpOnly, Secure in production, SameSite=Strict) |
| Regenerated on | login + privilege change + sensitive action (e.g. password change) |
| CSRF token | Per-session, rotated on regeneration; required on every POST |
Active sessions screen
URL: /my-profile/sessions. Lists every active session for the logged-in user:
- Session ID (last 8 chars)
- Login timestamp
- IP address
- User agent (parsed: device + browser)
- Last activity
- "This device" badge on current session
- Revoke action per row (kills that session)
Useful when a user suspects their account was used elsewhere — kill all other sessions in one click.
Change password
My Profile → Change password. Form: current password (verified) · new password · confirm new password. Validation:
- Min 12 characters
- Must differ from current
- Must differ from last 5 passwords (history check)
- Bcrypt rehash on save
- All other sessions invalidated (security best-practice)
- Audit log entry
Logout
Header → user dropdown → Sign out. POST to /logout with CSRF. Session destroyed server-side + cookie cleared. Audit log entry. Redirect to /login.
The user dropdown menu
Top-right of every page once logged in:
- Identity block — avatar (initials or photo) + name + role badge
- Session meta — signed-in timestamp + live JS session timer
- My profile · Change password · Notification preferences · Active sessions
- System settings (gated on
m18.view) - Sign out (red, inside CSRF-bearing POST form)
Don't share login credentials. Every action is stamped with the logged-in user's ID — sharing means audit trail breaks. If a junior needs partner-level access for a specific task, give them temporary elevated permission via Roles & Permissions, not a borrowed login.
"correct-horse-battery-staple" beats "P@ssw0rd123!" for both memorability + entropy. The 12-char minimum supports passphrase use. Encourage staff to memorise long passphrases.