Skip to content

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

  • Authorization concern mounted on GraphQL base classes with require_auth!, require_admin!, require_ownership! helpers.
  • Extension codes — every auth failure returns UNAUTHORIZED or FORBIDDEN in extensions.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_specs CI 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_id accept-but-ignore shim — 7 mutations accept user_id for backward compatibility but silently prefer context[:current_user].
  • Apollo errorLink rewriteUNAUTHORIZED attempts one silent token refresh via clerk.session.getToken({ skipCache: true }) then redirects to /sign-in?redirect=<current>&reason=expired; FORBIDDEN shows a warning toast and keeps the user in place.
  • Router role guard — non-admins hitting /admin redirect 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.
  • useApiError composable and useClerkSync.isAdmin ref for reactive role state.

Phases

PhaseNameStatusPlansHighlights
5Backend Error ContractShipped2Extension codes, Authorization concern, shared examples, bin/check_auth_specs
6Backend Resolver MigrationShipped4All ~45 call sites migrated, 9 critical violations closed, user_id shim
7Frontend Auth UXShipped4Apollo errorLink, UNAUTHORIZED redirect, FORBIDDEN toast, multi-tab sign-out
8Mass-Assignment HardeningShipped2user_id removed from 7 mutations (backend + frontend)
9Cleanup & CI EnforcementShipped2Old 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_id shim — 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.

CategoryCountStatus
Backend Error Contract (BERR)10All satisfied
Critical Auth Fixes (CRIT)9All satisfied
Resolver Migration (MIGR)7All satisfied
Frontend Auth UX (FEUX)14All satisfied
Mass-Assignment Hardening (MASS)9All satisfied
Edge-Case Hardening (EDGE)5All 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 accept user_id.
  • (phase 7) Router fast-path sync resets isAdmin=false on cached navigation — may cause admin flicker on subsequent navigations.
  • (phase 9) BERR-04 uses respond_to?(:user) inline instead of an Ownable concern — documented intentional deviation.
  • a34e60d04chore: complete v1.1 milestone — Auth & Authorization Remediation
  • 583d1ba0dfix: update specs for v1.1 mass-assignment removal and auth error response format

Last updated: 2026-05-22

Loading…