OIDC authentication
Marrow’s backend acts as an OIDC Relying Party. Any compliant provider works. This page covers Auth0 (recommended for multi-provider sign-in), Google, and Keycloak.
How it works
Section titled “How it works”- User hits
GET /api/auth/login. Backend redirects to the IdP. - IdP redirects back to
GET /api/auth/callback?code=…. - Backend exchanges the code for tokens, upserts the user in the
userstable, claims any pending org memberships matching the email, auto-creates a personal org if the user has none, and sets themarrow_sessioncookie (httpOnly JWT, 24h). - Subsequent requests carry the cookie; routes use it for RBAC.
Backend config
Section titled “Backend config”Add these to api/.env (or your prod env):
OIDC_ISSUER=https://accounts.google.comOIDC_CLIENT_ID=...OIDC_CLIENT_SECRET=...OIDC_REDIRECT_URI=https://api.example.com/api/auth/callbackFRONTEND_URL=https://app.example.comCOOKIE_DOMAIN=.example.comSECRET_KEY=<long random string>CORS_ORIGINS=https://app.example.comAnd on the frontend (web container env, or web/.env.local for dev):
MARROW_API_URL=https://api.example.comMARROW_OIDC_ENABLED=trueMARROW_OIDC_ENABLED enables the /login page and route-protection middleware. Without it the frontend assumes anonymous mode.
Cookie domain
Section titled “Cookie domain”The session cookie is set on COOKIE_DOMAIN. For the cookie to be sent from the web app to the API:
- Same domain (e.g. both on
localhost):COOKIE_DOMAIN=localhost. - Split subdomains (e.g.
app.example.com+api.example.com):COOKIE_DOMAIN=.example.com(note the leading dot).
If the cookie isn’t being sent on API calls, this is almost always the misconfiguration.
Provider setup
Section titled “Provider setup”Auth0 (recommended — supports GitHub + Google simultaneously)
Section titled “Auth0 (recommended — supports GitHub + Google simultaneously)”Marrow’s backend connects to a single OIDC_ISSUER. Auth0 sits in front of multiple social providers and presents one OIDC endpoint to Marrow, so users can sign in with either GitHub or Google.
Auth0 free tier: 7,500 monthly active users.
- Create an account at auth0.com. Create a Regular Web Application (not SPA or Machine-to-Machine).
- In Applications → Settings:
- Allowed Callback URLs:
https://api.example.com/api/auth/callback - Allowed Logout URLs:
https://app.example.com
- Allowed Callback URLs:
- Note your Domain (e.g.
myapp.us.auth0.com), Client ID, and Client Secret. - Enable GitHub — Authentication → Social → GitHub:
- Create a GitHub OAuth app. Homepage URL: your app URL. Callback URL:
https://<auth0-domain>/login/callback. - Paste the GitHub client ID and secret into Auth0.
- Create a GitHub OAuth app. Homepage URL: your app URL. Callback URL:
- Enable Google — Authentication → Social → Google. Auth0’s dev keys are fine for testing; replace with your own Google OAuth credentials for production.
- Set these env vars / secrets:
OIDC_ISSUER=https://<auth0-domain>/ # trailing slash requiredOIDC_CLIENT_ID=<auth0-client-id>OIDC_CLIENT_SECRET=<auth0-client-secret>
- Google Cloud Console → APIs & Services → Credentials.
- Create an OAuth 2.0 Client ID for Web application.
- Add Authorized redirect URIs:
https://api.example.com/api/auth/callback(andhttp://localhost:8000/api/auth/callbackfor dev). - Copy the Client ID and Client Secret into
OIDC_CLIENT_ID/OIDC_CLIENT_SECRET. - Set
OIDC_ISSUER=https://accounts.google.com.
Keycloak
Section titled “Keycloak”- In your realm, Clients → Create client.
- Client type: OpenID Connect. Client ID:
marrow. - Enable Client authentication. Set Valid redirect URIs to your callback URL.
- Credentials tab → copy the secret to
OIDC_CLIENT_SECRET. OIDC_ISSUER=https://keycloak.example.com/realms/<realm>.
Other providers
Section titled “Other providers”Any OIDC-compliant provider works. The OIDC_ISSUER URL must serve a valid /.well-known/openid-configuration.
Inviting members
Section titled “Inviting members”Once OIDC is on, invite teammates by email from the org settings page (/orgs/<id>/settings). The invite creates a pending membership; when the user logs in via OIDC with that email, the membership is automatically claimed. See routers/auth.py for the claim flow.