| /* | |
| * | |
| * Copyright 2025 gRPC authors. | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| * | |
| */ | |
| package jwt | |
| import ( | |
| "encoding/base64" | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "os" | |
| "strings" | |
| "time" | |
| ) | |
| var ( | |
| errTokenFileAccess = errors.New("token file access error") | |
| errJWTValidation = errors.New("invalid JWT") | |
| ) | |
| // jwtClaims represents the JWT claims structure for extracting expiration time. | |
| type jwtClaims struct { | |
| Exp int64 `json:"exp"` | |
| } | |
| // jwtFileReader handles reading and parsing JWT tokens from files. | |
| // It is safe to call methods on this type concurrently as no state is stored. | |
| type jwtFileReader struct { | |
| tokenFilePath string | |
| } | |
| // readToken reads and parses a JWT token from the configured file. | |
| // Returns the token string, expiration time, and any error encountered. | |
| func (r *jwtFileReader) readToken() (string, time.Time, error) { | |
| tokenBytes, err := os.ReadFile(r.tokenFilePath) | |
| if err != nil { | |
| return "", time.Time{}, fmt.Errorf("%v: %w", err, errTokenFileAccess) | |
| } | |
| token := strings.TrimSpace(string(tokenBytes)) | |
| if token == "" { | |
| return "", time.Time{}, fmt.Errorf("token file %q is empty: %w", r.tokenFilePath, errJWTValidation) | |
| } | |
| exp, err := r.extractExpiration(token) | |
| if err != nil { | |
| return "", time.Time{}, fmt.Errorf("token file %q: %v: %w", r.tokenFilePath, err, errJWTValidation) | |
| } | |
| return token, exp, nil | |
| } | |
| const tokenDelim = "." | |
| // extractClaimsRaw returns the JWT's claims part as raw string. Even though the | |
| // header and signature are not used, it still expects that the input string to | |
| // be well-formed (ie comprised of exactly three parts, separated by a dot | |
| // character). | |
| func extractClaimsRaw(s string) (string, bool) { | |
| _, s, ok := strings.Cut(s, tokenDelim) | |
| if !ok { // no period found | |
| return "", false | |
| } | |
| claims, s, ok := strings.Cut(s, tokenDelim) | |
| if !ok { // only one period found | |
| return "", false | |
| } | |
| _, _, ok = strings.Cut(s, tokenDelim) | |
| if ok { // three periods found | |
| return "", false | |
| } | |
| return claims, true | |
| } | |
| // extractExpiration parses the JWT token to extract the expiration time. | |
| func (r *jwtFileReader) extractExpiration(token string) (time.Time, error) { | |
| claimsRaw, ok := extractClaimsRaw(token) | |
| if !ok { | |
| return time.Time{}, fmt.Errorf("expected 3 parts in token") | |
| } | |
| payloadBytes, err := base64.RawURLEncoding.DecodeString(claimsRaw) | |
| if err != nil { | |
| return time.Time{}, fmt.Errorf("decode error: %v", err) | |
| } | |
| var claims jwtClaims | |
| if err := json.Unmarshal(payloadBytes, &claims); err != nil { | |
| return time.Time{}, fmt.Errorf("unmarshal error: %v", err) | |
| } | |
| if claims.Exp == 0 { | |
| return time.Time{}, fmt.Errorf("no expiration claims") | |
| } | |
| expTime := time.Unix(claims.Exp, 0) | |
| // Check if token is already expired. | |
| if expTime.Before(time.Now()) { | |
| return time.Time{}, fmt.Errorf("expired token") | |
| } | |
| return expTime, nil | |
| } |