v1.1 — Auth & Authorization Remediation
Every GraphQL resolver now enforces auth at the boundary with consistent extension codes, and the frontend handles 401 (sign in) vs 403 (no access) as distinct UX flows.
Summary
An audit of Objectuve's GraphQL surface caught nine resolvers returning bare error strings, seven mutations accepting a user_id argument that could be spoofed, and a frontend that silently logged auth errors to the console instead of redirecting the user to sign in. v1.1 remediated every finding in a single sweep.
The milestone installed a shared Authorization concern, migrated all ~45 resolver call sites off the old authenticate_user!/authorize_admin! helpers onto require_auth!/require_admin! with machine-readable extension codes (UNAUTHORIZED, FORBIDDEN), and rewrote Apollo's errorLink to refresh the session on UNAUTHORIZED and toast-then-stay on FORBIDDEN. Mass-assignment hardening stripped user_id from seven mutations; multi-tab sign-out now propagates via storage events. A CI script (bin/check_auth_specs) fails the build if any mutation spec lacks the auth shared examples.
Goal
Every GraphQL resolver enforces auth at the boundary with consistent extension codes, and the frontend handles 401 (sign in) and 403 (no access) as distinct UX flows — not silent console.log noise.
Scope — What Shipped
Authorizationconcern mounted on GraphQL base classes withrequire_auth!,require_admin!,require_ownership!helpers.- Extension codes — every auth failure returns
UNAUTHORIZEDorFORBIDDENinextensions.code; eight interaction error strings standardised. - RSpec shared examples —
'requires authentication','requires admin','requires resource ownership'assert on extension codes, enforced across every mutation spec. bin/check_auth_specsCI script fails the build if any mutation spec lacks the shared examples.- ~45 resolver call sites migrated — all mutations and queries in
app/graphql/use the new helpers; old helpers emit deprecation warnings and are removed. - 9 critical violations closed — four AI mutations (
get_advice,generate_milestones,refine_description,get_insight) and five others now require auth. user_idaccept-but-ignore shim — 7 mutations acceptuser_idfor backward compatibility but silently prefercontext[:current_user].- Apollo
errorLinkrewrite —UNAUTHORIZEDattempts one silent token refresh viaclerk.session.getToken({ skipCache: true })then redirects to/sign-in?redirect=<current>&reason=expired;FORBIDDENshows a warning toast and keeps the user in place. - Router role guard — non-admins hitting
/adminredirect to/dashboard?denied=admin, not to sign-in. - Multi-tab sign-out — signing out in one tab clears Sentry context, Apollo cache, and storage; other tabs detect and follow.
useApiErrorcomposable anduseClerkSync.isAdminref for reactive role state.
Phases
| Phase | Name | Status | Plans | Highlights |
|---|---|---|---|---|
| 5 | Backend Error Contract | Shipped | 2 | Extension codes, Authorization concern, shared examples, bin/check_auth_specs |
| 6 | Backend Resolver Migration | Shipped | 4 | All ~45 call sites migrated, 9 critical violations closed, user_id shim |
| 7 | Frontend Auth UX | Shipped | 4 | Apollo errorLink, UNAUTHORIZED redirect, FORBIDDEN toast, multi-tab sign-out |
| 8 | Mass-Assignment Hardening | Shipped | 2 | user_id removed from 7 mutations (backend + frontend) |
| 9 | Cleanup & CI Enforcement | Shipped | 2 | Old helpers removed, JWKS fallback, race rescue |
Key Decisions
- Extension codes over error messages — frontends branch on structured
extensions.code, never on string matching. The contract is versioned in shared examples. - Deprecation-then-delete — old
authenticate_user!/authorize_admin!helpers emitted deprecation warnings for one phase before removal, so mid-migration call sites had a loud signal. - CI gate for auth specs — a build-breaking script is the cheapest way to prevent regressions. Nothing else enforces "every mutation must assert auth" at the PR boundary.
- Accept-but-ignore
user_idshim — protected against mass-assignment without breaking existing clients still passing the argument. 22 out-of-scope mutations acknowledged in the audit remain for a future pass.
Requirements Coverage
54 / 54 requirements satisfied. All phases passed audit.
| Category | Count | Status |
|---|---|---|
| Backend Error Contract (BERR) | 10 | All satisfied |
| Critical Auth Fixes (CRIT) | 9 | All satisfied |
| Resolver Migration (MIGR) | 7 | All satisfied |
| Frontend Auth UX (FEUX) | 14 | All satisfied |
| Mass-Assignment Hardening (MASS) | 9 | All satisfied |
| Edge-Case Hardening (EDGE) | 5 | All satisfied |
Full list: v1.1-REQUIREMENTS.md.
Outcomes
- No silent auth failures. Every 401/403 reaches the user as a deliberate UX, not a console log.
- Resolver-level enforcement. Business logic never has to check
current_user; the resolver rejected the request before the interaction was invoked. - CI-enforced contract. New mutations cannot merge without auth shared examples, full stop.
- Foundation for delegation. v1.3 (Admin Dashboard) and v1.4 (AI Workforce) both rely on the
require_admin!helper and extension-code contract shipped here.
Tech Debt
From the audit (v1.1-MILESTONE-AUDIT.md):
- (phase 6) 22 mutations outside v1.1 scope remain without auth guards (acknowledged out-of-scope).
- (phase 6) 4 out-of-scope mutations (follow/leave/unfollow community,
update_showcased_achievements) still acceptuser_id. - (phase 7) Router fast-path sync resets
isAdmin=falseon cached navigation — may cause admin flicker on subsequent navigations. - (phase 9)
BERR-04usesrespond_to?(:user)inline instead of anOwnableconcern — documented intentional deviation.
Related Artifacts
- Roadmap: v1.1-ROADMAP.md
- Requirements: v1.1-REQUIREMENTS.md
- Milestone audit: v1.1-MILESTONE-AUDIT.md
- Phase artifacts:
.planning/milestones/v1.1-phases/ - PRD: Auth Audit & Authorization PRD
- Git tag: v1.1
Related Commits
a34e60d04—chore: complete v1.1 milestone — Auth & Authorization Remediation583d1ba0d—fix: update specs for v1.1 mass-assignment removal and auth error response format
Last updated: 2026-05-22