feat(auth): portal redirect-only login, surface-aware OIDC error bounces, password-ops guard - #20
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
⏱️ Code Review completed (14 files · 25,769 chars · 1 PR unit(s))
⏱️ Adversarial Review completed (Model: qwen3.8-27b)
🔍 Verified Adversarial Review Findings
🔴 BLOCKER
web/app/(support)/support/login/_components/login-page.tsx:155: Undefined functionstartOIDCLogininSupportLoginPage- Failure Trace:
- The diff adds a "Try again" button to the OIDC error alert in
SupportLoginPage(around line 155 in the new file context). - The
onClickhandler is set tostartOIDCLogin. - The diff for
login-page.tsxdoes not define a function namedstartOIDCLoginwithin theSupportLoginPagecomponent scope. - The diff for
web/components/login-form.tsxdefinesstartOIDCLogin, butlogin-page.tsxis a separate component file and does not import it. - When the component renders and the user clicks the "Try again" button, JavaScript throws
ReferenceError: startOIDCLogin is not defined, preventing the user from retrying the OIDC login flow.
- The diff adds a "Try again" button to the OIDC error alert in
- Actionable Fix: Define the
startOIDCLoginfunction within theSupportLoginPagecomponent, mirroring the logic inlogin-form.tsx.
- Failure Trace:
const startOIDCLogin = () => {
window.location.href = `/api/auth/oidc_login?next=${encodeURIComponent(nextDestination)}`
}
(Insert this function definition inside the SupportLoginPage component, before the return statement.)
🛡️ Dismissed Claims
internal/handlers/api/auth_handler.go:120-121Open Redirect: Dismissed.oidcLoginErrorRedirectuses hardcoded paths (/dashboard/loginor/support/login) and does not reflect thenextparameter in the redirect URL, preventing open redirects.web/app/(support)/support/login/_components/login-page.tsx:55-58Null Safety: Dismissed.shouldRedirectToOIDCcorrectly guards againstpublicConfigbeing null/undefined viaBoolean(publicConfig && ...), and theuseEffectearly-returns if false.
There was a problem hiding this comment.
Code Review
This pull request implements auto-redirect to OIDC for the customer support portal when OIDC is the only enabled transport, routes failed OIDC round-trips back to their originating login surface, and enforces a security guard that prevents setting local passwords in SSO-only mode unless the user is a break-glass principal. Feedback from the review highlights potential Open Redirect vulnerabilities on both the backend and frontend, recommending sanitization of the redirect path parameters to ensure only safe local paths are processed.
| next := ctx.Query("next") | ||
| loginURL, err := services.OIDCLoginService.BuildOIDCLoginURL(next) |
There was a problem hiding this comment.
The next parameter is retrieved directly from the query string and passed to the OIDC login flow without validation. If an attacker provides a protocol-relative URL (e.g., //evil.com) or a path traversal sequence (e.g., /support/..//evil.com), it can bypass basic prefix checks on the frontend and lead to an Open Redirect vulnerability upon successful authentication. Sanitizing the next parameter here ensures that only safe local paths are processed.
next := ctx.Query("next")
if next != "" {
if !strings.HasPrefix(next, "/") || strings.HasPrefix(next, "//") || strings.HasPrefix(next, "/\\") || strings.Contains(next, "..") || strings.Contains(next, "\\") {
if services.IsSupportPortalNext(next) {
next = "/support"
} else {
next = "/dashboard"
}
}
}
loginURL, err := services.OIDCLoginService.BuildOIDCLoginURL(next)| function startOIDCLogin() { | ||
| window.location.href = `/api/auth/oidc_login?next=${encodeURIComponent(redirectPath)}` | ||
| } |
There was a problem hiding this comment.
As a defense-in-depth measure on the frontend, we should sanitize redirectPath before initiating the OIDC login flow to prevent potential open redirects via protocol-relative URLs or path traversal sequences.
| function startOIDCLogin() { | |
| window.location.href = `/api/auth/oidc_login?next=${encodeURIComponent(redirectPath)}` | |
| } | |
| function startOIDCLogin() { | |
| const safePath = redirectPath.startsWith("//") || redirectPath.includes("..") ? "/dashboard" : redirectPath | |
| window.location.href = "/api/auth/oidc_login?next=" + encodeURIComponent(safePath) | |
| } |
…ces, password-ops guard - /support/login auto-redirects to OIDC when it is the only enabled transport, mirroring the staff login; suppressed on an ?oidcError bounce and the break-glass ?direct=1 form. Portal-origin logins provision customer-type users, so the redirect can never mint staff accounts - both auto-redirects wait for the session probe (ready && !session), so an already-signed-in visitor is routed to their destination instead of being shipped to the IdP for a needless re-authentication - failed OIDC round-trips bounce back to the surface that started them: /support/login?oidcError= for /support/* targets, /dashboard/login otherwise (recovered from the signed state on callback failures), so a portal customer is never stranded on the staff login page - both login pages render a persistent DOS ID failure notice with a retry action instead of a transient toast - while password login is disabled, password mutations (/api/dashboard/user/reset_password, /api/dashboard/change_password) reject any target that is not on the break-glass allowlist, so SSO-only deployments never accumulate dormant local passwords - tests: error-redirect surface routing, password-ops guard matrix
30eacc1 to
a2059e1
Compare
Summary
Completes the Wave 2 redirect-only rollout that #18 started, now that #19 makes portal OIDC logins provision customer-type users:
/support/loginnow auto-redirects to DOS ID under the same conditions as the staff login: OIDC enabled, password login disabled, WxWork disabled, no break-glass?direct=1, and nooidcErrorbounce (loop guard). Portal-origin logins provision customer-type users (fix(oidc): portal-origin logins provision customer-type users #19), so the redirect can never mint staff accounts./support/login?oidcError=for/support/*targets,/dashboard/login?oidcError=otherwise. The callback recovers the target from the signed state (NextFromState), so a portal customer is never stranded on the staff login page - where a retry would have provisioned them as an employee.oidcErrortoast with a persistent alert (title + server message + retry action that restarts the OIDC flow).changePassword(the shared choke point for/api/dashboard/user/reset_passwordand/api/dashboard/change_password) rejects any target that is not on the break-glass allowlist, so an SSO-only deployment never accumulates dormant local passwords that would come alive if the switch were flipped back. Break-glass admins keep the path; mixed mode is untouched.Evidence
go vet -tags dev ./...clean;go test -tags devgreen across the CI package list, including new tests:TestOIDCLoginErrorRedirectTargetsOriginSurface, sixTestChangeOwnPassword*/TestResetPassword*guard-matrix cases.cd web && pnpm typecheckclean.pnpm lint: changed files clean; the 6 reported errors are the pre-existing react-hooks debt documented as non-blocking inci.yml(PROC-16).zh-CN.ymlanden-US.yml;auth.retryadded tozh-CN/en-US/vi-VNmessages (validated as JSON).Notes for reviewers
UserService.ResetPassword/ChangeOwnPasswordnow takeconfig.AuthConfig(handlers passconfig.Current().Auth), matching howAuthService.Loginalready receives it - keeps the guard unit-testable without loading global config.OIDCLogin(start failure) and bothOIDCCallbackfailure paths (IdP oauth error, token/login failure). WxWork bounces still go to/login?wxworkError=(pre-existing, WxWork disabled in DOS deployments).