Abstract


Without OAuth 2.0With OAuth 2.0
Permission controlSharing your password gives the unlimited accessScopes limit the Client to only the permissions it requests
Attack surfaceStolen password = permanent, full accessAccess Tokens expire via TTL, limiting the damage window

Resource Owner


  • The owner of the identity, i.e. the live user themselves

Microsoft Entra consent prompt example

Managing previously-granted consent

Client


  • Third-party application that wants to act on the Resource Owner’s behalf

Scope

  • The specific permissions the Client requests, limiting what data and actions it can access on the Resource Server

Client ID

Client Secret

Access Token

Authorization Server


  • The application that hosts the Resource Owner’s account and acts as the single source of identity

Redirect URI

  • Also known as Callback URI. A URL on the Client where the Authorization Server sends the user back after they grant (or deny) consent

Response Type

Authorization Code

  • Short-lived code the Authorization Server returns to the Client, later exchanged for an Access Token

Resource Server


Login SSO vs Data Integration


Login SSOData Integration
PurposeAuthenticate a user into your appAccess user’s data on the provider’s APIs
Scopesopenid, email, profileResource-specific (gmail.readonly, calendar.events, …)
ResultApp issues its own session, often a JWTApp stores access + refresh tokens to call provider APIs
Example”Sign in with Google” on a login pageA CRM syncing Gmail threads and Calendar events

Swapping the two redirect URIs silently breaks both flows

If your app’s config accidentally assigns the login redirect URI to the data-integration flow and vice versa, the Authorization Server returns auth codes to the wrong handler. The login handler tries to exchange a code meant for Gmail scopes, the data-integration handler tries to issue a session from a login code, and both produce cryptic errors (often a malformed JWT or “No payload”). See Debugging the No Payload Error for a real-world walkthrough.

Debugging Redirect URI Mismatches


1. Verify the Actual Redirect URI at Runtime

  • Do not trust config files. curl the auth initiation endpoint and inspect what redirect_uri your app actually sends to the provider
# Follow the redirect and capture the full URL the provider receives
curl -sk -o /dev/null -w '%{redirect_url}\n' 'https://twenty.nushackers.org/auth/google'
  • URL-decode the redirect_uri parameter and compare character-by-character with what is registered in the Authorization Server
# URL-decode to make the comparison easier
python3 -c "from urllib.parse import unquote; print(unquote('PASTE_REDIRECT_URL_HERE'))"

2. Check for Config Override Layers

# Query the database directly to see what the app is actually reading
docker exec -it <db-container> psql -U postgres -d default \
  -c "SELECT key, value FROM core.\"keyValuePair\" WHERE key LIKE '%CALLBACK%';"

TwentyConfigService Precedence is a concrete example

DB values in core."keyValuePair" override .env with no warning. Editing the env file alone will not fix the bug.

3. Match URIs Exactly

  • The Redirect URI must match byte-for-byte between the app config and the auth server registration
  • Common mismatches: trailing slash, http vs https, port number, path casing, www. prefix

4. Verify the Authorization Server’s Registered URIs

  • In Google Cloud Console: APIs & Services > Credentials > OAuth 2.0 Client IDs > Authorized redirect URIs
  • The gcloud CLI cannot read or modify OAuth client redirect URIs, so this must be done in the web console

5. Distinguish Multiple OAuth Clients

  • If your infrastructure uses separate OAuth 2.0 clients for different purposes (e.g. one for Traefik forward-auth SSO, another for the app’s own login), ensure each client has the correct redirect URIs for its flow

Swapped credentials

Using the wrong Client ID or Client Secret in the wrong service produces confusing errors. Label your OAuth clients clearly in the provider console (e.g. “Traefik SSO” vs “Twenty CRM Login”).

References