Skip to content

fix(sso): stop sending OIDC logins to localhost; return users to the UI - #136

Merged
snevesbarros merged 1 commit into
masterfrom
claude/fix-oidc-redirects
Oct 6, 2026
Merged

snevesbarros merged 1 commit into
masterfrom
claude/fix-oidc-redirects

Conversation

@snevesbarros

Copy link
Copy Markdown
Collaborator

Summary

An Okta (OIDC) login could send the browser to localhost, and after a successful login it ended up on the API rather than the UI. This PR fixes the two backend causes. The third cause is in the UI; see AnglesHQ/angles-ui (claude/fix-localhost-api-urls).

1. The redirect URI registered with the IdP ignored the configured API URL

Each provider's callback URL came from ANGLES_BASE_URL alone, which defaults to http://localhost:3000. Deployments set ANGLES_API_BASE_URL, which the UI and Swagger already use, and expected SSO to use it too. docker-compose.yml also passed ANGLES_BASE_URL=http://localhost:3000 explicitly, whatever else was configured.

  • The base URL now resolves in this order: ANGLES_BASE_URL, then ANGLES_API_BASE_URL, then http://localhost:3000. A value without a scheme takes the first of SWAGGER_SCHEMES, as Swagger does.
  • In docker-compose.yml, ANGLES_BASE_URL is empty unless set, and ANGLES_API_BASE_URL is ${ANGLES_API_BASE_URL:-http://localhost:3000} on both services.

2. After the IdP, the API redirected to its own origin

The callback redirected to / (or /login?error=true) relative to the API. The UI is usually on another port (3001) or host, so the user landed on the API.

  • When the UI starts a login, it now passes its origin as returnTo. The API keeps it in the session for that login only and redirects back to ${origin}/ or ${origin}/login?error=true. The callback reads it before req.logIn() replaces the session.
  • Open-redirect protection: only an http(s) origin on the API's own host is accepted. This is the same rule CORS already applies, with localhost and 127.0.0.1 treated as one host. It is now shared in app/utils/ui-origin.js, which CORS uses as well. Paths, credentials, other hosts, javascript: and protocol-relative values are all ignored.
  • Without a returnTo, for example from an older UI, the redirect stays relative, as before.
  • SAML: an assertion is usually posted back cross-site, and the SameSite=Lax session cookie doesn't come with it, so SAML keeps the old behaviour. docs/authentication.md says so.

Docs and config

  • docs/authentication.md documents the new ANGLES_BASE_URL fallback and the return to the UI.
  • The comments in docker-compose.yml and the Kubernetes ConfigMap are updated.

Testing

  • New tests:
    • test/sso-urls.tests.js: base URL resolution, and which origins are accepted.
    • Six end-to-end cases in test/settings.tests.js, run against the real signed-token test IdP: return to the UI, a denied login returning to the UI login page, keeping only the origin, ignoring a foreign host and invalid values, and forgetting returnTo after a login.
  • npm test against a local MongoDB: 620 passing. npx eslint app test server.js config is clean.
  • Browser check: I ran the API on 127.0.0.2:3000 with only ANGLES_API_BASE_URL set, the UI from the companion PR on 127.0.0.2:3001, and the test OIDC provider as the IdP. Clicking "Sign in with Okta" in Chromium went UI → API (?returnTo=http://127.0.0.2:3001) → IdP with redirect_uri=http://127.0.0.2:3000/rest/api/v1.0/auth/sso/okta/callback → API callback → http://127.0.0.2:3001/, signed in as the Okta user.

Deploying

Deployments that set ANGLES_BASE_URL are unaffected. Deployments that didn't will now register ANGLES_API_BASE_URL with their identity provider, so make sure that URL is the one registered there. The admin UI shows each provider's callback URL.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KgQXSUjuLVXmLWxobnMfSf


Generated by Claude Code

Two things sent an SSO login to the wrong place.

The redirect/callback URL registered with the identity provider came from
ANGLES_BASE_URL alone, defaulting to http://localhost:3000. Deployments set
ANGLES_API_BASE_URL (for the UI and Swagger) and reasonably expected that to
be used, and docker-compose passed ANGLES_BASE_URL=http://localhost:3000
explicitly whatever was configured. The base URL now falls back to
ANGLES_API_BASE_URL (taking the Swagger scheme when it has none) before
localhost, and docker-compose leaves ANGLES_BASE_URL empty unless set.

After a login the API redirected to `/` (or `/login?error=true`) on its own
origin, but the UI is usually on another port or host. The UI now passes its
origin as `returnTo` when it starts a login; the API keeps it in the session
for that login only and redirects back to it. Only an http(s) origin on the
API's own host is accepted (the same rule CORS applies, now shared in
app/utils/ui-origin.js), so it cannot be used as an open redirect. Without one
the redirect stays relative, as before. A SAML assertion is usually posted
back cross-site without the session cookie, so SAML keeps the old behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KgQXSUjuLVXmLWxobnMfSf
@snevesbarros
snevesbarros merged commit 8a34a18 into master Oct 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants