{"record":{"id":"89e289862a07542e","repo":"juanfont/headscale","slug":"registration-info-not-in-cache","errorCode":null,"errorMessage":"registration info not in cache","messagePattern":"registration info not in cache","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/oidc.go","lineNumber":49,"sourceCode":"\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\n}\n","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/oidc.go#L31-L67","documentation":"Returned by getOauth2Token (hscontrol/oidc.go:473-476) when PKCE is enabled and the callback 'state' is not found in the in-memory auth cache that stores the PKCE verifier. HTTP 404 'registration not found'. The cache is per-process and bounded (authCacheMaxEntries = 1024).","triggerScenarios":"PKCE enabled (oidc.pkce.enabled) and the state is absent from the auth cache: session older than register_cache_expiration (default 15m), headscale restarted between /register and callback, cache eviction under heavy registration volume, or multiple headscale replicas without sticky routing.","commonSituations":"User sits on the IdP login page past the cache expiry; headscale redeploy/restart mid-login; load-balanced control planes where the callback lands on a different instance; scripted load tests minting >1024 pending registrations.","solutions":["Restart the login flow from /register so a fresh state+verifier pair is cached","Complete the IdC login within register_cache_expiration (raise it if users are slow)","With multiple replicas, pin callbacks to the instance that started the flow (sticky routing) — the cache is not shared"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if httpErr, ok := err.(*HTTPError); ok && errors.Is(httpErr.err, errNoOIDCRegistrationInfo) { redirect user to /register to mint a fresh state+verifier }","preventionTips":["Complete login within register_cache_expiration","Use sticky routing when running multiple headscale replicas with PKCE","Avoid triggering >1024 concurrent pending registrations (cache cap)"],"tags":["oidc","pkce","cache","auth","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}