{"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/03255a9237b6eb32710f6bc4f2de9a675b99fe36/credentials/jwt/file_reader.go#L89-L119","documentation":"After unmarshalling, if claims.Exp == 0 (the zero value for int64) extractExpiration returns 'no expiration claims' (file_reader.go:106-108). The reader requires an exp claim because it needs to know when to consider the token dead; a JWT without exp is unusable for this credential.","triggerScenarios":"The JWT payload has no exp field, exp is absent, or the issuer emitted exp:0. Because jwtClaims only decodes exp, any other claim structure is fine — but exp must be a positive Unix timestamp.","commonSituations":"A token minted by an issuer configured without expiration, a misconfigured OIDC provider, or a service-account token minted with a custom claims override that dropped exp.","solutions":["Configure the issuer to include a real exp (future Unix timestamp).","If using a custom claims builder, ensure exp is set and non-zero.","Switch to an ID-token source that always emits exp (GCP metadata server, OIDC provider)."],"exampleFix":"// before: token minted without exp\n// {\"iss\":\"svc\",\"aud\":\"api\"}\n\n// after: issuer includes exp\n// {\"iss\":\"svc\",\"aud\":\"api\",\"exp\":1735689600}","handlingStrategy":"validation","validationCode":"// Confirm exp is present and positive before use.\nvar c struct{ Exp int64 `json:\"exp\"` }\n_ = json.Unmarshal(payloadBytes, &c)\nif c.Exp == 0 {\n    return errors.New(\"token has no exp claim\")\n}","typeGuard":null,"tryCatchPattern":"_, _, err := r.readToken()\nif err != nil && strings.Contains(err.Error(), \"no expiration claims\") {\n    // issuer dropped exp; reconfigure the issuer to include it.\n    return err\n}","preventionTips":["Require the issuer to always emit a positive exp claim.","Use ID-token sources that mandate exp (GCP metadata, OIDC providers).","In custom claims builders, set exp explicitly.","Reject tokens without exp at integration boundaries."],"tags":["grpc","jwt","validation","credentials","configuration"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}