Skip to main content

Go

Same patterns as Python. Code is shorter because the JWT story is cleaner.

Most Go services are high-throughput; per-request HTTP calls to IdentSphere add latency you don't want. Validate JWTs locally instead.

go get github.com/golang-jwt/jwt/v5 github.com/MicahParks/keyfunc/v3
package main

import (
"context"
"fmt"
"net/http"
"os"
"time"

"github.com/MicahParks/keyfunc/v3"
"github.com/golang-jwt/jwt/v5"
)

var jwksURL = os.Getenv("IDENTSPHERE_URL") + "/.well-known/jwks.json"

func main() {
// Auto-refreshing JWKS cache
jwks, err := keyfunc.NewDefault([]string{jwksURL})
if err != nil {
panic(err)
}

mux := http.NewServeMux()
mux.Handle("/api/me", authMiddleware(jwks)(http.HandlerFunc(me)))
http.ListenAndServe(":8080", mux)
}

func authMiddleware(jwks keyfunc.Keyfunc) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("identsphere_at")
if err != nil {
http.Error(w, "unauthenticated", http.StatusUnauthorized)
return
}
tok, err := jwt.Parse(cookie.Value, jwks.Keyfunc)
if err != nil || !tok.Valid {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
claims := tok.Claims.(jwt.MapClaims)
ctx := context.WithValue(r.Context(), "user", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

func me(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("user").(jwt.MapClaims)
fmt.Fprintf(w, "user=%s org=%s", claims["sub"], claims["org"])
}

That's a complete working auth-validating Go server in ~50 lines.

Pattern 1 — Proxy through IdentSphere

For login / signup / etc., forward to IdentSphere:

func loginHandler(w http.ResponseWriter, r *http.Request) {
req, _ := http.NewRequestWithContext(r.Context(), "POST",
os.Getenv("IDENTSPHERE_URL")+"/v1/auth/login", r.Body)
req.Header = r.Header.Clone()
resp, err := http.DefaultClient.Do(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
// forward cookies
for _, c := range resp.Cookies() {
http.SetCookie(w, c)
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}

Chi router

import "github.com/go-chi/chi/v5"

r := chi.NewRouter()
r.Group(func(r chi.Router) {
r.Use(authMiddlewareChi(jwks))
r.Get("/api/me", me)
})

Gin

Same pattern — wrap jwks.Keyfunc in gin.HandlerFunc. Sample in the examples/gin-go-starter/ directory (coming v1.1).

Verify tokens in your backend

This is Path 2 — per-request authorization: the RS256 access token is validated locally against the cached JWKS, no network hop per request. The published helper at github.com/IdentSphere/sdks/go/verify wraps the whole thing — JWKS caching, RS256 pinning, and iss / exp (and aud) checks — plus a net/http middleware. See that package's README for the full API; the shape is:

go get github.com/IdentSphere/sdks/go/verify
import "github.com/IdentSphere/sdks/go/verify"

v, err := verify.NewVerifier(verify.Config{
JWKSURL: os.Getenv("IDENTSPHERE_URL") + "/.well-known/jwks.json",
Issuer: "IdentSphere", // IDENTSPHERE_ISSUER
Audience: os.Getenv("IDENTSPHERE_TOKEN_AUDIENCE"), // optional; "" skips the check
})
if err != nil { panic(err) }

// Per request:
claims, err := v.Verify(tokenString) // claims.Sub, claims.Org, claims.Aal, ...

// Or as middleware:
mux.Handle("/api/me", v.Middleware(http.HandlerFunc(me)))

The underlying libraries

The helper is built on github.com/MicahParks/keyfunc/v3 (the auto-refreshing JWKS cache) and github.com/golang-jwt/jwt/v5. If you'd rather wire it yourself, pin RS256 with WithValidMethods, require exp with WithExpirationRequired, and check the issuer with WithIssuer:

jwks, _ := keyfunc.NewDefault([]string{jwksURL})

tok, err := jwt.Parse(tokenString, jwks.Keyfunc,
jwt.WithValidMethods([]string{"RS256"}), // never accept 'none' / HS256
jwt.WithExpirationRequired(), // reject tokens with no/expired exp
jwt.WithIssuer("IdentSphere"), // verify iss
// jwt.WithAudience(os.Getenv("IDENTSPHERE_TOKEN_AUDIENCE")), // if configured
)
if err != nil || !tok.Valid {
// fail closed
}
claims := tok.Claims.(jwt.MapClaims) // claims["sub"], claims["org"], claims["aal"]

keyfunc.NewDefault caches the key set and refreshes it in the background, so jwt.Parse runs entirely in-process.

Caveat: revocation lag

See the Python notes on revocation lag. Same trade-off applies in Go.