Go Backend Example
This page shows a complete, production-ready Go implementation for verifying SpartanAuth tokens in a service that uses grpc-gateway.
The introspection response type
Section titled “The introspection response type”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"`}The introspection function
Section titled “The introspection function”// 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}Wiring into a gRPC unary interceptor
Section titled “Wiring into a gRPC unary interceptor”// Context keys — use typed strings to avoid collisionstype 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) }, ),)Using identity in handlers
Section titled “Using identity in handlers”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) // ...}Token forwarding with grpc-gateway
Section titled “Token forwarding with grpc-gateway”grpc-gateway automatically forwards the HTTP Authorization header to gRPC metadata when the client sends Authorization: Bearer <token>. No extra configuration is needed.
Required imports
Section titled “Required imports”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")