/* * * 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 }