fix(auth): close legacy PSA auth path security gaps #201

Merged
David merged 2 commits from fix/pms-190-legacy-auth-security-gaps into main 2026-06-13 23:09:08 +02:00
Owner

Harden the legacy HS256 / OAuth auth surface against eight audit findings:

  • Google login (login_with_google) now enforces the same mfa_enabled gate as password login(), returning mfa_required with empty tokens instead of silently bypassing a user's second factor.
  • The HS256 Bearer path in auth middleware only accepts claims.typ == "access", so a typ:"refresh" token can no longer be replayed as an access credential (decode_token runs Validation::default() and does not assert typ).
  • Bunyip JIT provisioning persists the IdP email only when email_verified is true; an unverified address falls back to the sub placeholder so upsert_user_from_oidc cannot stamp email_verified_at = NOW() on an unverified email (auto-link/capture vector). Unverified users still self-signup into their own isolated personal tenant (PMS-248 contract preserved).
  • upsert_user_from_oidc scopes its ON CONFLICT (id) DO UPDATE with WHERE users.tenant_id = EXCLUDED.tenant_id, so a PK collision against a row in another tenant cannot cross-tenant overwrite (the scoped get_user_by_id then fails closed).
  • create_session stores a SHA-256 hex digest in user_sessions.token_hash instead of the raw token, so the column never holds a plaintext secret.
  • cookie_secure defaults to true everywhere except dev/test (is_dev_or_test), so a misconfigured ENVIRONMENT fails safe and the OAuth state cookie is not exposed over plain HTTP.
  • parse_mokosh_role maps super_admin to UserRole::SuperAdmin instead of silently downgrading it to Technician.
  • The OIDC RS JWKS cache coalesces concurrent refreshes via a refresh Mutex with double-checked staleness, preventing a thundering-herd of network fetches at cache expiry or on a kid miss.

#PMS-190

Harden the legacy HS256 / OAuth auth surface against eight audit findings: - Google login (login_with_google) now enforces the same mfa_enabled gate as password login(), returning mfa_required with empty tokens instead of silently bypassing a user's second factor. - The HS256 Bearer path in auth middleware only accepts claims.typ == "access", so a typ:"refresh" token can no longer be replayed as an access credential (decode_token runs Validation::default() and does not assert typ). - Bunyip JIT provisioning persists the IdP email only when email_verified is true; an unverified address falls back to the sub placeholder so upsert_user_from_oidc cannot stamp email_verified_at = NOW() on an unverified email (auto-link/capture vector). Unverified users still self-signup into their own isolated personal tenant (PMS-248 contract preserved). - upsert_user_from_oidc scopes its ON CONFLICT (id) DO UPDATE with WHERE users.tenant_id = EXCLUDED.tenant_id, so a PK collision against a row in another tenant cannot cross-tenant overwrite (the scoped get_user_by_id then fails closed). - create_session stores a SHA-256 hex digest in user_sessions.token_hash instead of the raw token, so the column never holds a plaintext secret. - cookie_secure defaults to true everywhere except dev/test (is_dev_or_test), so a misconfigured ENVIRONMENT fails safe and the OAuth state cookie is not exposed over plain HTTP. - parse_mokosh_role maps super_admin to UserRole::SuperAdmin instead of silently downgrading it to Technician. - The OIDC RS JWKS cache coalesces concurrent refreshes via a refresh Mutex with double-checked staleness, preventing a thundering-herd of network fetches at cache expiry or on a kid miss. #PMS-190
fix(auth): close legacy PSA auth path security gaps
Some checks failed
E2E (staging) / Playwright against staging (pull_request) Failing after 28s
Check / fmt + clippy + compile + tests (pull_request) Successful in 2m34s
Build OCI container / Build and push mokosh-api image (push) Successful in 3m48s
ffb58e17bd
Harden the legacy HS256 / OAuth auth surface against eight audit findings:

- Google login (login_with_google) now enforces the same mfa_enabled gate as password login(), returning mfa_required with empty tokens instead of silently bypassing a user's second factor.
- The HS256 Bearer path in auth middleware only accepts claims.typ == "access", so a typ:"refresh" token can no longer be replayed as an access credential (decode_token runs Validation::default() and does not assert typ).
- Bunyip JIT provisioning persists the IdP email only when email_verified is true; an unverified address falls back to the sub placeholder so upsert_user_from_oidc cannot stamp email_verified_at = NOW() on an unverified email (auto-link/capture vector). Unverified users still self-signup into their own isolated personal tenant (PMS-248 contract preserved).
- upsert_user_from_oidc scopes its ON CONFLICT (id) DO UPDATE with WHERE users.tenant_id = EXCLUDED.tenant_id, so a PK collision against a row in another tenant cannot cross-tenant overwrite (the scoped get_user_by_id then fails closed).
- create_session stores a SHA-256 hex digest in user_sessions.token_hash instead of the raw token, so the column never holds a plaintext secret.
- cookie_secure defaults to true everywhere except dev/test (is_dev_or_test), so a misconfigured ENVIRONMENT fails safe and the OAuth state cookie is not exposed over plain HTTP.
- parse_mokosh_role maps super_admin to UserRole::SuperAdmin instead of silently downgrading it to Technician.
- The OIDC RS JWKS cache coalesces concurrent refreshes via a refresh Mutex with double-checked staleness, preventing a thundering-herd of network fetches at cache expiry or on a kid miss.

#PMS-190
Merge origin/main into fix/pms-190-legacy-auth-security-gaps
Some checks failed
E2E / Playwright against staging (pull_request) Failing after 16s
Check / fmt + clippy + compile + tests (pull_request) Successful in 2m53s
Create release / Create release from merged PR (pull_request) Has been skipped
7dd8b6afa7
#PMS-190
David merged commit 3337ef3df1 into main 2026-06-13 23:09:08 +02:00
David deleted branch fix/pms-190-legacy-auth-security-gaps 2026-06-13 23:09:08 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!201
No description provided.