mirror of
https://github.com/hanzoai/id.git
synced 2026-10-16 18:38:15 +00:00
fix(auth): thread OIDC nonce through the password-login path
Confidential OIDC clients that validate strictly (LibreChat openid-client with OPENID_REUSE_TOKENS) send a nonce on authorize and require the id_token to echo it. The portal's password-login path read code_challenge from the authorize URL but DROPPED nonce, so the IAM minted a code (and id_token) with no nonce -> openid-client 'unexpected JWT claim value encountered' -> hanzo.chat callback HTTP 500. Thread nonce: Login.tsx reads ?nonce, LoginForm forwards it, and client.login() puts it on the /v1/iam/login query (next to code_challenge). Additive + forward-only (never defaulted); social login already rode authorize() which carried nonce. Verified: id SPA builds green.
This commit is contained in:
@@ -9,6 +9,7 @@ export function Login({ client, brand }: { client: AuthClient; brand: BrandContr
|
||||
const clientIdOverride = sp.get('client_id') ?? undefined
|
||||
const codeChallenge = sp.get('code_challenge') ?? undefined
|
||||
const codeChallengeMethod = (sp.get('code_challenge_method') as 'S256' | 'plain' | null) ?? undefined
|
||||
const nonce = sp.get('nonce') ?? undefined
|
||||
return (
|
||||
<div className="hanzo-id-page hanzo-id-login">
|
||||
<BrandHeader brand={brand} />
|
||||
@@ -27,6 +28,7 @@ export function Login({ client, brand }: { client: AuthClient; brand: BrandContr
|
||||
clientIdOverride={clientIdOverride ?? undefined}
|
||||
codeChallenge={codeChallenge}
|
||||
codeChallengeMethod={codeChallengeMethod}
|
||||
nonce={nonce}
|
||||
/>
|
||||
<p className="hanzo-id-footer-links">
|
||||
<a href="/forget">Forgot password?</a> · <a href="/signup">Create account</a>
|
||||
|
||||
@@ -91,6 +91,10 @@ export function createAuthClient(opts: AuthClientOptions): AuthClient {
|
||||
if (req.redirectUri) url.searchParams.set('redirectUri', req.redirectUri)
|
||||
url.searchParams.set('scope', 'openid profile email')
|
||||
if (req.state) url.searchParams.set('state', req.state)
|
||||
// Echo the downstream OIDC nonce so the minted code -> id_token carries it.
|
||||
// Strict openid-client consumers (LibreChat OPENID_REUSE_TOKENS) reject an
|
||||
// id_token whose nonce != the one they sent ("unexpected JWT claim value").
|
||||
if (req.nonce) url.searchParams.set('nonce', req.nonce)
|
||||
if (req.codeChallenge) {
|
||||
url.searchParams.set('code_challenge', req.codeChallenge)
|
||||
url.searchParams.set('code_challenge_method', req.codeChallengeMethod ?? 'S256')
|
||||
|
||||
@@ -26,6 +26,15 @@ export interface LoginRequest {
|
||||
readonly state?: string
|
||||
readonly codeChallenge?: string
|
||||
readonly codeChallengeMethod?: 'S256' | 'plain'
|
||||
/**
|
||||
* OIDC nonce from the downstream authorize request. MUST be threaded through
|
||||
* the password-login path so the minted code (and resulting id_token) echo it.
|
||||
* Confidential OIDC clients that validate strictly (e.g. LibreChat /
|
||||
* openid-client with OPENID_REUSE_TOKENS) reject an id_token whose nonce
|
||||
* doesn't match the one they sent -> "unexpected JWT claim value" -> callback
|
||||
* 500. Forward, never default — only echo what the authorize URL carried.
|
||||
*/
|
||||
readonly nonce?: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface LoginFormProps {
|
||||
readonly clientIdOverride?: string
|
||||
readonly codeChallenge?: string
|
||||
readonly codeChallengeMethod?: 'S256' | 'plain'
|
||||
readonly nonce?: string
|
||||
readonly onSuccess?: (res: LoginResponse) => void
|
||||
readonly onMfaRequired?: (res: LoginResponse) => void
|
||||
}
|
||||
@@ -40,6 +41,7 @@ export function LoginForm(props: LoginFormProps) {
|
||||
state: props.state,
|
||||
codeChallenge: props.codeChallenge,
|
||||
codeChallengeMethod: props.codeChallengeMethod,
|
||||
nonce: props.nonce,
|
||||
})
|
||||
if (res.error) {
|
||||
setError(res.error)
|
||||
|
||||
Reference in New Issue
Block a user