Help us shape the future of Go! Take the 2026 Go Developer Survey

authletas

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

Documentation

Overview

Package authletas wires authlet into memory-system: UserResolver, AS construction, JWKS/PRM handlers, and dual-auth middleware.

Index

Constants

View Source
const (
	// PathPrefix is where the AS handler is mounted under the public base URL.
	PathPrefix = "/oauth"
)
View Source
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

View Source
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).

View Source
var ErrUnauthorized = errors.New("authletas: no tenant for google email")

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

func (r *MemoryUserResolver) Resolve(ctx context.Context, c idp.Claims) (string, error)

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

func (w *Wiring) DualAuth(legacy func(http.Handler) http.Handler) func(http.Handler) http.Handler

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

func (w *Wiring) Mount(mux *http.ServeMux)

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

func (w *Wiring) UserContextBridge() func(http.Handler) http.Handler

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

func (w *Wiring) WWWAuth401() func(http.Handler) http.Handler

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL