{"record":{"id":"2af247ac71db4b5a","repo":"router-for-me/CLIProxyAPI","slug":"home-jwt-certificate-id-is-required","errorCode":null,"errorMessage":"home jwt certificate_id is required","messagePattern":"home jwt certificate_id is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/home/certificate.go","lineNumber":95,"sourceCode":"\t\t},\n\t}, nil\n}\n\nfunc parseHomeJWTClaims(rawJWT string) (homeJWTClaims, error) {\n\tvar claims homeJWTClaims\n\tparts := strings.Split(strings.TrimSpace(rawJWT), \".\")\n\tif len(parts) != 3 {\n\t\treturn claims, fmt.Errorf(\"home jwt is invalid\")\n\t}\n\tpayload, errDecode := decodeJWTPart(parts[1])\n\tif errDecode != nil {\n\t\treturn claims, errDecode\n\t}\n\tif errUnmarshal := json.Unmarshal(payload, &claims); errUnmarshal != nil {\n\t\treturn claims, errUnmarshal\n\t}\n\tif strings.TrimSpace(claims.CertificateID) == \"\" {\n\t\treturn claims, fmt.Errorf(\"home jwt certificate_id is required\")\n\t}\n\tif strings.TrimSpace(claims.ClusterID) == \"\" {\n\t\treturn claims, fmt.Errorf(\"home jwt cluster_id is required\")\n\t}\n\tif normalizeFingerprint(claims.CAFingerprint) == \"\" {\n\t\treturn claims, fmt.Errorf(\"home jwt ca_fingerprint is required\")\n\t}\n\tif strings.TrimSpace(claims.EnrollmentSecret) == \"\" {\n\t\treturn claims, fmt.Errorf(\"home jwt enrollment_secret is required\")\n\t}\n\tif strings.TrimSpace(claims.IP) == \"\" || claims.Port <= 0 {\n\t\treturn claims, fmt.Errorf(\"home jwt target address is invalid\")\n\t}\n\treturn claims, nil\n}\n\nfunc decodeJWTPart(part string) ([]byte, error) {\n\tif decoded, errDecode := base64.RawURLEncoding.DecodeString(part); errDecode == nil {","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/router-for-me/CLIProxyAPI/blob/78f0c4079e3e6273d65d03b5549cffc898703264/internal/home/certificate.go#L77-L113","documentation":"Claim-level validation in parseHomeJWTClaims: the decoded JWT payload's certificate_id claim is empty or whitespace-only. The certificate_id is required because it becomes the CSR CN when issuing the client certificate during home enrollment.","triggerScenarios":"The home coordinator issued an enrollment JWT whose payload lacks (or blanks) the certificate_id claim. Decoding succeeds, so this is a token-content problem, not a token-format problem.","commonSituations":"A stale or mis-minted token from an older coordinator version, manual token construction that omits the claim, or a payload copied from a different JWT type.","solutions":["Request a fresh enrollment token from the home coordinator / service that minted it","If you mint tokens yourself, include a non-empty certificate_id claim","Decode the payload (base64url of part 2) and confirm certificate_id is present before retrying"],"exampleFix":"// before (JWT payload)\n{\"cluster_id\":\"c1\",\"ca_fingerprint\":\"AA:BB..\"}\n\n// after (JWT payload)\n{\"certificate_id\":\"client-123\",\"cluster_id\":\"c1\",\"ca_fingerprint\":\"AA:BB..\"}","handlingStrategy":"validation","validationCode":"func hasClaim(payload []byte, key string) bool {\n    var m map[string]any\n    if json.Unmarshal(payload, &m) != nil {\n        return false\n    }\n    v, ok := m[key].(string)\n    return ok && strings.TrimSpace(v) != \"\"\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Decode and inspect the JWT payload before enrollment to verify certificate_id is present","Keep coordinator and client versions in lockstep so claim contracts match","Never hand-craft enrollment tokens; mint them from the coordinator"],"tags":["jwt","authentication","enrollment","home","validation"],"backgroundTag":null,"analyzedSha":"78f0c4079e3e6273d65d03b5549cffc898703264","analyzedAt":"2026-08-15T12:26:37.444Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}