Documentation
¶
Overview ¶
Package authletas wires authlet into memory-system: UserResolver, AS construction, JWKS/PRM handlers, and dual-auth middleware.
Index ¶
Constants ¶
const (
// PathPrefix is where the AS handler is mounted under the public base URL.
PathPrefix = "/oauth"
)
const ResolverRejectEvent = "authletas.resolver.reject"
ResolverRejectEvent is the stable structured-log event key emitted on every resolver rejection; operators aggregate on it and read `reason`.
Variables ¶
var ErrProvisionNotAllowed = errors.New("authletas: provision not allowed")
ErrProvisionNotAllowed is the sentinel a Provision callback returns when the verified claims are not permitted to auto-provision a tenant. The composition root translates the service's not-allowed error into this sentinel before it reaches Resolve, which then emits the standard reject (reason=not_allowed) and returns ErrUnauthorized (a 403, never a leaked internal error).
ErrUnauthorized is returned when an upstream Google identity has no matching tenant and no Provision callback resolves one. Without a Provision hook the resolver never auto-provisions from federated claims — a tenant_users row must already exist.
Functions ¶
This section is empty.
Types ¶
type MemoryUserResolver ¶
type MemoryUserResolver struct {
DB *gorm.DB
// Logger receives the rejection events; nil falls back to slog.Default.
Logger *slog.Logger
// Provision, when non-nil, is called on a tenant_users miss to
// provision-and-resolve a tenant for the verified claims, returning the new
// tenant id. nil ⇒ no auto-provision (preserve today's reject: a missing
// tenant_users row yields ErrUnauthorized). The composition root sets this
// post-Setup; it must translate the service's not-allowed error into
// ErrProvisionNotAllowed so Resolve can reject with reason=not_allowed.
Provision func(ctx context.Context, c idp.Claims) (string, error)
}
MemoryUserResolver maps upstream Google OIDC claims to a tenant UUID via the verified email in tenant_users. Email is the trust anchor — empty or unverified emails are refused regardless of DB state.
func (*MemoryUserResolver) Resolve ¶
Resolve returns the per-user tenant_users.id (the unified authorization subject) for the upstream Google email — NOT the tenant_id, so that co-tenants in a shared tenant map to distinct subjects. It returns ErrUnauthorized for empty/unverified emails (never trust unverified claims) and for any DB miss or error (don't leak DB state as auth decisions); the AS maps all to a 403 at /idp/callback. Every rejection emits one slog entry (event=ResolverRejectEvent + `reason`): DB errors at Warn, the rest at Info.
type Wiring ¶
type Wiring struct {
// AS is the assembled authorization server (Handler() serves /authorize,
// /token, /register, /idp/callback, /revoke, /userinfo).
AS *as.AS
// Resolver is the MemoryUserResolver wired into the AS. The composition
// root sets wiring.Resolver.Provision post-Setup to enable auto-provisioning
// on a tenant_users miss (nil Provision preserves the reject-by-default
// behavior). Exposed so the callback can be injected without changing
// Setup's signature.
Resolver *MemoryUserResolver
// BearerMW wraps protected handlers (the MCP endpoint): enforces iss/aud,
// rejects with 401 + WWW-Authenticate challenge pointing at prmURL.
BearerMW func(http.Handler) http.Handler
// PRMHandler serves the Protected Resource Metadata JSON document.
PRMHandler http.HandlerFunc
// RunCleanup starts the AS cleanup goroutine; the returned channel closes
// when it exits (after ctx is canceled).
RunCleanup func(ctx context.Context) <-chan struct{}
// contains filtered or unexported fields
}
Wiring is the result of Setup — everything the main server needs to mount the AS and the bearer-protected MCP endpoint.
func Setup ¶
func Setup( ctx context.Context, db *gorm.DB, store storage.Storage, googleClientID, googleClientSecret, baseURL string, logger *slog.Logger, ) (*Wiring, error)
Setup builds the authlet AS, bearer middleware, PRM handler, and cleanup launcher; caller mounts routes with Wiring.Mount. Reads AUTHLET_MASTER_KEY from env and does synchronous Google OIDC discovery — an unreachable Google is a startup failure. The JWT `sub` is the per-user tenant_users.id (set by MemoryUserResolver). idTokenClaims/additionalClaims look the email up by that unique id, and additionalClaims also embeds a signed `tenant_id` claim so UserContextBridge can attach tenant+subject+email from the token alone — no DB read on the request path. Email is treated as verified since a tenant_users row only exists via a verified Google email, giving the JWT path parity with the API-key path. logger flows to as.Config.Logger.
func (*Wiring) DualAuth ¶
DualAuth splits requests by Authorization header shape: a JWT-looking Bearer token (three dot-separated segments) goes through the authlet bearer middleware, everything else (no header, opaque tokens, cookies, Basic) falls back to the legacy API-key middleware. Transition-window adapter: once downstreams are on OAuth, /mcp uses BearerMW directly and this (plus legacy) can be deleted.
func (*Wiring) Mount ¶
Mount registers the AS and well-known endpoints on a stdlib ServeMux; call before any sub-route that would catch /oauth/*. The AS Handler() is a chi router served under PathPrefix with the prefix stripped so its relative routes resolve.
func (*Wiring) UserContextBridge ¶
UserContextBridge translates authlet's validated JWT claims (under rs.ContextKey{}) into memory-system's auth context, so downstream handlers see a tenant/subject/email for JWT requests. Everything is read from the signed token — the bridge does no DB work. The subject is the token `sub` (tenant_users.id); the tenant and email come from the signed "tenant_id" and "email" custom claims (set in Setup's additionalClaims).
It is a no-op passthrough (original context, no auth attached) when claims are absent (legacy API-key path) or the "tenant_id" claim is missing/unparseable. That fails OPEN for unrelated bearer-shaped requests and fails SECURE for legacy pre-fix tokens (which lack the tenant_id claim), forcing re-auth rather than trusting a stale `sub`.
func (*Wiring) WWWAuth401 ¶
WWWAuth401 injects an RFC 6750 / RFC 9728 WWW-Authenticate challenge on any downstream 401 unless the inner handler already set the header. Covers the legacy API-key path (BearerMW emits its own) so first-contact MCP clients can always discover the AS.