{"record":{"id":"6de998e5726ab80f","repo":"Tencent/WeKnora","slug":"id-token-missing-kid-and-jwks-contains-multiple-rs","errorCode":null,"errorMessage":"id_token missing kid and JWKS contains multiple RSA signing keys","messagePattern":"id_token missing kid and JWKS contains multiple RSA signing keys","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/application/service/user.go","lineNumber":1897,"sourceCode":"\t\tif kid != \"\" && k.Kid != kid {\n\t\t\tcontinue\n\t\t}\n\t\tif _, err := k.rsaPublicKey(); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tusable = append(usable, k)\n\t}\n\tif kid != \"\" {\n\t\tif len(usable) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"no matching JWKS RSA key for kid %q\", kid)\n\t\t}\n\t\treturn usable[0].rsaPublicKey()\n\t}\n\tif len(usable) == 0 {\n\t\treturn nil, errors.New(\"no matching JWKS key for id_token\")\n\t}\n\tif len(usable) > 1 {\n\t\treturn nil, errors.New(\"id_token missing kid and JWKS contains multiple RSA signing keys\")\n\t}\n\treturn usable[0].rsaPublicKey()\n}\n\n// fetchOIDCJWKS loads the provider's JWKS document over the SSRF-safe client.\nfunc (s *userService) fetchOIDCJWKS(ctx context.Context, jwksURI string) (*oidcJWKS, error) {\n\tif err := validateOIDCEndpoint(\"jwks\", jwksURI, true); err != nil {\n\t\treturn nil, err\n\t}\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, jwksURI, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tresp, err := newOIDCHTTPClient().Do(req)\n\tif err != nil {\n\t\treturn nil, err","sourceCodeStart":1879,"sourceCodeEnd":1915,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/application/service/user.go#L1879-L1915","documentation":"When an id_token lacks a kid header, verification can only proceed unambiguously if the JWKS contains exactly one RSA key. With multiple RSA keys present the correct one cannot be chosen, so the service fails closed instead of guessing.","triggerScenarios":"Token header has no kid, rsaKeyForKid skips the kid-match branch, and `usable` contains 2+ RSA keys from the JWKS.","commonSituations":"Provider rotates between several active signing keys but omits kid from token headers (provider bug/misconfiguration); custom token issuer that doesn't set kid; test tokens generated without a kid header.","solutions":["Fix the token issuer to include kid in the JOSE header (standard behavior for Auth0/Keycloak/Okta, etc.).","Use only one active signing key on the provider, or","If you control token generation, sign with the key whose kid you emit.","For testing, generate tokens with the same kid that the test JWKS publishes."],"exampleFix":"// before: token header {\"alg\":\"RS256\"}\n// after: token header includes key id\n{\"alg\":\"RS256\",\"kid\":\"key-1\"}","handlingStrategy":"validation","validationCode":"hdr := decodeHeader(idToken)\nrsaKeys := countRSAKeys(jwks)\nif hdr.Kid == \"\" && rsaKeys > 1 {\n    return fmt.Errorf(\"token has no kid but JWKS has %d RSA keys; issuer must emit kid\", rsaKeys)\n}","typeGuard":"func unambiguousToken(idToken string, jwks *oidcJWKS) bool {\n    kid := decodeHeader(idToken).Kid\n    if kid != \"\" { return true }\n    n := 0\n    for _, k := range jwks.Keys { if k.Kty == \"RSA\" { n++ } }\n    return n == 1\n}","tryCatchPattern":null,"preventionTips":["Ensure your token issuer always sets kid in the JOSE header.","Keep a single active signing key if you control the IdP and omit kid.","Check provider settings for an 'include kid in token header' option."],"tags":["jwks","kid","jwt-verification","ambiguous-key"],"backgroundTag":"jwks-key-mismatch","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}