Base IdP

Base IdP

Frameworks

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 go

This 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/go

No required env

.env
# Verify-only. Nothing required.
# Optional: pin the audience.
# BASE_IDP_CLIENT_ID=sq_live_yourapp

For multi-tenant environments, setting BASE_IDP_CLIENT_ID so the SDK pins the audience is worth it.

The auth package

auth/baseidp.go
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

main.go
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.

auth/exchange.go
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

main.go
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.

Where to go next

On this page