{"record":{"id":"e321bbce5abdb5f5","repo":"nats-io/nats-server","slug":"jwt-subject-nkey-and-provided-nkey-do-not-match","errorCode":null,"errorMessage":"jwt subject nkey and provided nkey do not match","messagePattern":"jwt subject nkey and provided nkey do not match","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/dirstore.go","lineNumber":535,"sourceCode":"\tdirPath := filepath.Dir(path)\n\tif _, err := validateDirPath(dirPath); err != nil {\n\t\tif err := os.MkdirAll(dirPath, defaultDirPerms); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif _, err := os.Stat(path); err == nil {\n\t\tif newJWT, err := jwt.DecodeGeneric(theJWT); err != nil {\n\t\t\treturn err\n\t\t} else if existing, err := os.ReadFile(path); err != nil {\n\t\t\treturn err\n\t\t} else if existingJWT, err := jwt.DecodeGeneric(string(existing)); err != nil {\n\t\t\t// skip if it can't be decoded\n\t\t} else if existingJWT.ID == newJWT.ID {\n\t\t\treturn nil\n\t\t} else if existingJWT.IssuedAt > newJWT.IssuedAt {\n\t\t\treturn nil\n\t\t} else if newJWT.Subject != publicKey {\n\t\t\treturn fmt.Errorf(\"jwt subject nkey and provided nkey do not match\")\n\t\t} else if existingJWT.Subject != newJWT.Subject {\n\t\t\treturn fmt.Errorf(\"subject of existing and new jwt do not match\")\n\t\t}\n\t}\n\tstore.Lock()\n\tcb := store.changed\n\tchanged, err := store.write(path, publicKey, theJWT)\n\tstore.Unlock()\n\tif err != nil {\n\t\treturn err\n\t} else if changed && cb != nil {\n\t\tcb(publicKey)\n\t}\n\treturn nil\n}\n\nfunc xorAssign(lVal *[sha256.Size]byte, rVal [sha256.Size]byte) {\n\tfor i := range rVal {","sourceCodeStart":517,"sourceCodeEnd":553,"githubUrl":"https://github.com/nats-io/nats-server/blob/3a66a489d262bf89b71a71c955c94920394532f3/server/dirstore.go#L517-L553","documentation":"During saveIfNewer, after decoding the existing and new JWTs, the store checks that the new JWT's subject claim matches the public key the caller supplied as the storage key. A mismatch means the caller is trying to file a JWT under a key that does not own it, which would corrupt the resolver's key-to-JWT mapping.","triggerScenarios":"Calling Store/Merge where publicKey != the Subject claim of theJWT (after the existing-JWT short-circuits for identical ID or older IssuedAt).","commonSituations":"Passing an issuer key instead of the subject key; storing activation JWTs under the account key rather than the correct subject; bug in batch-merge code pairing keys with the wrong JWTs.","solutions":["Ensure the key passed to Store/saveIfNewer equals newJWT.Subject as decoded from the JWT","Decode the JWT first and use its claims.Subject as the storage key","Audit Merge callers that zip external key lists with JWT lists for off-by-one/mispairing"],"exampleFix":"// before\nstore.Store(issuerKey, accJWT) // error: jwt subject nkey and provided nkey do not match\n// after\nclaims, _ := jwt.DecodeAccountClaims(accJWT)\nstore.Store(claims.Subject, accJWT)","handlingStrategy":"validation","validationCode":"claims, err := jwt.DecodeAccountClaims(theJWT)\nif err != nil { return err }\nif claims.Subject != publicKey { return fmt.Errorf(\"key %q does not own jwt subject %q\", publicKey, claims.Subject) }\nerr = store.Store(publicKey, theJWT)","typeGuard":null,"tryCatchPattern":"if err := store.Store(pub, jwt); err != nil && strings.Contains(err.Error(), \"do not match\") {\n    log.Printf(\"jwt/key mismatch for %s\", pub)\n}","preventionTips":["Always use claims.Subject as the storage key","Pair keys and JWTs carefully in bulk merge code","Add unit tests covering key/subject agreement"],"tags":["jwt","validation","claims"],"backgroundTag":"jwt-subject-key-mismatch","analyzedSha":"3a66a489d262bf89b71a71c955c94920394532f3","analyzedAt":"2026-09-02T04:41:54.247Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}