Web SSO (desktop Apple/Google login)
General RP integration is a separate track
- Web apps → 🌐 Web integration track
- Mobile apps → 📱 Mobile integration track
This page covers the desktop web SSO mechanism by which logi uses Apple/Google as upstream IdPs.
Two entry points
| Host | User | Destination |
|---|---|---|
api.1pass.dev/session/new | End user | /oauth/authorize?... (RP consent) |
start.1pass.dev/console/login | Developer | /developer (cross-host handoff) |
Flow
User browser
│
▼ ① Click "Continue with Apple/Google"
api.1pass.dev/auth/{apple,google}/web/start?return_to=<path>
│ ② Issue an OauthWebState row (state, nonce, return_to, 10 min TTL)
│ ③ 302 to the Apple/Google authorization endpoint
▼
appleid.apple.com / accounts.google.com
│ ④ User logs in + consents
▼ ⑤ Callback
api.1pass.dev/auth/{apple,google}/web/callback
│ ⑥ Consume the state once (provider-scoped)
│ ⑦ Exchange code → id_token
│ ⑧ Verify id_token against the JWKS (alg, iss, aud, exp, nonce, sub, email_verified)
│ ⑨ WebSsoUserResolver: find by sub → email merge → create
│ ⑩ start_new_session_for → issue a Session
▼
┌─────────── Is return_to /oauth/authorize?
│ YES NO (console path)
▼ ▼
Set api.1pass.dev cookie Issue a SessionHandoffToken (30s TTL)
Redirect to return_to Delete the api.1pass.dev cookie
start.1pass.dev/session/handoff?token=...
▼
start.1pass.dev consumes the token
Issues its own host cookie
Redirects to return_toRoutes
| Controller | Route |
|---|---|
Web::GoogleSsoController | GET /auth/google/web/start, GET /auth/google/web/callback |
Web::AppleSsoController | GET /auth/apple/web/start, POST /auth/apple/web/callback (form_post, CSRF skip) |
SessionHandoffsController | GET /session/handoff (console-host-constrained) |
Console setup
Apple Services ID
- Identifiers → Services IDs → +
- Description:
logi Web SSO - Identifier:
dev.1pass.web(=APPLE_WEB_SERVICES_ID) - Sign in with Apple → Configure
- Primary App ID:
com.dcodelabs.logi(the same SIWA key as the native bundle also signs the web client_secret JWT) - Domains:
api.1pass.dev - Return URLs:
https://api.1pass.dev/auth/apple/web/callback
- Primary App ID:
Google OAuth Web Client
- APIs & Services → Credentials → + CREATE CREDENTIALS → OAuth client ID
- Application type: Web application
- Name:
logi Web SSO - Authorized JavaScript origins:
https://api.1pass.dev - Authorized redirect URIs:
https://api.1pass.dev/auth/google/web/callback - OAuth Consent Screen → PUBLISH APP (in Testing mode, only registered test users can sign in)
ENV (Render logi-server)
APPLE_WEB_SERVICES_ID=dev.1pass.web
# Apple SIWA key (shared between native + web)
APPLE_TEAM_ID=<your Apple Developer Team ID>
APPLE_KEY_ID=<Key ID of your SIWA key>
APPLE_PRIVATE_KEY=<.p8 PEM contents — or mount via APPLE_PRIVATE_KEY_PATH>
GOOGLE_WEB_CLIENT_ID=<public Client ID>
GOOGLE_WEB_CLIENT_SECRET=<Client Secret>⚠️ Always update Render env vars with an individual PUT (never a Collection PUT). The MCP update_environment_variables tool is safe because it defaults to replace=false (merge).
Security invariants
- State is a server-side
OauthWebStaterow: single-use, provider-scoped, 10 min TTL. - return_to is validated at
/startand stored in the DB; the callback uses only the row value (it does not trust the parameter). safe_return_to?does a path-exact match (/oauth/authorize,/console,/console/..., etc.).- ID token: alg whitelist (
ES256,RS256), kid required, iss/aud exact, exp+iat skew 60s, nonce, sub non-empty, Googleazp(when present) exact, Googleemail_verified=trueenforced. - The Apple nonce uses direct equality (unlike the SHA256 hashing in native).
- The JWKS forces a refresh on an unknown kid (to avoid key-rotation outages).
/startrate limit 30/min/IP.- After the callback,
reset_session→start_new_session_for(session fixation defense). - Apple
userform field: 2KB raw cap + 128B per name part. - Pending-purge / locked / suspended accounts are blocked (
AccountPendingPurge,AccountLockedOrSuspended,ProviderSubConflict). - The session cookie
domain:is host-only (keeps embed.1pass.dev isolated). - The handoff endpoint return_to whitelist allows only
/console,/developer,/demo(/oauth/authorizeis rejected).
Operational procedures
Rotating the Google Client Secret
- Google Cloud Console → Credentials → the OAuth Client → Add Secret
- Update the Render env var
GOOGLE_WEB_CLIENT_SECRET(merge) - Deploy and confirm login works
- Disable the old secret
Rotating the Apple .p8
- Apple Developer → Keys → create a new key (SIWA capability)
- Render env: update
APPLE_KEY_ID+APPLE_PRIVATE_KEY - Deploy and confirm login works
- Revoke the old key
Pending-purge users
If a user within the soft-delete grace period tries to re-register via web SSO, they are blocked with AccountPendingPurge. Retry after the mobile-app recovery flow (POST /api/v1/account_recoveries → consume).
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Returned to the login page after console SSO | SessionHandoffToken not issued or expired (30s) | grep [session_handoff] failure: in the Render logs |
Google access_denied | Consent screen in Testing mode | OAuth consent screen → PUBLISH APP |
Apple invalid_client | The Bundle ID was used as client_id | Confirm APPLE_WEB_SERVICES_ID=dev.1pass.web |
jwks: unknown kid after refresh | Key rotation or token forgery | Force-invalidate auth/jwks_cache/{apple_web,google} in Rails.cache and retry |
Increased frequency of state mismatch | 10 min TTL exceeded or multi-tab | Check the TTL. Because state is provider-scoped, multi-tab is safe |
Known limitations
- Mobile UAs do not show the SSO card — the native app flow (
logi://+ Universal Links) is canonical. - Passkey is not enforced immediately after console SSO (advisory).
- Cross-provider merge is not possible for Apple private relay (it can only be unified via a native device link).
Related documents
- Login method restriction — restrict the SSO/login methods an RP surfaces on the login screen (the
providerparameter · console setting)