authletas

package
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 14, 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