Go
The baseidp Go package — offline PASETO verification, net/http middleware, and a typed principal.
The Go SDK is what you reach for when your backend is a Go service sitting
behind a frontend. It verifies Base IdP tokens offline against published
public keys, ships a net/http middleware, and returns a typed principal
your handlers can read. Verify-only services do not need any Base IdP env at
all.
The package lives under the main Base IdP repo, exported as a Go module.
There is nothing to set up beyond go get.
Install
go get github.com/squareexp/base-idp/sdk/goImport it as baseidp:
import baseidp "github.com/squareexp/base-idp/sdk/go"Quick start
A verify-only service looks like this in full:
package main
import (
"fmt"
"log"
"net/http"
baseidp "github.com/squareexp/base-idp/sdk/go"
)
func main() {
client := baseidp.MustNew(baseidp.ConfigFromEnv())
mux := http.NewServeMux()
mux.Handle("/me", client.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p, _ := baseidp.PrincipalFromContext(r.Context())
fmt.Fprintf(w, "Hello %s (%s)\n", p.DisplayName, p.Email)
}), baseidp.MiddlewareOptions{}))
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}That is the whole integration. The middleware verifies the bearer token, attaches the principal to the request context, and rejects everything that fails verification with a 401.
Building a client
Config from env
ConfigFromEnv is the idiomatic constructor. It reads BASE_IDP_CLIENT_ID
if present, falls back to defaults otherwise, and produces a ready-to-use
Config.
client := baseidp.MustNew(baseidp.ConfigFromEnv())Config by hand
You can also construct the config directly if you need to.
client, err := baseidp.New(baseidp.Config{
ClientID: "sq_live_yourapp",
RequiredScope: "tasks:read",
ClockSkew: 30 * time.Second,
KeyCacheTTL: 10 * time.Minute,
})
if err != nil {
log.Fatal(err)
}Most fields have sensible defaults. The ones you might tune in production:
| Field | Default | When to change |
|---|---|---|
ClientID | empty | Set to pin the audience. |
RequiredScope | empty | Reject tokens without this scope. |
ClockSkew | 30s | Tolerance for clock drift between hosts. |
KeyCacheTTL | 10m | How long to cache the public-key set. |
HTTPClient | http.DefaultClient | Override for custom transports, timeouts, or test fakes. |
Middleware
Middleware
The plain middleware wraps a handler and only invokes the next handler if the token is valid. On failure it writes a 401.
mux.Handle("/me", client.Middleware(baseidp.MiddlewareOptions{})(handler))RequireAuth
A shortcut that takes the next handler directly. This is the form most projects use.
mux.Handle("/me", client.RequireAuth(handler, baseidp.MiddlewareOptions{}))MiddlewareOptions
type MiddlewareOptions struct {
VerifyOptions
CookieName string // alternative source for the token
ErrorHandler func(http.ResponseWriter, *http.Request, error) // custom error response
}The VerifyOptions are embedded, so you can set RequiredScope, an explicit
Audience, or a MaxClockSkewSeconds per-route if you need different rules
for different endpoints.
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)
}Verifying tokens directly
If middleware is the wrong shape for your codebase (a gRPC interceptor, a
custom auth layer, a worker that processes queued jobs with embedded tokens),
call VerifyAccessToken directly.
ctx := r.Context()
principal, err := client.VerifyAccessToken(ctx, token, baseidp.VerifyOptions{
RequiredScope: "tasks:read",
})
if err != nil {
return fmt.Errorf("token rejected: %w", err)
}The Principal struct
type Principal struct {
ID string // gid
Subject string // sub
Email string
DisplayName string
Scopes []string
Role string
SessionID string
AssuranceLevel int
Audience string
Issuer string
ExpiresAt time.Time
IssuedAt time.Time
}Reach for the fields you need. Subject and Email are the most common.
Running the full login (confidential)
If your Go service runs the login itself — a server-rendered web app, a Hugo-style frontend with a Go backend — pass the secret and use the authorize and exchange helpers.
client := baseidp.MustNew(baseidp.Config{
ClientID: "sq_live_web",
Secret: os.Getenv("BASE_IDP_CLIENT_SECRET"),
RedirectURI: "https://app.example.com/auth/callback",
})
// 1. Build the authorize URL.
url, err := client.AuthorizeURL(baseidp.AuthorizeOptions{
State: "return-to=/dashboard",
})
http.Redirect(w, r, url, http.StatusFound)// 2. Handle the callback.
code := r.URL.Query().Get("code")
tokens, err := client.ExchangeCode(r.Context(), baseidp.TokenOptions{
Code: code,
CodeVerifier: storedVerifier,
})// 3. Verify the access token.
principal, err := client.VerifyAccessToken(r.Context(), tokens.AccessToken, baseidp.VerifyOptions{})Refresh
tokens, err := client.Refresh(ctx, baseidp.RefreshOptions{
RefreshToken: stored.RefreshToken,
})The refresh token rotates on each call — the response always carries a new refresh token, and the old one is invalidated.
Discovery and keys
The SDK fetches the discovery document and the public keys automatically. You can also call them directly for diagnostics.
metadata, err := client.Discovery(ctx, false)
keys, err := client.PublicKeys(ctx, false)Pass true to force a refresh that bypasses the cache.
A complete API example
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
baseidp "github.com/squareexp/base-idp/sdk/go"
)
func main() {
client := baseidp.MustNew(baseidp.ConfigFromEnv())
mux := http.NewServeMux()
mux.Handle("/v1/me", client.RequireAuth(
http.HandlerFunc(handleMe),
baseidp.MiddlewareOptions{},
))
mux.Handle("/v1/admin", client.RequireAuth(
http.HandlerFunc(handleAdmin),
baseidp.MiddlewareOptions{
VerifyOptions: baseidp.VerifyOptions{
RequiredScope: "admin",
},
},
))
addr := ":" + getenvOr("PORT", "8080")
log.Printf("listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, 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,
})
}
func handleAdmin(w http.ResponseWriter, r *http.Request) {
_, _ = baseidp.PrincipalFromContext(r.Context())
w.Write([]byte(`{"status":"ok"}`))
}
func getenvOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}A verify-only Go API with two routes, scope enforcement on the admin route, and zero Base IdP env required. That is the production shape for most services.