Drop-in authentication, authorization, and entitlements for your Go backend.
Validate Frontegg JWTs, guard your routes, check feature entitlements, and call the Frontegg API β in a few lines of idiomatic Go.
Full guide Β· Quickstart Β· Guides Β· Frontegg docs Β· Report a bug
Frontegg gives SaaS teams production-grade auth, user management, and entitlements out of the box. This SDK brings that to your Go services with first-class Go ergonomics:
- π Auth in ~5 lines β guard any
net/httproute withWithAuthentication. Bearer JWTs and API keys, roles, permissions, and step-up MFA all handled. - ποΈ Entitlements at the edge β evaluate feature flags and plan rules locally, in-memory, with zero per-check network calls. A faithful port of Frontegg's entitlements engine.
- β‘ Built for production β goroutine-safe,
context.Context-aware, automatic token refresh, pluggable in-memory or Redis caching. - π§© Idiomatic & unsurprising β errors as values (
errors.Is/errors.As), functional options, standard-library HTTP. No magic, no globals you didn't ask for. - β Battle-tested β ~89% test coverage, race-tested, CI on every push.
Idiomatic Go counterpart to the official
@frontegg/clientNode SDK, with feature parity.
go get github.com/frontegg/go-sdkRequires Go 1.24+.
Protect a route in one snippet β initialize once, then wrap any handler:
package main
import (
"net/http"
"github.com/frontegg/go-sdk"
"github.com/frontegg/go-sdk/middleware"
)
func main() {
// Initialize the package-level client once at startup.
frontegg.Init(frontegg.Credentials{
ClientID: "<YOUR_CLIENT_ID>",
APIKey: "<YOUR_API_KEY>",
})
// Guard a route: requires a valid token with the "admin" role.
protected := frontegg.WithAuthentication(middleware.Options{
Roles: []string{"admin"},
})
mux := http.NewServeMux()
mux.Handle("/admin", protected(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, _ := middleware.UserFromContext(r.Context())
// user.ID(), user.Email, user.TenantID, user.Roles, user.Permissions β¦
_, _ = w.Write([]byte("hello " + user.Email))
})))
_ = http.ListenAndServe(":8080", mux)
}The middleware reads the token from the Authorization: Bearer β¦ header or the x-api-key header, validates the signature and claims, enforces any required roles/permissions, and puts the decoded user on the request context. Unauthorized requests get 401; insufficient role/permission gets 403.
| Capability | Package | What it does |
|---|---|---|
| Auth middleware | middleware |
net/http guard for Bearer JWT / API-key auth, roles & permissions |
| Identity | identity |
Validate JWTs and access tokens, roles/permissions, step-up MFA |
| Entitlements | entitlements |
Local feature-flag & plan evaluation with background snapshot refresh |
| Hosted login | hostedlogin |
OAuth 2.1 authorize URL + PKCE code exchange |
| Audit logs | audits |
Send and query Managed Audit Logs |
| Events | events |
Trigger Frontegg events and read delivery status |
| REST client | httpclient |
Authenticated client for the full Frontegg API |
| M2M auth | authenticator |
Vendor token with automatic refresh |
| Caching | cache, cache/redisstore |
In-memory (default) or Redis-backed token cache |
| Entry point | frontegg |
One Client that builds all of the above |
Design principles: errors are values (errors.Is/errors.As), every network call takes a context.Context, all clients are safe for concurrent use, and configuration is read from the standard FRONTEGG_* environment variables.
π Prefer a single end-to-end walkthrough? See the full integration guide.
WithAuthentication works with the standard library and any router built on it (chi, gorilla, http.ServeMux, β¦):
guard := frontegg.WithAuthentication(middleware.Options{
Roles: []string{"admin", "owner"}, // any one is sufficient
Permissions: []string{"fe.secure.read"}, // any one is sufficient
})
mux.Handle("/reports", guard(reportsHandler))Need a dedicated client instead of the package-level default? Build one and pass its identity validator:
c := frontegg.New(frontegg.Credentials{ClientID: id, APIKey: key})
guard := middleware.WithAuthentication(c.Identity(), middleware.Options{Roles: []string{"admin"}})c := frontegg.New(frontegg.Credentials{ClientID: id, APIKey: key})
ident := c.Identity()
user, err := ident.ValidateToken(ctx, bearerToken, &identity.ValidateTokenOptions{
Roles: []string{"admin"},
Permissions: []string{"fe.secure.read"},
WithRolesAndPermissions: true, // hydrate roles/permissions
StepUp: &identity.StepUpOptions{MaxAge: 3600}, // require step-up MFA
}, identity.JWTHeader)
if err != nil {
// errors.Is(err, identity.ErrInsufficientRole), etc.
}Evaluate feature and permission entitlements locally β no network round-trip per check. The client keeps an in-memory snapshot fresh in the background.
ent := c.Entitlements()
if err := ent.Start(ctx); err != nil {
log.Fatal(err)
}
defer ent.Close()
if err := ent.Ready(ctx); err != nil { // wait for the first snapshot
log.Fatal(err)
}
// Scope to a user/tenant straight from their token β¦
scoped, err := ent.ForFronteggToken(ctx, token)
// β¦ or from an already-validated entity:
// scoped := ent.ForUser(entity)
if res := scoped.IsEntitledToFeature(ctx, "advanced-analytics", nil); res.Result {
// entitled
} else {
log.Printf("not entitled: %s", res.Justification) // missing-feature | bundle-expired | β¦
}
// Permissions, or the unified entry point:
_ = scoped.IsEntitledToPermission(ctx, "fe.secure.read", nil)
_, _ = scoped.IsEntitledTo(ctx, "advanced-analytics", "", nil)The hosted-login flow uses PKCE, as required by OAuth 2.1. RequestAuthorize returns a code_verifier you must persist (e.g. in the user's session, keyed by state) and pass back to CodeExchange.
hl := c.HostedLogin("https://app.acme.com/oauth/callback")
// 1. Build the redirect and stash the verifier.
authReq, err := hl.RequestAuthorize(ctx, "csrf-state-token")
// β redirect the user to authReq.URL
// β save authReq.CodeVerifier, keyed by authReq.State
// 2. On the callback (?code=β¦&state=β¦), exchange the code.
res, err := hl.CodeExchange(ctx, code, state, savedCodeVerifier)
// res.User, res.AccessToken, res.RefreshTokenThe
redirect_urimust exactly match an allowed callback configured on your Frontegg application.
auth := c.NewAuthenticator()
if err := auth.Init(ctx, id, key); err != nil {
log.Fatal(err)
}
api := c.HTTPClient(auth, httpclient.WithBaseURL("https://api.frontegg.com"))
resp, err := api.Post(ctx, "identity/resources/auth/v1/user",
map[string]string{"email": "john@acme.com", "password": "β¦"},
map[string]string{"frontegg-vendor-host": "acme.frontegg"}, // optional per-request headers
)
// resp.StatusCode, resp.JSON(&v)The client injects the vendor x-access-token on every request and refreshes it automatically before expiry.
// Managed Audit Logs
audits := c.Audits()
_ = audits.Init(ctx, "<CLIENT_ID>", "<AUDITS_KEY>")
_ = audits.SendAudit(ctx, audits.SendAuditParams{
TenantID: "my-tenant",
Severity: audits.SeverityMedium,
Fields: map[string]any{"user": "info@frontegg.com", "action": "Login", "ip": "1.2.3.4"},
})
page, _ := audits.GetAudits(ctx, audits.GetAuditsParams{TenantID: "my-tenant", Offset: 0, Count: 50})
// Events
ev := c.Events(auth)
id, _ := ev.Send(ctx, "my-tenant", events.EventTrigger{
EventKey: "user.invited",
Data: events.EventProperties{Title: "You're invited", Description: "Join the team"},
})
status, _ := ev.GetStatus(ctx, id)The in-memory cache is the default. To share a cache across instances, use the Redis backend (only consumers who import it pull in go-redis):
import (
"github.com/redis/go-redis/v9"
"github.com/frontegg/go-sdk/cache/redisstore"
)
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
store := redisstore.New[MyType](rdb) // satisfies cache.Cache[MyType]Credentials are passed explicitly, or fall back to the environment. Service URLs default to the public Frontegg gateway and can be overridden β handy for EU/regional or self-hosted deployments.
| Variable | Default | Purpose |
|---|---|---|
FRONTEGG_CLIENT_ID |
β | Vendor client ID (fallback when not passed in code) |
FRONTEGG_API_KEY |
β | Vendor API key (fallback when not passed in code) |
FRONTEGG_API_GATEWAY_URL |
https://api.frontegg.com |
Base URL for all services |
FRONTEGG_AUTHENTICATOR_NUMBER_OF_TRIES |
3 |
Auth retry attempts |
FRONTEGG_IDENTITY_SERVICE_URL |
<base>/identity |
Override the identity service |
FRONTEGG_ENTITLEMENTS_SERVICE_URL |
<base>/entitlements |
Override the entitlements service |
Per-service overrides also exist for audits, events, metadata, vendors, and OAuth (FRONTEGG_*_SERVICE_URL). See config.
Errors are typed and inspectable. Identity failures carry an HTTP status and match sentinels:
_, err := ident.ValidateToken(ctx, token, opts, identity.JWTHeader)
switch {
case errors.Is(err, identity.ErrInsufficientRole): // 403
case errors.Is(err, identity.ErrInsufficientPermission): // 403
case errors.Is(err, identity.ErrFailedToAuthenticate): // 401
}
var sce *identity.StatusCodeError
if errors.As(err, &sce) {
http.Error(w, sce.Message, sce.StatusCode)
}go test ./... # unit tests (no network β fully stubbed with httptest)
go test -race ./... # race detector
go test -tags e2e ./... # end-to-end against a real tenant (see .env.e2e.example)Issues and pull requests are welcome. Before opening a PR:
gofmt -l . # must be clean
go vet ./... # must pass
go test ./... # must passMIT Β© Frontegg