{"record":{"id":"eb549c68923f7b47","repo":"grpc/grpc-go","slug":"no-expiration-claims","errorCode":null,"errorMessage":"no expiration claims","messagePattern":"no expiration claims","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"credentials/jwt/file_reader.go","lineNumber":107,"sourceCode":"\n// extractExpiration parses the JWT token to extract the expiration time.\nfunc (r *jwtFileReader) extractExpiration(token string) (time.Time, error) {\n\tclaimsRaw, ok := extractClaimsRaw(token)\n\tif !ok {\n\t\treturn time.Time{}, fmt.Errorf(\"expected 3 parts in token\")\n\t}\n\tpayloadBytes, err := base64.RawURLEncoding.DecodeString(claimsRaw)\n\tif err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"decode error: %v\", err)\n\t}\n\n\tvar claims jwtClaims\n\tif err := json.Unmarshal(payloadBytes, &claims); err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"unmarshal error: %v\", err)\n\t}\n\n\tif claims.Exp == 0 {\n\t\treturn time.Time{}, fmt.Errorf(\"no expiration claims\")\n\t}\n\n\texpTime := time.Unix(claims.Exp, 0)\n\n\t// Check if token is already expired.\n\tif expTime.Before(time.Now()) {\n\t\treturn time.Time{}, fmt.Errorf(\"expired token\")\n\t}\n\n\treturn expTime, nil\n}\n","sourceCodeStart":89,"sourceCodeEnd":119,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/credentials/jwt/file_reader.go#L89-L119","documentation":"Returned by extractExpiration when the decoded JSON claims have exp == 0 (the field is absent or explicitly zero). Without an expiration the reader cannot decide freshness, so it rejects the token. The sentinel errJWTValidation maps to codes.Unauthenticated.","triggerScenarios":"A JWT issued without an exp claim (non-compliant with the reader's requirement); a token with a differently-named/typed expiry field; a refresh token or assertion that omits exp.","commonSituations":"Custom token issuer that does not set exp; using an OAuth refresh token instead of an ID token; field name mismatch (e.g. expiry vs exp).","solutions":["Configure the token issuer to include a standard numeric exp claim (seconds since epoch).","Use an ID token from a compliant issuer (Google, Auth0, etc.) rather than a custom/refresh token.","Inspect the decoded payload to confirm the exp field name and type."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"func hasExpClaim(claimsSeg string) bool {\n    b, _ := base64.RawURLEncoding.DecodeString(claimsSeg)\n    var c struct{ Exp int64 `json:\"exp\"` }\n    _ = json.Unmarshal(b, &c)\n    return c.Exp != 0\n}","typeGuard":"func hasExpiry(claims map[string]any) bool {\n    exp, ok := claims[\"exp\"]\n    if !ok {\n        return false\n    }\n    switch v := exp.(type) {\n    case float64:\n        return v != 0\n    case int64:\n        return v != 0\n    case json.Number:\n        n, err := v.Int64()\n        return err == nil && n != 0\n    }\n    return false\n}","tryCatchPattern":null,"preventionTips":["Configure the issuer to always set exp.","Validate the exp claim presence at token ingestion.","Use ID tokens (which require exp) rather than refresh tokens."],"tags":["grpc","jwt","validation","claims","credentials"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}