{"record":{"id":"43ba6bbded6d3c42","repo":"juanfont/headscale","slug":"extracting-id-token","errorCode":null,"errorMessage":"extracting ID token","messagePattern":"extracting ID token","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/oidc.go","lineNumber":48,"sourceCode":"\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\n}","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/oidc.go#L30-L66","documentation":"Returned by getOauth2Token (hscontrol/oidc.go:498) when the OAuth2 token exchange succeeded but the returned token contains no id_token. Headscale's OIDC flow requires an OpenID Connect ID token to identify the principal. HTTP 400 'no id_token'.","triggerScenarios":"The code-for-token exchange at the IdP token endpoint returns an access token only. Typical causes: the 'openid' scope missing from the request/client config, the IdP client not configured as an OIDC client, or an authorization code being reused (some IdPs then return a degraded token).","commonSituations":"oidc.scope config in headscale.yaml missing 'openid'; IdP application created as plain OAuth2 instead of OIDC; clock skew causing the IdP to silently drop the id_token; replaying a used code during debugging.","solutions":["Add 'openid' to the scope list in the oidc section of headscale.yaml","Verify the IdP client is an OpenID Connect client and can issue ID tokens (test with an OIDC debugger flow)","Ensure the authorization code is exchanged exactly once — a replayed code can yield a token without id_token"],"exampleFix":"# before\noidc:\n  scope: [\"profile\", \"email\"]\n\n# after\noidc:\n  scope: [\"openid\", \"profile\", \"email\"]","handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always include 'openid' in oidc.scope","Smoke-test the IdP with an OIDC flow that returns an id_token before pointing headscale at it","Never replay authorization codes during debugging"],"tags":["oidc","auth","config","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}