sso

package
v0.8.13 Latest Latest
Warning

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

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

Documentation

Overview

Package sso implements OIDC single sign-on for organizations (US-43.10, D17).

It owns three concerns:

  1. SSO config encryption — the IdP client secret is encrypted at rest with the server KEK (D17-S4), always decryptable with no org DEK dependency.
  2. The PKCE Authorization Code login flow (start + callback).
  3. Auto-provisioning + group-claim → role mapping applied on every login (D17-S1 + D17-S3) so IdP-driven role changes propagate on re-login.

The state/verifier pair is carried in a short-lived HMAC-signed cookie (the API is stateless, so there is no server-side session store).

Index

Constants

View Source
const DefaultStateTTL = 10 * time.Minute

DefaultStateTTL is the PKCE/state cookie lifetime.

Variables

View Source
var (
	ErrSSONotConfigured = errors.New("SSO not configured for this organization")
	ErrStateExpired     = errors.New("SSO session expired, please try again")
	ErrStateInvalid     = errors.New("invalid SSO state")
	ErrAutoProvisionOff = apierrors.NewForbiddenError("account provisioning is disabled; contact your administrator", nil)
	ErrUserSuspended    = apierrors.NewForbiddenError("account suspended", nil)
	// ErrEmailUnverified fires when the ID token's email claim is not verified
	// by the IdP. Per OIDC spec the `email` claim MUST NOT be trusted for
	// account-binding decisions (auto-provision or login-match) without
	// email_verified==true; otherwise a permissive IdP that lets a user
	// register victim@example.com unverified would let the attacker SSO into
	// the victim's existing account (US-43.10 / F8).
	ErrEmailUnverified = apierrors.NewForbiddenError("identity provider has not verified the email claim", nil)
	// ErrRedirectBaseURLNotSet fires when an SSO flow needs to build an
	// absolute callback URL but oidc.redirectBaseUrl is not configured.
	// Returned instead of deriving the URL from X-Forwarded-* / Host headers
	// (F11): those headers are attacker-influenceable at a misconfigured
	// reverse proxy, and the SSO callback URL is security-sensitive (it is
	// where the IdP redirects with the authorization code). The deployment
	// must state the canonical base URL explicitly.
	ErrRedirectBaseURLNotSet = errors.New("OIDC redirect base URL is not configured; set oidc.redirectBaseUrl")
)

Sentinel errors surfaced to handlers. Handlers map these to HTTP codes.

View Source
var ErrDNSNotMatching = errors.New("DNS TXT record does not contain the verification token")

ErrDNSNotMatching is returned when the TXT record at the verification host does not contain the org's verification token. Surfaced to handlers as a 422.

View Source
var ErrDomainNotClaimed = errors.New("domain is not in claimed domains")

ErrDomainNotClaimed is returned when VerifyDomain is called for a domain the org has not claimed. Surfaced to handlers as a 400.

View Source
var ErrNoVerificationToken = errors.New("no verification token configured; rotate one first")

ErrNoVerificationToken is returned when VerifyDomain is called but the org has no verification token configured. The org admin must rotate/generate one first via RotateVerificationToken. Surfaced to handlers as a 409.

Functions

func NormalizeDomains

func NormalizeDomains(in []string) []string

NormalizeDomains lowercases, strips a leading "@", and de-duplicates claimed domains so the GIN-indexed lookup (`$1 = ANY(claimed_domains)`) matches regardless of how the admin entered them.

Types

type CallbackResult

type CallbackResult struct {
	Token       string
	UserID      string
	Email       string
	CreatedUser bool
	Role        types.OrgRole
}

CallbackResult is the outcome of HandleCallback.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service implements the OIDC SSO login flow and SSO-config encryption.

func New

func New(orgs orgStore, users userStore, cfg ServiceConfig) (*Service, error)

New creates an SSO service. keyProvider/stateKey may be nil in which case config-mutation and login are rejected at runtime (returned as errors); this keeps the service constructible in test setups that exercise only a subset.

func (*Service) ApplyConfigMutation

func (s *Service) ApplyConfigMutation(ctx context.Context, orgID string, req types.UpsertSSOConfigRequest, existingEncrypted []byte, existingVerified []string) ([]byte, error)

ApplyConfigMutation validates and persists an SSO config upsert. clientSecret is the plaintext IdP secret; an empty value means "leave the existing secret unchanged" (the caller must pre-load the existing config and re-supply its encrypted blob). existingVerified is the org's current verified_domains — the service intersects it with the new claimed_domains so verifications are preserved for domains still claimed and dropped for removed domains (D17 Q-S2 invariant: verified ⊆ claimed). Returns the encrypted blob actually stored.

func (*Service) CookieName

func (s *Service) CookieName() string

CookieName returns the configured PKCE/state cookie name.

func (*Service) EncryptClientSecret

func (s *Service) EncryptClientSecret(ctx context.Context, plaintext string) ([]byte, error)

EncryptClientSecret encrypts a plaintext IdP client secret with the server KEK (D17-S4). Returns the at-rest blob suitable for OrgSSOConfig.ClientSecret.

func (*Service) FrontendRedirectURL

func (s *Service) FrontendRedirectURL() string

FrontendRedirectURL returns the post-callback browser destination.

func (*Service) HandleCallback

func (s *Service) HandleCallback(ctx context.Context, orgSlug, redirectURL, code, state, cookieValue string) (*CallbackResult, error)

HandleCallback completes the OIDC flow: verifies state, exchanges the code, verifies the ID token, resolves (or auto-provisions) the user, applies the group-claim → role mapping, ensures org membership, and issues a JWT. redirectURL must match the URL passed to StartLogin (the IdP-registered callback).

func (*Service) RedirectBaseURL

func (s *Service) RedirectBaseURL() string

RedirectBaseURL returns the configured absolute base for SSO callback URLs. When empty, the handler refuses to build a callback URL and returns ErrRedirectBaseURLNotSet rather than deriving one from the request (F11).

func (*Service) RotateToken

func (s *Service) RotateToken(ctx context.Context, orgID string) (string, error)

RotateToken replaces the org's verification token with a fresh random value and returns it. Used for both initial creation and rotation. Old tokens stop matching immediately — admins must update their DNS TXT record after rotation. Returns ErrSSONotConfigured if the org has no SSO config (so the handler can map it to 404 rather than 500).

func (*Service) SetDNSResolver

func (s *Service) SetDNSResolver(r dnsResolver)

SetDNSResolver overrides the DNS resolver. Production code never calls this (the default netResolver is set in New); tests inject a fake to avoid real DNS dependencies.

func (*Service) StartLogin

func (s *Service) StartLogin(ctx context.Context, orgSlug, redirectURL string) (*StartResult, error)

StartLogin begins the OIDC Authorization Code + PKCE flow for an org. redirectURL is the absolute callback URL registered with the IdP. The handler resolves it from OIDC.RedirectBaseURL and fails with ErrRedirectBaseURLNotSet when that is unset (F11: the handler never derives it from the request).

func (*Service) StateTTL

func (s *Service) StateTTL() time.Duration

StateTTL returns the state cookie lifetime (for Set-Cookie Max-Age).

func (*Service) TokenTTL

func (s *Service) TokenTTL() time.Duration

TokenTTL returns the session JWT lifetime (for the success cookie Max-Age).

func (*Service) VerifyDomain

func (s *Service) VerifyDomain(ctx context.Context, orgID, domain string) (*VerifyDomainResult, error)

VerifyDomain checks the DNS TXT record at _llmsafespaces-verify.<domain> for the org's verification token and, on match, promotes the domain to verified. On-demand: the org admin triggers this after adding the TXT record. Returns ErrDomainNotClaimed if the domain is not in the org's claimed list, ErrNoVerificationToken if the org has no token (must rotate one first), or ErrDNSNotMatching if the TXT record doesn't contain the token.

type ServiceConfig

type ServiceConfig struct {
	TokenIssuer         TokenIssuer
	KeyManager          UserKeyManager
	KeyProvider         secrets.RootKeyProvider
	StateKey            []byte
	TokenTTL            time.Duration
	StateTTL            time.Duration
	RedirectBaseURL     string
	FrontendRedirectURL string
	StateCookieName     string
	Logger              *logger.Logger
}

ServiceConfig holds the non-store dependencies of the SSO service.

type SignedCookie

type SignedCookie struct {
	Name    string
	Value   string
	MaxAge  time.Duration
	Expires time.Time
}

SignedCookie carries the Set-Cookie value plus its Max-Age.

type StartResult

type StartResult struct {
	AuthURL string
	Cookie  *SignedCookie
}

StartResult is the outcome of StartLogin: the IdP authorization URL to redirect the browser to, and the signed state cookie to set on the response.

type TokenIssuer

type TokenIssuer interface {
	GenerateToken(userID string) (string, error)
}

TokenIssuer issues a session JWT for an authenticated user.

type UserKeyManager added in v0.7.0

type UserKeyManager interface {
	ProvisionServerKEKKeys(ctx context.Context, userID string) error
	IssueTokenAndUnlockDEK(ctx context.Context, userID string, ttl time.Duration, dekSource string) (string, error)
}

UserKeyManager is the server-KEK DEK capability the auth.Service exposes to the SSO (Epic 58) and passkey (Epic 59) login flows. It provisions a server-KEK-wrapped DEK for users who have none, and issues a session token whose jti is bound to the unlocked DEK — the non-password analog of auth.Service.Login's unlock step. Optional on the SSO service: when nil, completeLogin falls back to TokenIssuer.GenerateToken (pre-epic behavior; SSO users then have no personal-secret encryption until a later login).

type VerifyDomainResult

type VerifyDomainResult struct {
	Domain   string
	Verified bool
}

VerifyDomainResult is the outcome of VerifyDomain.

Jump to

Keyboard shortcuts

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