{"record":{"id":"fab72c4438ace2ef","repo":"juanfont/headscale","slug":"empty-oidc-callback-params","errorCode":null,"errorMessage":"empty OIDC callback params","messagePattern":"empty OIDC callback params","errorType":"http","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"hscontrol/oidc.go","lineNumber":47,"sourceCode":"\tauthCacheExpiration      = time.Minute * 15\n\n\t// authCacheMaxEntries bounds the OIDC state→[AuthInfo] cache to prevent\n\t// unauthenticated cache-fill DoS via repeated /register/{auth_id} or\n\t// /auth/{auth_id} GETs that mint OIDC state cookies.\n\tauthCacheMaxEntries = 1024\n\n\t// cookieNamePrefixLen is the number of leading characters from a\n\t// state/nonce value that [getCookieName] splices into the cookie name.\n\t// State and nonce values that are shorter than this are rejected at\n\t// the callback boundary so [getCookieName] cannot panic on a slice\n\t// out-of-range.\n\tcookieNamePrefixLen = 6\n)\n\nvar errOIDCStateTooShort = errors.New(\"oidc state parameter is too short\")\n\nvar (\n\terrEmptyOIDCCallbackParams = errors.New(\"empty OIDC callback params\")\n\terrNoOIDCIDToken           = errors.New(\"extracting ID token\")\n\terrNoOIDCRegistrationInfo  = errors.New(\"registration info not in cache\")\n\terrOIDCAllowedDomains      = errors.New(\n\t\t\"authenticated principal does not match any allowed domain\",\n\t)\n\terrOIDCAllowedGroups = errors.New(\"authenticated principal is not in any allowed group\")\n\terrOIDCAllowedUsers  = errors.New(\n\t\t\"authenticated principal does not match any allowed user\",\n\t)\n\terrOIDCUnverifiedEmail = errors.New(\"authenticated principal has an unverified email\")\n\terrInvalidPKCEMethod   = errors.New(\"invalid pkce.method\")\n)\n\n// AuthInfo contains both auth ID and verifier information for OIDC validation.\ntype AuthInfo struct {\n\tAuthID       types.AuthID\n\tVerifier     *string\n\tRegistration bool","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/oidc.go#L29-L65","documentation":"Returned by extractCodeAndStateParamFromRequest (hscontrol/oidc.go:449-451) when the OIDC callback request lacks the 'code' or 'state' query parameter. HTTP 400 'missing code or state parameter'. It is the first validation on the callback path before any IdP interaction happens.","triggerScenarios":"GET /oidc/callback with no query string, or with only one of code/state — e.g. an IdP error redirect (?error=access_denied) that omits both, or a user bookmarking the bare callback URL.","commonSituations":"IdP redirects with an error payload instead of code/state (user cancelled login, client misregistration); direct navigation/bookmark of the callback; monitoring probes hitting the endpoint.","solutions":["Handle IdP error redirects upstream: if the callback carries error/error_description, surface that to the user instead of retrying","Ensure headscale is registered with the IdP with the exact issuer/client/redirect configuration so successful logins always include code and state","Exclude the callback endpoint from health probes"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if r := mux.CurrentRoute(r); isCallback(r) { q := r.URL.Query(); if q.Get(\"code\") == \"\" || q.Get(\"state\") == \"\" { http.Error(w, \"login incomplete\", http.StatusBadRequest); return } }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Handle IdP error redirects (?error=...) before reaching headscale","Keep monitors and bookmarks off the callback endpoint","Register the exact callback URL with the IdP"],"tags":["oidc","auth","http","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}