Skip to content

OAuth relay to support multiple runs active at a time. #144

@dpup

Description

@dpup

Problem

Google OAuth requires registering specific host:port combinations as authorized redirect URIs. In production this is straightforward — one origin per app. Without Moat, running multiple services locally means each gets its own port (localhost:3001, localhost:3002, etc.) and each port must be registered as a separate redirect URI in Google's console. With Moat, multiple applications share a single host:port (localhost:8080) via subdomain routing (e.g. web.run1.localhost:8080, web.run2.localhost:8080), but Google doesn't allow wildcard subdomains in redirect URIs.

The OAuth relay solves this by giving Google a single registered callback, then routing the authorization code to the correct application.

How Moat Routing Already Works

Moat maps subdomained requests to containers:

web.run1.localhost:8080  →  container1:3000
web.run2.localhost:8080  →  container2:3000

The OAuth relay extends this pattern to the OAuth callback.


Design

What Moat Injects

Moat injects three values into each application container:

Variable Description
MOAT_OAUTH_RELAY_URL URL to start the OAuth flow via Moat
GOOGLE_CLIENT_ID Moat-managed OAuth client ID
GOOGLE_CLIENT_SECRET Moat-managed OAuth client secret

In production, the application uses its own GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET and redirects to Google directly. These variable names are not Moat-specific — the application reads the same variables in both environments.

Flow

Browser             App                 Moat Relay              Google
  │                  │                      │                      │
  │── click login ──▶│                      │                      │
  │◀── redirect ─────│                      │                      │
  │                  │                      │                      │
  │── GET relay?app=myapp ─────────────────▶│                      │
  │◀── redirect to Google ──────────────────│                      │
  │                                         │                      │
  │── authorize ──────────────────────────────────────────────────▶│
  │◀── redirect to Moat callback ──────────────────────────────────│
  │                                         │                      │
  │── code= ───────────────────────────────▶│                      │
  │◀── redirect to app callback + code ─────│                      │
  │                                         │                      │
  │── /__auth/callback?code=… ──▶│          │                      │
  │                  │── exchange code ───────────────────────────▶│
  │                  │◀── tokens ──────────────────────────────────│
  │◀── session ──────│                      │                      │

Step by Step

  1. User clicks "Login." App redirects browser to ${MOAT_OAUTH_RELAY_URL}?app=myapp.
  2. Moat relay initiates OAuth with Google, using its registered redirect URI (oauthrelay.localhost:8080/callback).
  3. User authenticates with Google.
  4. Google redirects browser to Moat's callback with ?code=....
  5. Moat looks up which app started the flow, and redirects the browser to web.myapp.localhost:8080/__auth/callback?code=....
  6. App receives the authorization code and exchanges it for tokens using GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET — the same code path as production.
  7. App establishes session. Login complete.

What the App Does

The app's callback handler is identical in all environments. It always:

  1. Receives an authorization code.
  2. Exchanges it for tokens.
  3. Establishes a session.

It does not know or care whether the code arrived via Google directly or via Moat's relay.


Production Comparison

Production Local (Moat)
Browser redirects to Google directly Moat relay → Google
Google calls back to App's registered redirect URI Moat's registered redirect URI
Code delivered to app by Google Moat (forwarded)
App exchanges code for tokens Yes Yes
App callback handler (e.g.) /__auth/callback /__auth/callback
OAuth credentials owned by App Moat (injected into app)

The app's callback handler is the same in both environments. The path itself is up to the app. The only difference is how the authorization code arrives.


What Moat Needs to Track

Per login attempt:

  • Which app initiated the flow (the app parameter).
  • Where to forward the code — derived from the app's subdomain routing (e.g. app=myappweb.myapp.localhost:8080).

This is the same routing table Moat already maintains.


What Moat Registers with Google

A single OAuth client with one authorized redirect URI:

https://oauthrelay.localhost:8080/callback

No per-app registration. No enumerating ports.


Non-Goals

  • Abstracting OAuth away from applications. Apps still do their own token exchange.
  • Hiding credentials from containers. The client ID and secret are injected — apps use them directly.
  • Replacing production OAuth infrastructure. This is a local development convenience.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions