Skip to content
Trilha
Chapters

Reference

auth

Provider, Options, Auth, User and Store — the API of the auth package, with the defaults and what each field changes.

import "github.com/emersonjoe/trilha/auth" — OpenID Connect login with the standard library. The package registers no route: it exposes handlers that your app/ publishes.

Providers#

func OIDC(issuer, clientID, clientSecret, redirectURL string) *Provider
func EntraID(tenant, clientID, clientSecret, redirectURL string) *Provider
func Keycloak(baseURL, realm, clientID, clientSecret, redirectURL string) *Provider
func Cognito(region, userPoolID, clientID, clientSecret, redirectURL string) *Provider
func Clerk(frontendAPI, clientID, clientSecret, redirectURL string) *Provider
ConstructorResulting issuerRoles read from
OIDCwhatever you passroles, groups
EntraIDhttps://login.microsoftonline.com/<tenant>/v2.0roles, groups, wids
Keycloak<baseURL>/realms/<realm>realm_access.roles, resource_access[clientID].roles
Cognitohttps://cognito-idp.<region>.amazonaws.com/<userPoolID>cognito:groups
Clerkthe Frontend API URL, normalized (https://<slug>.clerk.accounts.dev)roles, groups — Clerk's id_token carries the organization (org_id), not the role in it; a configured claim goes in Options.RoleClaims

Provider.LogoutDomain exists for Cognito: set it to the managed login domain (<prefix>.auth.<region>.amazoncognito.com, or your own) and Logout redirects to /logout?client_id=…&logout_uri=… there; the return URL must be in the app client's Allowed sign-out URLs. Left empty, Logout clears the local session, says so in the log and does not pretend it federated. Other providers ignore the field. Clerk publishes no end_session_endpoint either, and has no equivalent address: there Logout is always local, and the log says the Clerk session was left open.

Provider.HTTPClient swaps the HTTP client (default: 10 s timeout). Discovery happens on first use and is valid for one hour; an issuer that differs between the configuration and the document is an error, not a warning.

Options#

FieldDefaultWhat it does
Scopes []stringopenid profile emailscopes requested from the provider
Absolute time.Duration8 hmaximum session lifetime, counted from the login
Idle time.Duration30 minends an idle session; IdleOff: true disables it
CookieName stringtrilha_sessionsession cookie name
LoginPath string/entrarwhere Require sends an anonymous browser
AfterLogin string/destination after the callback, when there is no next
AfterLogout string/destination after the logout
RoleClaims []stringadditional claims to read roles from
Store Storenilpersists the session; nil = signed cookie, stateless

Auth#

func New(p *Provider, o Options) *Auth      // no network
func (a *Auth) Start(c *trilha.Ctx) error   // → provider (PKCE, state, nonce)
func (a *Auth) Callback(c *trilha.Ctx) error // validates the callback and creates the session
func (a *Auth) Logout(c *trilha.Ctx) error   // deletes the session; RP-Initiated Logout when available
func (a *Auth) Require() trilha.MiddlewareFunc
func (a *Auth) RequireRole(roles ...string) trilha.MiddlewareFunc
func (a *Auth) Optional() trilha.MiddlewareFunc
func (a *Auth) User(c *trilha.Ctx) *User     // nil when anonymous
func (a *Auth) Session(c *trilha.Ctx) (*User, error)

Require answers 302 to the login when the request is a navigation (Accept with text/html, outside /api/) and 401 otherwise. RequireRole answers 403 to someone authenticated without the role. One of the listed roles is enough; the comparison ignores case.

User#

type User struct {
	Subject   string    // sub: the stable identifier
	Email     string    // email, or preferred_username when there is none
	Name      string
	Roles     []string
	IssuedAt  time.Time // moment of the login
	ExpiresAt time.Time
	Seen      time.Time // last activity (idle window)
	SessionID string    // changes on every login
}

func (u *User) HasRole(role string) bool

Store#

type Store interface {
	Save(id string, u *User, ttl time.Duration) error
	Load(id string) (*User, bool)
	Delete(id string) error
}

func NewMemoryStore() *MemoryStore

With a Store the cookie carries only the identifier and the logout takes effect immediately for everyone. MemoryStore is for a single process: replicas do not share it, and a restart drops every session. For several replicas, implement the interface over your database or cache.

Cookies#

CookieLifetimeContent
trilha_oidc_state10 minstate of the request in progress
trilha_oidc_nonce10 minnonce of the request in progress
trilha_oidc_verifier10 minPKCE verifier
trilha_oidc_next10 mindestination after the login (relative path only)
trilha_sessionAbsolutethe session (or its id, with a Store)

All are signed (they require TRILHA_SECRET), HttpOnly, SameSite=Lax and Secure under HTTPS. The four flow cookies are deleted on the callback, whether it succeeds or not.

Accepted algorithms#

RS256, RS384, RS512, ES256, ES384. The list is fixed: the token's alg chooses nothing. RSA keys with a modulus smaller than 2048 bits are ignored in the JWKS, kid is required, and clock tolerance is 60 seconds.

Audit#

trilha audit checks, when the project imports trilha/auth: client secret written in the code (critical) and redirect_uri over http:// outside localhost (critical).