Go
Protect a Go API with Base IdP — verify-only middleware, scope checks, and net/http integration.
A Go API behind a mobile or SPA frontend is verify-only. It checks the token, reads the user, and serves the request. No secret. No required env.
This guide walks the integration end to end in Go's standard net/http,
which works directly with the Go SDK's middleware. Chi, gorilla/mux, Gin,
and Echo all wrap net/http and accept the same middleware shape.
Scaffold
npx base-idp create --stack goThis writes auth/baseidp.go, a .env (commented placeholder), and a setup
readme into ./base-idp-go/. Move the Go file into your project.
Install
go get github.com/squareexp/base-idp/sdk/goNo required env
# Verify-only. Nothing required.
# Optional: pin the audience.
# BASE_IDP_CLIENT_ID=sq_live_yourappFor multi-tenant environments, setting BASE_IDP_CLIENT_ID so the SDK
pins the audience is worth it.
The auth package
package auth
import (
"net/http"
baseidp "github.com/squareexp/base-idp/sdk/go"
)
var client = baseidp.MustNew(baseidp.ConfigFromEnv())
// Protect wraps a handler so only requests with a valid Base IdP token pass.
func Protect(next http.Handler) http.Handler {
return client.RequireAuth(next, baseidp.MiddlewareOptions{})
}That is the whole package. baseidp.MustNew(baseidp.ConfigFromEnv()) builds
the client from env. RequireAuth returns a handler that verifies the
bearer token and writes 401 if anything fails.
Mounting on routes
net/http
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
baseidp "github.com/squareexp/base-idp/sdk/go"
"example.com/api/auth"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
})
mux.Handle("/v1/me", auth.Protect(http.HandlerFunc(handleMe)))
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
func handleMe(w http.ResponseWriter, r *http.Request) {
p, _ := baseidp.PrincipalFromContext(r.Context())
json.NewEncoder(w).Encode(map[string]any{
"id": p.Subject,
"email": p.Email,
"name": p.DisplayName,
})
}Chi
import "github.com/go-chi/chi/v5"
r := chi.NewRouter()
r.Get("/healthz", healthz)
r.Group(func(r chi.Router) {
r.Use(func(next http.Handler) http.Handler {
return auth.Protect(next)
})
r.Get("/v1/me", handleMe)
})
http.ListenAndServe(":8080", r)Gin
import "github.com/gin-gonic/gin"
r := gin.Default()
r.GET("/healthz", func(c *gin.Context) { c.String(200, "ok") })
protected := r.Group("/v1")
protected.Use(func(c *gin.Context) {
auth.Protect(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Request = r
c.Next()
})).ServeHTTP(c.Writer, c.Request)
})
protected.GET("/me", handleMe)Reading the principal
Inside a handler, pull the principal off the context.
func handleMe(w http.ResponseWriter, r *http.Request) {
p, ok := baseidp.PrincipalFromContext(r.Context())
if !ok {
http.Error(w, "no principal", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "user %s (%s)", p.Subject, p.Email)
}The _, ok shape is defensive but useful — if a route is mistakenly mounted
without Protect, you fail closed instead of nil-dereferencing.
Scope enforcement
The middleware can be configured to require a scope.
mux.Handle("/admin", client.RequireAuth(adminHandler, baseidp.MiddlewareOptions{
VerifyOptions: baseidp.VerifyOptions{
RequiredScope: "admin",
},
}))A token without admin in its scopes is rejected with 403.
For more flexible per-route scope checks, do it in the handler:
func handleAdmin(w http.ResponseWriter, r *http.Request) {
p, _ := baseidp.PrincipalFromContext(r.Context())
if !contains(p.Scopes, "admin") {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
// ...
}
func contains(list []string, target string) bool {
for _, s := range list {
if s == target {
return true
}
}
return false
}A mobile-to-backend exchange endpoint
A common pattern: your Flutter or React Native app POSTs the session it got from the mobile SDK, and your Go backend upserts a user and issues its own session tokens.
package auth
import (
"encoding/json"
"net/http"
baseidp "github.com/squareexp/base-idp/sdk/go"
)
type ExchangeRequest struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type ExchangeResponse struct {
UserID string `json:"user_id"`
SessionToken string `json:"session_token"`
}
func HandleExchange(w http.ResponseWriter, r *http.Request) {
var req ExchangeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
principal, err := client.VerifyAccessToken(r.Context(), req.AccessToken, baseidp.VerifyOptions{})
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
user, err := upsertUser(r.Context(), principal)
if err != nil {
http.Error(w, "internal", http.StatusInternalServerError)
return
}
sessionToken, err := mintSession(user.ID)
if err != nil {
http.Error(w, "internal", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(ExchangeResponse{
UserID: user.ID,
SessionToken: sessionToken,
})
}This endpoint is public (no middleware), because the Base IdP access token itself is what authorizes the request. The middleware would not let mobile apps in if it required your own session token.
A complete verify-only service
package main
import (
"encoding/json"
"log"
"net/http"
"os"
baseidp "github.com/squareexp/base-idp/sdk/go"
"example.com/api/auth"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
mux.HandleFunc("/v1/auth/idp-exchange", auth.HandleExchange)
mux.Handle("/v1/me", auth.Protect(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p, _ := baseidp.PrincipalFromContext(r.Context())
json.NewEncoder(w).Encode(map[string]any{
"id": p.Subject,
"email": p.Email,
})
})))
mux.Handle("/v1/admin", auth.Protect(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p, _ := baseidp.PrincipalFromContext(r.Context())
if !containsScope(p.Scopes, "admin") {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
w.Write([]byte(`{"status":"ok"}`))
})))
addr := ":" + envOr("PORT", "8080")
log.Printf("listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}
func containsScope(list []string, target string) bool {
for _, s := range list {
if s == target {
return true
}
}
return false
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}Three routes — health, exchange, protected — and a small scope helper. That is a full production-shape Go API.
Common issues
signing key missing on startup
The SDK could not reach the keys endpoint. Check network. Run
npx base-idp test.
401 invalid token for every request
Check the bearer token by decoding it: npx base-idp token <token>. Look at
aud, iss, exp. If aud does not match the client id you pinned, the
token is for a different app.
Slow first request
The SDK fetches keys on first use. For warm-up, call
client.PublicKeys(ctx, false) at startup.