{"record":{"id":"fb997e64427db9d4","repo":"hashicorp/nomad","slug":"invalid-jwt-issuer-v","errorCode":null,"errorMessage":"invalid JWT issuer: %v","messagePattern":"invalid JWT issuer: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"lib/auth/jwt/validator.go","lineNumber":82,"sourceCode":"\t\treturn nil, err\n\t}\n\n\tclaims, err := validator.Validate(ctx, token, expected)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to verify signature of JWT token: %v\", err)\n\t}\n\n\t// validate issuer manually, because we allow users to specify an array\n\tif len(methodConf.BoundIssuer) > 0 {\n\t\tif _, ok := claims[\"iss\"]; !ok {\n\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\"auth method specifies BoundIssuers but the provided token does not contain issuer information\",\n\t\t\t)\n\t\t}\n\t\tif iss, ok := claims[\"iss\"].(string); !ok {\n\t\t\treturn nil, fmt.Errorf(\"unable to read iss property of provided token\")\n\t\t} else if !slices.Contains(methodConf.BoundIssuer, iss) {\n\t\t\treturn nil, fmt.Errorf(\"invalid JWT issuer: %v\", claims[\"iss\"])\n\t\t}\n\t}\n\n\treturn claims, nil\n}\n\nfunc usingStaticKeys(keys []string) (jwt.KeySet, error) {\n\tvar parsedKeys []crypto.PublicKey\n\tfor _, v := range keys {\n\t\tkey, err := jwt.ParsePublicKeyPEM([]byte(v))\n\t\tparsedKeys = append(parsedKeys, key)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse public key for JWT auth: %v\", err)\n\t\t}\n\t}\n\treturn jwt.NewStaticKeySet(parsedKeys)\n}\n","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/lib/auth/jwt/validator.go#L64-L100","documentation":"The JWT validator rejects a token because its `iss` (issuer) claim is not present in the auth method's configured BoundIssuers list. Nomad requires that every JWT used for login be signed by a trusted issuer; this check runs only when the auth method specifies BoundIssuers. The token itself was decodable, but its issuer is not one of the allowed values.","triggerScenarios":"ACL auth method login with JWT auth: `nomad acl login -jwt <token>` against an auth method with JWTAuthMethod.BoundIssuers set, where the token's `iss` claim does not exactly match any entry in BoundIssuer.","commonSituations":"Misconfigured auth method (typo'd or missing issuer in BoundIssuers, e.g. `https://accounts.google.com` vs `accounts.google.com`); pointing clients at the wrong OIDC provider; provider changed its issuer URL after a tenant/region migration; using a token from a different environment's IdP.","solutions":["Decode the token (e.g. jwt.io or `nomad acl login`) and read its `iss` claim, then add that exact string to the auth method's BoundIssuers via `nomad acl auth-method update`.","Verify the auth method's BoundIssuers values match the provider's documented issuer URL exactly (scheme, host, trailing slash).","If the issuer is dynamic/unexpected, confirm the client is fetching tokens from the intended OIDC provider and not a stale or test IdP."],"exampleFix":"// before\nauthMethod := &structs.ACLAuthMethod{\n  Name: \"keycloak\",\n  Config: &structs.ACLAuthMethodConfig{\n    BoundIssuers: []string{\"https://keycloak.example.com/realms/old\"},\n  },\n}\n// after: issuer updated to match the token's iss claim\nauthMethod := &structs.ACLAuthMethod{\n  Name: \"keycloak\",\n  Config: &structs.ACLAuthMethodConfig{\n    BoundIssuers: []string{\"https://keycloak.example.com/realms/prod\"},\n  },\n}","handlingStrategy":"validation","validationCode":"// decode token's iss claim client-side before login\nparts := strings.Split(token, \".\")\nif len(parts) != 3 { return fmt.Errorf(\"not a JWT\") }\npayload, _ := base64.RawURLEncoding.DecodeString(parts[1])\nvar claims map[string]interface{}\njson.Unmarshal(payload, &claims)\niss, _ := claims[\"iss\"].(string)\nallowed := authMethodConfig.BoundIssuers\nif len(allowed) > 0 && !slices.Contains(allowed, iss) {\n  return fmt.Errorf(\"issuer %q not in BoundIssuers %v\", iss, allowed)\n}","typeGuard":"func hasTrustedIssuer(claims map[string]interface{}, bound []string) bool {\n  iss, ok := claims[\"iss\"].(string)\n  return ok && iss != \"\" && slices.Contains(bound, iss)\n}","tryCatchPattern":null,"preventionTips":["Copy BoundIssuers values from the provider's discovery document `issuer` field verbatim.","After any IdP migration, decode a fresh token and diff its iss against auth method config.","Note oidc CLI errors include the rejected issuer — use it to update config quickly."],"tags":["jwt","auth","acl","issuer-validation"],"backgroundTag":"jwt-issuer-mismatch","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}