Skip to content

feat(api): google SSO (OIDC) + auth-scoped access & geo-attendance#10

Draft
soumyaray wants to merge 4 commits into
mainfrom
8-sso
Draft

feat(api): google SSO (OIDC) + auth-scoped access & geo-attendance#10
soumyaray wants to merge 4 commits into
mainfrom
8-sso

Conversation

@soumyaray
Copy link
Copy Markdown
Contributor

Summary

Week-14 API authorization work. This is the lead branch of a two-branch
chain (8-auth-scope8-sso), opened against main, so the PR carries
both branches' work cumulatively:

  • 8-auth-scope — OAuth-style authorization scopes carried inside the
    encrypted auth token (AuthScope, resource:permission + * wildcard);
    every policy checks scope before role/ownership. Plus the Tyto-only
    geo-validated attendance feature: Haversine proximity to the event
    location with student coordinates encrypted at rest.
  • 8-sso — "Sign in with Google" via OpenID Connect: the API verifies
    a Google-signed id_token against Google's JWKS (RS256, iss/aud/exp),
    reads identity claims, and finds/links/creates an account keyed on
    (provider, external_id).

Mirror branches of Credence f51f5e6 + 98b4856, re-architected for Google OIDC.

Note: 8-auth-scope is pushed separately but intentionally has no PR of
its own this week — it is reviewed here as part of the chain.

Plan

AuthScope (8-auth-scope)

  • AuthScope lib (FULL/READ_ONLY, resource-agnostic) + token scope plumbing
  • AuthorizedAccount + AuthorizeAccount; account-detail mints a READ_ONLY token
  • Scope threaded controller → service → policy across all resources
  • Geo-attendance: encrypted coords migration, GeoFence primitive, RecordAttendance rules, staff toggle route
  • Token refactor: tokenize minimal {id, username} identity (folded into the payload)

Google SSO (8-sso)

  • Generic OidcVerifier + GoogleIdToken (jwt gem, RS256/JWKS, claim checks)
  • GoogleAccount mapper; sso_identities migration + SsoIdentity model
  • Provider-agnostic FindOrCreateSsoAccount (find → verified-email link → create)
  • AuthenticateSso + POST /api/v1/auth/sso; avatar serialized in account envelope

Deferred (per project workflow)

  • Code review
  • Retrospective migration audit
  • Squash + merge to main

Changes

Libauth_scope.rb, auth_token.rb (scope + minimal payload), geo_fence.rb (pure Haversine), oidc_verifier.rb (generic JWKS/RS256 verifier), google_id_token.rb (Google-configured instance).

Modelsauthorized_account.rb, google_account.rb, sso_identity.rb; account.rb (avatar in to_json), attendance.rb (encrypted coords).

Servicesauthorize_account.rb, authenticate_sso.rb, find_or_create_sso_account.rb; scope threaded through authenticate_account + the create_*/enroll_* services; record_attendance.rb (coords + geofence + OutOfRangeError/InvalidCoordinatesError).

Policies — scope guards added to course/event/location/attendance/enrollment policies.

Migrations009 encrypted attendance coords; 010 sso_identities (unique (provider, external_id)).

ConfigGOOGLE_CLIENT_ID, ATTENDANCE_RADIUS_M in secrets-example.yml.

Design notes

  • OIDC, not userinfo. The API establishes identity by cryptographically verifying the signed id_token against Google's JWKS — zero network hops, audience-bound (aud == GOOGLE_CLIENT_ID), replay-resistant. Re-architecture vs Credence's GitHub access-token + userinfo flow.
  • Identity keyed on (provider, external_id = sub), not email. email_verified only gates the optional verified-email link to a pre-existing account; an unverified-email collision with an existing account neither links nor duplicates → new EmailConflictError409.
  • Minimal token payload. The encrypted token carries only {id, username}; the API re-derives roles/policies from the id each request. The rich envelope rides in the plaintext response only.
  • GeoFence is a pure primitive. Geometry (Haversine) lives in geo_fence.rb; attendance-domain rules (radius, no-location, coord validation) live in RecordAttendance; the temporal live_now? check stays in AttendancePolicy.

Test plan

  • Full suite: 245 runs / 537 assertions / 0 failures
  • RuboCop clean (102 files), bundler-audit clean
  • Live SSO smoke S1–S6 passed against a real Google OAuth client (auto-create + avatar, repeat-reuse, verified-email link, CSRF state guard, password login unaffected)
  • AuthScope smoke T1–T3 passed (READ_ONLY blocks writes 403 vs FULL 201)

soumyaray added 4 commits May 28, 2026 16:15
Adds OAuth-style authorization scopes carried inside the encrypted auth
token (AuthScope, "resource:permission" syntax with "*" wildcard). Every
policy checks scope before role/ownership logic; the web-app session token
is FULL (*:write) while the account-detail endpoint mints a READ_ONLY
(*:read) key via AuthorizedAccount / AuthorizeAccount. Mirrors Credence
f51f5e6.

Tyto extension (no Credence counterpart): geo-validated attendance.
- migration 009 adds encrypted longitude_secure/latitude_secure on attendances
- GeoFence (app/lib/geo_fence.rb): a pure, domain-agnostic Haversine geofence
  primitive (contains?/distance_to)
- RecordAttendance owns the attendance-domain rules: radius from
  ATTENDANCE_RADIUS_M (default 55 m), the "no location -> no geofence" rule,
  and coordinate validation; stores submitted coords encrypted at rest
- staff PUT toggle for event attendances
- OutOfRangeError -> 422, InvalidCoordinatesError -> 400

Suite: 226 runs, 0 failures. RuboCop (93 files) + bundler-audit clean.
Adds Google Single Sign-On via OpenID Connect.

The App exchanges the OAuth code for a Google-signed id_token and hands it to
POST /api/v1/auth/sso. The API verifies that id_token against Google's JWKS
(RS256) and checks iss/aud/exp, then reads identity claims directly from the
verified token -- no userinfo callback. Identity is keyed on
(provider, external_id) via a new sso_identities table, not email: a known
identity returns its account, a verified-email match links to an existing
account, an unverified-email collision is refused (409), otherwise a new member
account is created. SSO mints a FULL-scope auth token. The account envelope now
serializes `avatar` so the App can show the Google photo.

Re-architected from the reference branch's GitHub access-token + userinfo flow
to OIDC id_token verification (jwt gem, OpenSSL/RS256 -- RbNaCl cannot do RSA).
The verifier is a generic OidcVerifier(jwks_uri, audience, allowed_issuers)
caching JWKS keys by kid; GoogleIdToken is a thin configured instance.

Suite: 245 runs / 537 assertions / 0 failures. RuboCop (102) + bundler-audit clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant