fix(login): read the PKCE challenge from the query, not only the body
image / build (push) Successful in 1m21s

A public client's login was rejected with "PKCE is required for public clients"
while the browser was sending a challenge the whole time — we only looked in
one place. Captured live:

  POST /v1/iam/login?clientId=lux-cloud&…&code_challenge=JCCAo2ey…&code_challenge_method=S256
  body: {"type","username","password","application","signinMethod","autoSignin","organization"}
  -> {"status":"error","msg":"PKCE is required for public clients"}

The login form is posted to the URL the authorize step handed the page, so the
OAuth parameters ride the QUERY. The body binds `codeChallenge` (camelCase);
the query spells it `code_challenge` (RFC 7636). Take the query value when the
body has none — body still wins when both are present, so this can only ever
supply a challenge, never replace one.
This commit is contained in:
zeekay
2026-07-26 10:53:08 -07:00
parent 7badd9dc74
commit 219fc64ee8
+24
View File
@@ -72,6 +72,7 @@ func loginHandler(db orm.DB) zip.Handler {
if err := c.Bind(&f); err != nil {
return httpx.Err(c, "invalid request body")
}
adoptQueryPKCE(c, &f)
ctx := c.Context()
// A post carrying no fresh credential but naming an outstanding challenge is
@@ -261,3 +262,26 @@ func loginOrgPasswordType(ctx context.Context, db orm.DB, org string) string {
}
return o.PasswordType
}
// adoptQueryPKCE takes the PKCE challenge from the QUERY STRING when the body
// did not carry one.
//
// The login form is posted to the URL the authorize step handed the page, and
// that URL already carries the OAuth parameters:
//
// POST /v1/iam/login?clientId=…&code_challenge=…&code_challenge_method=S256
// {"type":…,"username":…,"password":…,"application":…,"organization":…}
//
// The body binds `codeChallenge` (camelCase); the query spells it
// `code_challenge` (RFC 7636). Reading only the body threw away a challenge the
// client HAD sent, so a public client was rejected with "PKCE is required for
// public clients" — the request was complete, we were looking in one place.
// Body wins when both are present; the query is a fallback, never an override.
func adoptQueryPKCE(c *zip.Ctx, f *loginForm) {
if f.CodeChallenge == "" {
f.CodeChallenge = firstNonEmpty(c.Query("code_challenge"), c.Query("codeChallenge"))
}
if f.CodeChallengeMethod == "" {
f.CodeChallengeMethod = firstNonEmpty(c.Query("code_challenge_method"), c.Query("codeChallengeMethod"))
}
}