Using Kubernetes ServiceAccount Token for Auth Between Microservices
In Kubernetes environments, microservices often need to authenticate with each other securely. While external authentication mechanisms like OAuth2 or mutual TLS are common, Kubernetes provides a built-in solution through ServiceAccount tokens. This article explores how to leverage ServiceAccount tokens as JWT credentials, using Kubernetes as an OIDC provider for lightweight, native authentication between microservices.
Understanding ServiceAccount Tokens as OIDC
ServiceAccount tokens are JWT (JSON Web Tokens) that Kubernetes automatically provisions for pods. Starting with Kubernetes 1.21, the API server can act as an OIDC provider, allowing services to validate these tokens directly without requiring Kubernetes API calls or special permissions.
How It Works
- Kubernetes API server generates a JWT token signed with the cluster’s private key
 - The token is mounted into the pod at 
/var/run/secrets/kubernetes.io/serviceaccount/token - The token includes standard JWT claims: issuer, subject, audience, expiration
 - Services validate tokens using OIDC discovery and JWKS (JSON Web Key Set) from the API server
 - No Kubernetes client libraries or RBAC permissions required on the server side
 
ServiceAccount Token Structure
A ServiceAccount token is a standard JWT with three parts: header, payload, and signature. Here’s what the decoded payload looks like:
{
  "aud": [
    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1793562486,
  "iat": 1762026486,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "jti": "f8e7d6c5-b4a3-9281-7f6e-5d4c3b2a1098",
  "kubernetes.io": {
    "namespace": "billing",
    "node": {
      "name": "worker-node-1.cluster.local",
      "uid": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
    },
    "pod": {
      "name": "api-gateway-7d8f9c5b6-x9k2m",
      "uid": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e"
    },
    "serviceaccount": {
      "name": "api-gateway",
      "uid": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"
    },
    "warnafter": 1762030093
  },
  "nbf": 1762026486,
  "sub": "system:serviceaccount:billing:api-gateway"
}
Key Claims:
- iss (issuer): The Kubernetes API server that issued the token
 - sub (subject): ServiceAccount identity in format 
system:serviceaccount:<namespace>:<serviceaccount-name> - aud (audience): Intended recipient(s) of the token
 - exp (expiration): Unix timestamp when token expires
 - iat (issued at): Unix timestamp when token was issued
 - nbf (not before): Unix timestamp before which token is not valid
 - jti (JWT ID): Unique identifier for this token
 - kubernetes.io: Kubernetes-specific metadata including:
- namespace: The namespace where the ServiceAccount exists
 - node: Information about the node running the pod
 - pod: Pod name and UID where the token is mounted
 - serviceaccount: ServiceAccount name and UID
 - warnafter: Timestamp when the token should be refreshed (typically before expiration)
 
 
The subject claim is what services use for authorization - it uniquely identifies which ServiceAccount is making the request.
Example Architecture
This article demonstrates authentication in a typical microservices architecture:
API Gateway (Client)
- Receives external requests
 - Calls downstream services with authentication
 - Uses its own ServiceAccount token to identify itself
 
Payment API (Server)
- Processes payment transactions
 - Validates incoming tokens to verify caller identity
 - Uses internal authorization policy to allow only api-gateway and admin-service
 
Notification API (Server)
- Sends notifications (email, SMS, etc.)
 - Validates incoming tokens to verify caller identity
 - Uses internal authorization policy to allow multiple services
 
Payment Notification Service (Client & Worker)
- Listens for payment events
 - Calls Notification API to send notifications
 - Uses its own ServiceAccount token to authenticate
 
POST /api/v1/payments → API Gateway (uses api-gateway token) → Payment API (validates & authorizes)
POST /api/v1/notifications → API Gateway (uses api-gateway token) → Notification API (validates & authorizes)
Payment Event → Payment Notification Service (uses payment-notification-service token) → Notification API (validates & authorizes)
Each service uses its own ServiceAccount token for identification. Receiving services validate tokens and enforce authorization based on the caller’s identity.
Setting Up ServiceAccounts
Creating ServiceAccounts
Create ServiceAccounts for your microservices:
# serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: api-gateway
  namespace: billing
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: payment-api
  namespace: billing
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: notification-api
  namespace: billing
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: payment-notification-service
  namespace: billing
Configuring Deployments
Each service uses its own ServiceAccount token. When calling other services, it presents its own identity:
# api-gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-gateway
  namespace: billing
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-gateway
  template:
    metadata:
      labels:
        app: api-gateway
    spec:
      serviceAccountName: api-gateway
      containers:
      - name: gateway
        image: myregistry/api-gateway:latest
        env:
        - name: PAYMENT_API_URL
          value: "http://payment-api.billing.svc.cluster.local"
        - name: NOTIFICATION_API_URL
          value: "http://notification-api.billing.svc.cluster.local"
The default ServiceAccount token is automatically mounted at /var/run/secrets/kubernetes.io/serviceaccount/token. Each service uses this single token to authenticate when calling other services.
API Server Configuration
The Kubernetes API server must be configured with the service account issuer and JWKS URI. Most managed Kubernetes services (GKE, EKS, AKS) have this enabled by default. For self-managed clusters, ensure these flags are set:
--service-account-issuer=https://kubernetes.default.svc.cluster.local
--service-account-jwks-uri=https://kubernetes.default.svc.cluster.local/openid/v1/jwks
--service-account-signing-key-file=/etc/kubernetes/pki/sa.key
--service-account-key-file=/etc/kubernetes/pki/sa.pub
No additional RBAC permissions are required for the server service, as token validation is performed locally using public keys.
Client Implementation
Reading the ServiceAccount Token
package client
import (
    "fmt"
    "os"
)
const (
    defaultTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
)
type TokenProvider struct {
    tokenPath string
}
func NewTokenProvider(tokenPath string) *TokenProvider {
    if tokenPath == "" {
        tokenPath = defaultTokenPath
    }
    return &TokenProvider{tokenPath: tokenPath}
}
func (tp *TokenProvider) GetToken() (string, error) {
    token, err := os.ReadFile(tp.tokenPath)
    if err != nil {
        return "", fmt.Errorf("failed to read service account token: %w", err)
    }
    if len(token) == 0 {
        return "", fmt.Errorf("service account token is empty")
    }
    return string(token), nil
}
Making Authenticated Requests
package client
import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"
)
type AuthenticatedClient struct {
    httpClient    *http.Client
    tokenProvider *TokenProvider
    baseURL       string
}
func NewAuthenticatedClient(baseURL string, tokenPath string) *AuthenticatedClient {
    return &AuthenticatedClient{
        httpClient: &http.Client{
            Timeout: 30 * time.Second,
        },
        tokenProvider: NewTokenProvider(tokenPath),
        baseURL:       baseURL,
    }
}
func (c *AuthenticatedClient) Do(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
    token, err := c.tokenProvider.GetToken()
    if err != nil {
        return nil, fmt.Errorf("failed to get token: %w", err)
    }
    url := c.baseURL + path
    req, err := http.NewRequestWithContext(ctx, method, url, body)
    if err != nil {
        return nil, fmt.Errorf("failed to create request: %w", err)
    }
    req.Header.Set("X-Authorization", "Bearer "+token)
    if body != nil {
        req.Header.Set("Content-Type", "application/json")
    }
    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("request failed: %w", err)
    }
    return resp, nil
}
Note on X-Authorization Header:
This implementation uses the X-Authorization header instead of the standard Authorization header for service-to-service communication. This is a deliberate design choice:
- The 
Authorizationheader is typically reserved for end-user authentication (from external clients, API gateways handling user requests) - Using 
X-Authorizationfor service-to-service communication allows you to clearly separate:- Authorization header: End-user credentials (JWT from users, OAuth tokens, etc.)
 - X-Authorization header: Service identity credentials (Kubernetes ServiceAccount tokens)
 
 - This separation makes it possible for services to receive requests that contain both user context AND service identity
 - If your architecture doesn’t need to distinguish between user and service credentials, you can use the standard 
Authorizationheader instead 
Usage Example: API Gateway
The API Gateway exposes endpoints and proxies requests to downstream services. It uses its own ServiceAccount token to authenticate with all backend APIs:
package main
import (
    "context"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "github.com/gorilla/mux"
)
type Gateway struct {
    paymentClient      *AuthenticatedClient
    notificationClient *AuthenticatedClient
}
func NewGateway() (*Gateway, error) {
    paymentURL := os.Getenv("PAYMENT_API_URL")
    notificationURL := os.Getenv("NOTIFICATION_API_URL")
    // Use the same token (api-gateway's own token) for all calls
    tokenPath := "/var/run/secrets/kubernetes.io/serviceaccount/token"
    return &Gateway{
        paymentClient: NewAuthenticatedClient(
            paymentURL,
            tokenPath,
        ),
        notificationClient: NewAuthenticatedClient(
            notificationURL,
            tokenPath,
        ),
    }, nil
}
func (g *Gateway) handleRequest(client *AuthenticatedClient) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        // Forward request to backend service
        resp, err := client.Do(ctx, r.Method, r.URL.Path, r.Body)
        if err != nil {
            log.Printf("Backend API call failed: %v", err)
            http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
            return
        }
        defer resp.Body.Close()
        // Copy response status and headers
        for key, values := range resp.Header {
            for _, value := range values {
                w.Header().Add(key, value)
            }
        }
        w.WriteHeader(resp.StatusCode)
        // Copy response body
        io.Copy(w, resp.Body)
    }
}
func main() {
    gateway, err := NewGateway()
    if err != nil {
        log.Fatalf("Failed to create gateway: %v", err)
    }
    router := mux.NewRouter()
    router.HandleFunc("/api/v1/payments", gateway.handleRequest(gateway.paymentClient)).Methods(http.MethodPost)
    router.HandleFunc("/api/v1/notifications", gateway.handleRequest(gateway.notificationClient)).Methods(http.MethodPost)
    if err := http.ListenAndServe(":8080", router); err != nil {
        log.Fatalf("Server failed: %v", err)
    }
}
Server Implementation
JWT Token Validation with OIDC
Each service validates incoming tokens to verify the caller’s identity. The service then checks its internal authorization policy to determine if that caller is allowed access:
package server
import (
    "context"
    "fmt"
    "github.com/coreos/go-oidc/v3/oidc"
)
type TokenValidator struct {
    verifier *oidc.IDTokenVerifier
}
func NewTokenValidator(issuerURL string) (*TokenValidator, error) {
    ctx := context.Background()
    provider, err := oidc.NewProvider(ctx, issuerURL)
    if err != nil {
        return nil, fmt.Errorf("failed to create OIDC provider: %w", err)
    }
    // Skip audience verification - we'll authorize based on subject
    verifier := provider.Verifier(&oidc.Config{
        SkipClientIDCheck: true,
    })
    return &TokenValidator{
        verifier: verifier,
    }, nil
}
func (v *TokenValidator) ValidateToken(ctx context.Context, rawToken string) (*TokenInfo, error) {
    idToken, err := v.verifier.Verify(ctx, rawToken)
    if err != nil {
        return nil, fmt.Errorf("token verification failed: %w", err)
    }
    var claims struct {
        Sub string `json:"sub"`
    }
    if err := idToken.Claims(&claims); err != nil {
        return nil, fmt.Errorf("failed to parse claims: %w", err)
    }
    return &TokenInfo{
        Subject:  claims.Sub,
        Issuer:   idToken.Issuer,
        Audience: idToken.Audience,
    }, nil
}
type TokenInfo struct {
    Subject  string
    Issuer   string
    Audience []string
}
The Subject field contains the ServiceAccount identity in the format system:serviceaccount:<namespace>:<serviceaccount-name>.
HTTP Middleware
package server
import (
    "context"
    "log"
    "net/http"
    "strings"
)
type contextKey string
const tokenInfoKey contextKey = "tokenInfo"
type AuthMiddleware struct {
    validator *TokenValidator
}
func NewAuthMiddleware(issuerURL string) (*AuthMiddleware, error) {
    validator, err := NewTokenValidator(issuerURL)
    if err != nil {
        return nil, err
    }
    return &AuthMiddleware{
        validator: validator,
    }, nil
}
func (m *AuthMiddleware) Authenticate(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("X-Authorization")
        if authHeader == "" {
            http.Error(w, "X-Authorization header required", http.StatusUnauthorized)
            return
        }
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            http.Error(w, "Invalid X-Authorization header format", http.StatusUnauthorized)
            return
        }
        token := parts[1]
        tokenInfo, err := m.validator.ValidateToken(r.Context(), token)
        if err != nil {
            log.Printf("Token validation failed: %v", err)
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), tokenInfoKey, tokenInfo)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}
func GetTokenInfo(ctx context.Context) (*TokenInfo, bool) {
    tokenInfo, ok := ctx.Value(tokenInfoKey).(*TokenInfo)
    return tokenInfo, ok
}
Server Application: Payment API
package main
import (
    "encoding/json"
    "log"
    "net/http"
    "github.com/gorilla/mux"
)
func main() {
    issuerURL := "https://kubernetes.default.svc.cluster.local"
    authMiddleware, err := NewAuthMiddleware(issuerURL)
    if err != nil {
        log.Fatalf("Failed to create auth middleware: %v", err)
    }
    router := mux.NewRouter()
    api := router.PathPrefix("/api/v1").Subrouter()
    api.Use(authMiddleware.Authenticate)
    api.HandleFunc("/payments", handlePayment).Methods(http.MethodPost)
    api.HandleFunc("/payments/{id}", handleGetPayment).Methods(http.MethodGet)
    if err := http.ListenAndServe(":8080", router); err != nil {
        log.Fatalf("Server failed: %v", err)
    }
}
func handlePayment(w http.ResponseWriter, r *http.Request) {
    tokenInfo, ok := GetTokenInfo(r.Context())
    if !ok {
        http.Error(w, "Token info not found", http.StatusInternalServerError)
        return
    }
    log.Printf("Payment request from: %s", tokenInfo.Subject)
    // Process payment logic here
    response := map[string]interface{}{
        "status":       "success",
        "payment_id":   "pay_12345",
        "amount":       100.00,
        "authenticated_by": tokenInfo.Subject,
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}
func handleGetPayment(w http.ResponseWriter, r *http.Request) {
    tokenInfo, ok := GetTokenInfo(r.Context())
    if !ok {
        http.Error(w, "Token info not found", http.StatusInternalServerError)
        return
    }
    vars := mux.Vars(r)
    paymentID := vars["id"]
    response := map[string]interface{}{
        "payment_id": paymentID,
        "status":     "completed",
        "requester":  tokenInfo.Subject,
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}
Authorization with ServiceAccount Identity
After validating the token, each service determines access based on the caller’s identity. The token’s subject claim identifies which ServiceAccount made the request, allowing fine-grained access control.
Extracting ServiceAccount Information
ServiceAccount subjects follow the format: system:serviceaccount:<namespace>:<name>
package authorization
import (
    "fmt"
    "strings"
)
type ServiceAccountIdentity struct {
    Namespace      string
    ServiceAccount string
}
func ParseServiceAccountSubject(subject string) (*ServiceAccountIdentity, error) {
    prefix := "system:serviceaccount:"
    if !strings.HasPrefix(subject, prefix) {
        return nil, fmt.Errorf("not a service account subject: %s", subject)
    }
    parts := strings.SplitN(strings.TrimPrefix(subject, prefix), ":", 2)
    if len(parts) != 2 {
        return nil, fmt.Errorf("invalid service account format: %s", subject)
    }
    return &ServiceAccountIdentity{
        Namespace:      parts[0],
        ServiceAccount: parts[1],
    }, nil
}
Authorization Middleware
package authorization
import (
    "log"
    "net/http"
)
type AuthorizationPolicy struct {
    allowedServiceAccounts map[string]map[string]bool
}
func NewAuthorizationPolicy() *AuthorizationPolicy {
    return &AuthorizationPolicy{
        allowedServiceAccounts: make(map[string]map[string]bool),
    }
}
func (p *AuthorizationPolicy) Allow(namespace, serviceAccount string) {
    if p.allowedServiceAccounts[namespace] == nil {
        p.allowedServiceAccounts[namespace] = make(map[string]bool)
    }
    p.allowedServiceAccounts[namespace][serviceAccount] = true
}
func (p *AuthorizationPolicy) IsAllowed(namespace, serviceAccount string) bool {
    if accounts, ok := p.allowedServiceAccounts[namespace]; ok {
        return accounts[serviceAccount]
    }
    return false
}
func (p *AuthorizationPolicy) Authorize(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tokenInfo, ok := GetTokenInfo(r.Context())
        if !ok {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        identity, err := ParseServiceAccountSubject(tokenInfo.Subject)
        if err != nil {
            log.Printf("Failed to parse service account: %v", err)
            http.Error(w, "Invalid service account", http.StatusForbidden)
            return
        }
        if !p.IsAllowed(identity.Namespace, identity.ServiceAccount) {
            log.Printf("ServiceAccount %s/%s not authorized",
                identity.Namespace, identity.ServiceAccount)
            http.Error(w, "Forbidden", http.StatusForbidden)
            return
        }
        next.ServeHTTP(w, r)
    })
}
Usage with Authorization: Payment API
The Payment API validates tokens and restricts access based on the caller’s ServiceAccount identity:
func main() {
    issuerURL := "https://kubernetes.default.svc.cluster.local"
    authMiddleware, err := NewAuthMiddleware(issuerURL)
    if err != nil {
        log.Fatalf("Failed to create auth middleware: %v", err)
    }
    // Only allow api-gateway and admin-service to access payment API
    authzPolicy := NewAuthorizationPolicy()
    authzPolicy.Allow("billing", "api-gateway")
    authzPolicy.Allow("billing", "admin-service")
    router := mux.NewRouter()
    api := router.PathPrefix("/api/v1").Subrouter()
    api.Use(authMiddleware.Authenticate)
    api.Use(authzPolicy.Authorize)
    api.HandleFunc("/payments", handlePayment).Methods(http.MethodPost)
    api.HandleFunc("/payments/{id}", handleGetPayment).Methods(http.MethodGet)
    if err := http.ListenAndServe(":8080", router); err != nil {
        log.Fatalf("Server failed: %v", err)
    }
}
Similarly, the Notification API has its own authorization policy allowing different callers:
func main() {
    issuerURL := "https://kubernetes.default.svc.cluster.local"
    authMiddleware, err := NewAuthMiddleware(issuerURL)
    if err != nil {
        log.Fatalf("Failed to create auth middleware: %v", err)
    }
    // Allow multiple services to send notifications
    authzPolicy := NewAuthorizationPolicy()
    authzPolicy.Allow("billing", "api-gateway")
    authzPolicy.Allow("billing", "payment-notification-service")
    router := mux.NewRouter()
    api := router.PathPrefix("/api/v1").Subrouter()
    api.Use(authMiddleware.Authenticate)
    api.Use(authzPolicy.Authorize)
    api.HandleFunc("/notifications", handleNotification).Methods(http.MethodPost)
    if err := http.ListenAndServe(":8080", router); err != nil {
        log.Fatalf("Server failed: %v", err)
    }
}
Example: Payment Notification Service
The Payment Notification Service listens for payment events and sends notifications. It uses its own ServiceAccount token to call the Notification API:
package main
import (
    "bytes"
    "context"
    "encoding/json"
    "log"
    "os"
)
type PaymentNotificationService struct {
    notificationClient *AuthenticatedClient
}
func NewPaymentNotificationService() (*PaymentNotificationService, error) {
    notificationURL := os.Getenv("NOTIFICATION_API_URL")
    tokenPath := "/var/run/secrets/kubernetes.io/serviceaccount/token"
    return &PaymentNotificationService{
        notificationClient: NewAuthenticatedClient(notificationURL, tokenPath),
    }, nil
}
func (s *PaymentNotificationService) SendPaymentNotification(paymentID string, amount float64) error {
    ctx := context.Background()
    notification := map[string]interface{}{
        "type":       "email",
        "to":         "customer@example.com",
        "subject":    "Payment Confirmation",
        "body":       "Your payment has been processed successfully",
        "payment_id": paymentID,
        "amount":     amount,
    }
    payloadBytes, err := json.Marshal(notification)
    if err != nil {
        return err
    }
    resp, err := s.notificationClient.Do(ctx, "POST", "/api/v1/notifications", bytes.NewReader(payloadBytes))
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    if resp.StatusCode != 200 {
        log.Printf("Notification API returned status: %d", resp.StatusCode)
        return err
    }
    log.Printf("Notification sent for payment %s", paymentID)
    return nil
}
func main() {
    service, err := NewPaymentNotificationService()
    if err != nil {
        log.Fatalf("Failed to create service: %v", err)
    }
    // Example: Process payment event
    if err := service.SendPaymentNotification("pay_12345", 100.00); err != nil {
        log.Printf("Failed to send notification: %v", err)
    }
}
Troubleshooting
Common Issues and Solutions
1. Token Not Found
Error: failed to read service account token: open /var/run/secrets/kubernetes.io/serviceaccount/token: no such file or directory
Solution: Verify ServiceAccount is mounted correctly in pod spec:
spec:
  serviceAccountName: my-service-account
  automountServiceAccountToken: true
2. OIDC Discovery Failed
Error: failed to create OIDC provider: failed to get provider configuration from https://kubernetes.default.svc.cluster.local/.well-known/openid-configuration
Solution: Verify the issuer URL is accessible and API server has OIDC discovery enabled:
# Test OIDC discovery endpoint
curl -k https://kubernetes.default.svc.cluster.local/.well-known/openid-configuration
# Verify API server flags
kubectl get pod -n kube-system kube-apiserver-<node> -o yaml | grep service-account-issuer
3. Unauthorized Access
Error: ServiceAccount billing/some-service not authorized
Solution: Check the service’s authorization policy to ensure the caller is allowed:
// In the receiving service
authzPolicy := NewAuthorizationPolicy()
authzPolicy.Allow("billing", "api-gateway")  // Add the calling service
Testing Token Validation
package main
import (
    "context"
    "fmt"
    "log"
    "os"
)
func main() {
    token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
    if err != nil {
        log.Fatalf("Failed to read token: %v", err)
    }
    fmt.Printf("Token length: %d\n", len(token))
    fmt.Printf("Token preview: %s...\n", string(token[:min(50, len(token))]))
    issuerURL := "https://kubernetes.default.svc.cluster.local"
    validator, err := NewTokenValidator(issuerURL)
    if err != nil {
        log.Fatalf("Failed to create validator: %v", err)
    }
    ctx := context.Background()
    tokenInfo, err := validator.ValidateToken(ctx, string(token))
    if err != nil {
        log.Fatalf("Token validation failed: %v", err)
    }
    fmt.Printf("Token is valid!\n")
    fmt.Printf("Subject: %s\n", tokenInfo.Subject)
    fmt.Printf("Issuer: %s\n", tokenInfo.Issuer)
    fmt.Printf("Audience: %v\n", tokenInfo.Audience)
}
func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}