Skip to content

Go Backend Example

This page shows a complete, production-ready Go implementation for verifying SpartanAuth tokens in a service that uses grpc-gateway.

SpartanAuth uses protobuf JSON encoding, which serializes int64 fields (exp, iat) as quoted strings. Use the ",string" JSON tag to handle this correctly:

// IntrospectResponse represents the JSON response from SpartanAuth.
// Note: exp and iat are quoted strings due to protobuf JSON encoding of int64.
type IntrospectResponse struct {
Sub string `json:"sub"`
Username string `json:"username"`
SectorID string `json:"sectorID"`
IsAdmin bool `json:"isAdmin"`
Exp int64 `json:"exp,string"`
Iat int64 `json:"iat,string"`
}
// introspectToken calls SpartanAuth to validate a bearer token.
// Returns (nil, nil) when the token is rejected (non-200 response).
// Returns (nil, err) only for network or decode failures.
func introspectToken(ctx context.Context, token string) (*IntrospectResponse, error) {
reqBody := `{"token":"` + token + `"}`
req, err := http.NewRequestWithContext(
ctx, http.MethodPost,
"https://api.spartanauth.com/api/v1/introspect",
strings.NewReader(reqBody),
)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, nil // token rejected
}
var result IntrospectResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode introspection response: %w", err)
}
return &result, nil
}
// Context keys — use typed strings to avoid collisions
type contextKey string
const (
ctxSub contextKey = "sub"
ctxSectorID contextKey = "sectorID"
ctxIsAdmin contextKey = "isAdmin"
)
// publicMethods lists gRPC methods that do not require authentication.
publicMethods := map[string]bool{
pb.MyService_SomePublicMethod_FullMethodName: true,
}
grpcServer := grpc.NewServer(
grpc.ChainUnaryInterceptor(
func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if publicMethods[info.FullMethod] {
return handler(ctx, req)
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
}
authValues := md.Get("authorization")
if len(authValues) == 0 {
return nil, status.Errorf(codes.Unauthenticated, "missing authorization header")
}
token := strings.TrimPrefix(authValues[0], "Bearer ")
identity, err := introspectToken(ctx, token)
if err != nil {
return nil, status.Errorf(codes.Internal, "authentication error")
}
if identity == nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
}
// Store identity in context for use by handlers
ctx = context.WithValue(ctx, ctxSub, identity.Sub)
ctx = context.WithValue(ctx, ctxSectorID, identity.SectorID)
ctx = context.WithValue(ctx, ctxIsAdmin, identity.IsAdmin)
return handler(ctx, req)
},
),
)
func (s *server) GetProfile(ctx context.Context, req *pb.GetProfileRequest) (*pb.GetProfileResponse, error) {
sub, ok := ctx.Value(ctxSub).(string)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "missing identity")
}
// sub is the user's stable identifier — use it as the foreign key
profile, err := s.db.GetProfileBySub(ctx, sub)
// ...
}

grpc-gateway automatically forwards the HTTP Authorization header to gRPC metadata when the client sends Authorization: Bearer <token>. No extra configuration is needed.

import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)