Problem (user terms)
When someone signs in with Google, GitHub, Entra (OAuth) or SAML and it
succeeds, they never reach the application. The browser is in the middle of
a redirect from the identity provider and gets a raw JSON page back, with the
access and refresh tokens printed on screen. The user is stuck on that page, and
the session tokens are delivered in a script-readable body when every other
login path puts them in HttpOnly cookies.
Found while working on #295 (PR #802).
What the code does today
backend/internal/handler/sso_session.go:96 issueSSOSession ends with
c.JSON({access_token, refresh_token, …}) (lines 127-139). It never calls
middleware.IssueSessionCookies.
- Two callers:
oauth2_handler.go (OAuth callback, a browser GET redirected by
the provider) and saml2_handler.go:240 (SAML ACS, a browser POST from the IdP).
Both are top-level navigations, not XHR.
- Password and MFA login (
handler/auth/mfa_handler.go:151) already do it right:
IssueSessionCookies sets or_access, or_refresh and or_csrf.
- The error branches in
issueSSOSession also return JSON with a 500, which is a
dead end for a navigating browser. Every other OAuth exit redirects to
/login?error=<code> (see oauthFailure).
- The OAuth flow stores
ReturnTo (sanitised, oauth2_handler.go) but nothing
reads it after a successful sign-in.
Acceptance criteria
- On success,
issueSSOSession sets the session with
middleware.IssueSessionCookies (same TTLs as password login) and answers
with a 302 to the SPA, not a JSON body.
- No response from
issueSSOSession carries access_token or refresh_token
in the body or in the redirect URL (query or fragment).
- The redirect target is the OAuth flow's sanitised
ReturnTo when it is set,
otherwise the SPA home (oauthAppBaseURL). An off-site target is impossible.
sanitiseReturnTo stays the only gate.
- Every failure inside
issueSSOSession (onboarding, session issuance)
redirects to /login?error=internal, never a JSON 500.
- SAML ACS gets the same behaviour (cookies + redirect), and the POST→302 hop
still works.
- Once redirected, the SPA starts signed in with no manual step: it loads the
session from the cookies (/auth/me), including the CSRF token for
mutations. Verify this in a real browser. If the SPA cannot start from
cookies alone, the frontend change is part of this issue.
- Audit and last-login behaviour are unchanged (
AuditActionLogin still
logged).
- Tests: success sets the three cookies and redirects with no token in the
body; ReturnTo is honoured and an off-site value is ignored; each failure
branch redirects with error=internal; SAML path covered.
Definition of done
- Backend tests green (
go test ./... -count=1), go vet clean.
- Live proof: the branch server plus the SPA, one full OAuth sign-in against the
stub provider or a real one, landing signed in on the dashboard.
Include a screenshot or a command transcript, together with a passing test.
docs/API_SECURITY_GUIDE.md OAuth2/SAML2 flow updated. Its step 5 currently
says only "JWT token issued".
- PR with
Closes #<n> and an issue checkpoint comment.
Out of scope
- The in-process OAuth state store (single-replica limit, noted in
oauth_state_service.go).
- Any change to MFA enforcement on SSO sign-in.
Problem (user terms)
When someone signs in with Google, GitHub, Entra (OAuth) or SAML and it
succeeds, they never reach the application. The browser is in the middle of
a redirect from the identity provider and gets a raw JSON page back, with the
access and refresh tokens printed on screen. The user is stuck on that page, and
the session tokens are delivered in a script-readable body when every other
login path puts them in HttpOnly cookies.
Found while working on #295 (PR #802).
What the code does today
backend/internal/handler/sso_session.go:96issueSSOSessionends withc.JSON({access_token, refresh_token, …})(lines 127-139). It never callsmiddleware.IssueSessionCookies.oauth2_handler.go(OAuth callback, a browser GET redirected bythe provider) and
saml2_handler.go:240(SAML ACS, a browser POST from the IdP).Both are top-level navigations, not XHR.
handler/auth/mfa_handler.go:151) already do it right:IssueSessionCookiessetsor_access,or_refreshandor_csrf.issueSSOSessionalso return JSON with a 500, which is adead end for a navigating browser. Every other OAuth exit redirects to
/login?error=<code>(seeoauthFailure).ReturnTo(sanitised,oauth2_handler.go) but nothingreads it after a successful sign-in.
Acceptance criteria
issueSSOSessionsets the session withmiddleware.IssueSessionCookies(same TTLs as password login) and answerswith a 302 to the SPA, not a JSON body.
issueSSOSessioncarriesaccess_tokenorrefresh_tokenin the body or in the redirect URL (query or fragment).
ReturnTowhen it is set,otherwise the SPA home (
oauthAppBaseURL). An off-site target is impossible.sanitiseReturnTostays the only gate.issueSSOSession(onboarding, session issuance)redirects to
/login?error=internal, never a JSON 500.still works.
session from the cookies (
/auth/me), including the CSRF token formutations. Verify this in a real browser. If the SPA cannot start from
cookies alone, the frontend change is part of this issue.
AuditActionLoginstilllogged).
body;
ReturnTois honoured and an off-site value is ignored; each failurebranch redirects with
error=internal; SAML path covered.Definition of done
go test ./... -count=1),go vetclean.stub provider or a real one, landing signed in on the dashboard.
Include a screenshot or a command transcript, together with a passing test.
docs/API_SECURITY_GUIDE.mdOAuth2/SAML2 flow updated. Its step 5 currentlysays only "JWT token issued".
Closes #<n>and an issue checkpoint comment.Out of scope
oauth_state_service.go).