Skip to content

OAuth Flow

Rack Gateway uses Google OAuth 2.0 with OpenID Connect (OIDC) for authentication. The implementation uses vetted libraries and follows security best practices including PKCE for CLI flows.

Create a Google OAuth client with the redirect URIs https://<DOMAIN>/api/v1/auth/web/callback and https://<DOMAIN>/api/v1/auth/cli/callback, and set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET and GOOGLE_ALLOWED_DOMAIN. See OAuth Setup for the steps.

The web flow uses standard OAuth 2.0 authorization code flow:

ParameterValuePurpose
response_typecodeRequest authorization code
scopeopenid profile emailRequest user info
stateRandom 32 bytesCSRF protection
access_typeonlineNo refresh token needed
promptselect_accountAlways show account picker
hdAllowed domainFilter to organization accounts

CLI Login Flow (RFC 8252 loopback, with PKCE)

Section titled “CLI Login Flow (RFC 8252 loopback, with PKCE)”

The CLI logs in with the RFC 8252 loopback flow. Two PKCE pairs are involved: the gateway’s own pair for the Google leg, and the CLI’s pair, which proves that only the CLI that started the login can finish it.

  • Loopback redirect only. /auth/cli/start only accepts a redirect URI of the form http://127.0.0.1:<port>/callback (or http://[::1]:<port>/callback). The login code is only ever sent to that address, so a login link someone else started delivers its code to your machine, never theirs.
  • Exchange before binding. The gateway exchanges Google’s authorization code at the callback, and only when that succeeds binds the login to the browser with an HttpOnly, SameSite=Lax cookie (path /api/v1/auth/cli). Every later browser step (MFA form, MFA submit, return, cancel) requires that cookie. The Google code is never stored.
  • Single-use login code. The login code is 256 bits of randomness, stored only as a SHA-256 hash, valid for 2 minutes, issued once per login, and deleted when redeemed (a failed redemption also burns it).
  • CLI PKCE. Redeeming the login code also requires the CLI’s code verifier, which never leaves the CLI.
  • Honest result page. The CLI holds the browser on its callback until it has redeemed the login code and saved the session, then redirects it (with Referrer-Policy: no-referrer, so the code isn’t leaked as a referrer) to the gateway’s success or error page. The error page only shows fixed messages for known error codes, so a crafted link can’t put its own text on it.
  • Time limit. The whole login must finish within 10 minutes.
  • First-factor enrollment. A user with no MFA factor can enroll one during the login, but only in the browser bound to the login.
  • Visible initiator. The MFA page shows the device name and IP that started the login, and the user gets a “New CLI Login” email when a CLI session is created. Cancel Login ends the login on the gateway and tells the waiting CLI.
ParameterDescription
code_verifier64 random bytes, base64url encoded (86 characters); kept by the CLI
code_challengeSHA-256(code_verifier), base64url encoded (43 characters)
code_challenge_methodAlways S256

The browser must run on the same machine as the CLI, because the login code is delivered to 127.0.0.1. To log in from a remote host (for example over SSH), forward the CLI’s port: start rack-gateway login --no-open, note the port in the printed “Waiting for the browser to return to http://127.0.0.1:<port>/callback” line, and run ssh -L <port>:127.0.0.1:<port> <host> from the machine with the browser before opening the printed login URL there.

The gateway and CLI must both support the loopback flow. After upgrading the gateway, install the matching rack-gateway CLI: an older CLI is told to upgrade, and a newer CLI refuses to log in to an older gateway.

The gateway verifies Google’s ID token using the go-oidc library:

  1. Signature verification

    Validates the JWT signature against Google’s public keys (fetched from OIDC discovery)

  2. Issuer validation

    Confirms the token was issued by https://accounts.google.com

  3. Audience validation

    Confirms the token was issued for our client ID

  4. Expiration check

    Ensures the token hasn’t expired

  5. Domain validation

    Requires email_verified, and an email domain and hd claim that both match GOOGLE_ALLOWED_DOMAIN

  6. User lookup

    The user must already exist: an admin adds them, or ADMIN_USERS (and the other seed variables) create them on first start. Signing in never creates a user.

Claims extracted from Google’s ID token:

{
"iss": "https://accounts.google.com",
"aud": "your-client-id.apps.googleusercontent.com",
"sub": "unique-user-id",
"email": "user@yourcompany.com",
"email_verified": true,
"name": "User Name",
"hd": "yourcompany.com"
}
ClaimDescriptionValidation
emailUser’s email addressRequired, domain checked
email_verifiedEmail verification statusMust be true
nameDisplay nameUsed for user record
hdHosted domainMust match allowed domain

A failed web login redirects to the login error page, /app/auth/error?error=<code>, which shows a fixed message for each code (never text from the URL or from Google):

CodeCause
unauthorizedThe account is outside GOOGLE_ALLOWED_DOMAIN, or isn’t a gateway user. An admin must add them
access_deniedThe user canceled or declined the sign-in at Google
identity_provider_errorGoogle returned another error
missing_codeThe callback had no authorization code or state
state_mismatchThe state doesn’t match this browser’s login (an old tab, or a forged link). Start again
exchange_failedGoogle wouldn’t exchange the code. Check the client ID and secret
session_failedThe gateway couldn’t create a session
audit_unavailableThe audit log couldn’t record the login, so it was refused

The CLI flow uses the same idea with its own page, /app/cli/auth/error?error=<code>.

If the gateway can’t reach Google’s OIDC discovery endpoint when it starts, it retries with exponential backoff for up to 20 seconds, then exits with “failed to create OIDC provider”. Check its outbound network access.

  • Generated using cryptographically secure random bytes
  • Stored in a short-lived cookie (web) or server-side (CLI) and validated on callback
  • Prevents CSRF attacks
  • ID tokens are verified and discarded immediately
  • Session tokens (not OAuth tokens) are stored hashed
  • No refresh tokens stored for web flow
  • Redirect URIs must exactly match those registered in Google Cloud Console
  • No wildcards or pattern matching
  • Separate URIs for web and CLI flows

For local development, use the mock OAuth server:

Terminal window
# Uses mock OAuth server at localhost:3345
task dev

The mock server simulates Google OAuth without requiring real credentials.

For integration testing against real Google OAuth:

  1. Create a separate OAuth client for testing
  2. Add http://localhost:8447/api/v1/auth/*/callback as redirect URIs
  3. Use test accounts from your Google Workspace

The redirect URI in your request doesn’t match any authorized URI in Google Cloud Console.

  1. Check the exact URI (including protocol and path)
  2. Ensure no trailing slashes mismatch
  3. Verify DOMAIN: the redirect URIs are https://<DOMAIN>/api/v1/auth/web/callback and https://<DOMAIN>/api/v1/auth/cli/callback

Client ID or secret is incorrect.

  1. Verify GOOGLE_CLIENT_ID matches the Cloud Console
  2. Verify GOOGLE_CLIENT_SECRET is correct
  3. Ensure the OAuth client hasn’t been deleted

If users aren’t seeing the account picker:

  1. Verify prompt=select_account is in the auth URL
  2. Check if the browser has cached credentials
  3. Try incognito/private browsing