Architecture
gosso is built around one load-bearing idea: the library owns the protocol, the consumer owns the session.
The seam
┌──────────────────┐ ┌────────────────────────────────┐ ┌──────────┐
│ your service │ │ gosso │ │ IdP │
│ │ │ │ │ │
│ ─ HTTP router │ mounts ─▶ │ saml.SP / oidc.RP │ ◀──────▶ │ │
│ │ │ │ │ │
│ ─ OnAuth cb ──── receives ── │ Subject[T] │ │ │
│ (write your │ │ │ │ │
│ own session) │ │ OnAuthenticated[T] │ │ │
│ │ │ OnLogout │ │ │
│ ─ OnLogout cb ── receives ── │ GroupMapper[R] │ │ │
└──────────────────┘ └────────────────────────────────┘ └──────────┘The library translates the SAML or OIDC round-trip into a typed Subject. The consumer decides what that authentication event means: what session to create, what roles to assign, what to audit-log, how long the session lasts, how logout destroys it.
Subject[T]
Every protocol produces the same struct, parameterised only by its protocol-specific raw payload.
type Subject[T any] struct {
ExternalID string
Email string
Firstname string
Lastname string
Groups []string
NameID string // SAML only; empty for OIDC
Raw T // saml.Payload or oidc.Payload
}You can write one session-construction helper for both IdPs and only reach into Raw for protocol-specific behaviour (e.g. stashing the OIDC RawIDToken for RP-initiated logout, or the SAML SessionIndex for SLO).
Load-bearing design choices
Stateless framework
Neither adapter ever reads or writes a session cookie on your behalf. The SAML SessionProvider under the hood returns ErrNoSession from GetSession every time on purpose — "is the user logged in?" is answered entirely by consumer code. You cannot accidentally get into a state where gosso thinks the user is logged in but your app doesn't.
Consumer-owned session
OnAuthenticated receives the http.ResponseWriter and the request. Set any cookies, issue any tokens, persist any rows you like. Return nil to let the adapter perform its post-authentication redirect, or a non-nil error to abort the login as HTTP 500.
Transit state ≠ session state
The OIDC adapter does set a short-lived signed cookie (gosso_oidc_transit, configurable) between /login and /callback to carry state, nonce and the PKCE verifier. This is protocol plumbing, not a user session — it carries no identity, exists for ~60 seconds, is scoped to the callback URL, and is deleted on first read.
Functional options, validated up-front
New(...Option) returns an error if any required option is missing or malformed. There is no zero-value SP{} or RP{} that silently works with wrong defaults.
What gosso does not do
- It does not store sessions.
- It does not enforce RBAC.
GroupMapperis a tiny convenience helper for translating raw groups into your role type; anything more lives in your app. - It does not audit-log authentication events. Log from inside
OnAuthenticatedor wrap the handler. - It does not manage token refresh. If you enabled
offline_access, the refresh token shows up inSubject.Raw.RefreshTokenand is yours to use. - It does not implement an E2E bypass handler. Write a consumer-side handler that invokes the same session-construction path and is gated behind basic auth or a build tag.