auth

package
v0.5.3 Latest Latest
Warning

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

Go to latest
Published: Jun 12, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package auth defines the provider-agnostic federated-identity surface for osctrl-api. Concrete provider implementations live in subpackages (pkg/auth/oidc and, eventually, pkg/auth/saml).

The package is intentionally minimal — it defines only what every provider must implement and what every caller must receive back. All protocol details (token verification, claim parsing, PKCE, metadata exchange) live in the subpackage.

Security note: this package never logs raw tokens, claims, or secrets. Concrete implementations must follow the same rule; see the spec at docs/proposals/osctrl-auth-providers-v0.1-spec.md for the full hardening rules.

Index

Constants

View Source
const (
	// TypeOIDC identifies an OpenID Connect provider (RFC 6749 +
	// OpenID Connect Core 1.0).
	TypeOIDC = "oidc"

	// TypeSAML is reserved for a future SAML 2.0 provider; not yet
	// implemented. Holding the constant prevents accidental
	// shadowing in subpackages and signals intent in the API.
	TypeSAML = "saml"
)

Type names for the discriminator on env_auth_providers. New protocol implementations must register a string here and provide a Provider implementation in a subpackage.

View Source
const StateCookieName = "osctrl_auth_state"

StateCookieName is the HttpOnly cookie that transports the per-login State from the LoginURL handler to the callback handler. The cookie path is scoped under /api/v1/auth/ so it never gets sent on other endpoints; the SPA's token cookie lives at /.

View Source
const StateCookiePath = "/api/v1/auth/"

StateCookiePath restricts the cookie scope. Callers must mount the auth routes at this prefix; if they don't, the cookie will not be sent on the callback request and HandleCallback will reject with ErrStateMissing.

View Source
const StateCookieTTL = 10 * time.Minute

StateCookieTTL is how long a login attempt can be in flight before the state JWT expires. Ten minutes accommodates an MFA prompt + IdP interstitials with margin; longer than this and we're inviting replay risk against the IdP's auth_time enforcement.

Variables

View Source
var (
	// ErrStateMissing is returned when the state cookie is not on the
	// request at all. Callback handlers map this to 403 with no body
	// (the legitimate flow always has the cookie because LoginURL set
	// it).
	ErrStateMissing = errors.New("auth: state cookie missing")

	// ErrStateInvalid is returned for any structural problem with the
	// state JWT — bad signature, wrong audience, expired, missing
	// claims, etc. Deliberately one error so external observers
	// cannot distinguish "tampered" from "expired" from "wrong
	// audience" via timing or status code. Threat T31.
	ErrStateInvalid = errors.New("auth: state cookie invalid")
)

Errors returned by IssueStateCookie / ParseStateCookie. Callers should match on these (via errors.Is) to map to HTTP status codes; the strings are stable and may appear in logs but never in user-facing error responses (we never reveal which check failed — see threat T31, T22).

Functions

func ClearStateCookie

func ClearStateCookie(w http.ResponseWriter)

ClearStateCookie removes the state cookie. Callers MUST invoke this immediately after a successful ParseStateCookie so that a replay of the callback URL fails (threat T8 / T9 — single-use state). The cookie is set with MaxAge=-1 which the browser interprets as "delete now."

func IssueStateCookie

func IssueStateCookie(w http.ResponseWriter, secret []byte, state State) error

IssueStateCookie writes the per-login state cookie. The state JWT is HMAC-SHA256 signed with the provided secret (typically the same secret pkg/users uses for user JWTs; audience claim segregates the two purposes — see threat T19).

secret length is not validated here because pkg/users already enforces MinJWTSecretBytes >= 32 at startup. If a caller manages to pass a shorter secret, HS256 signing will still succeed but is below RFC 7518 recommendation; the existing startup gate is the right place to enforce this, not this hot path.

IssueStateCookie sets the cookie with HttpOnly + Secure + SameSite=Lax + Path=/api/v1/auth/. The Secure flag means production deployments MUST use HTTPS for the callback URL; this is intentional. Dev mode with HTTP is not supported by this helper (use the SetSecure override only via the standard library if you really need it for local testing — production code paths must keep Secure=true).

func NewNonce

func NewNonce() (string, error)

NewNonce returns a fresh 256-bit cryptorandom nonce, base64url-encoded without padding. Suitable for State.Nonce, State.Verifier, or any other value that must be unguessable and URL-safe.

Errors only on crypto/rand failure, which on supported platforms means the system is too broken to be doing crypto at all. Callers should propagate the error (500 Internal Server Error) rather than retry.

Types

type Provider

type Provider interface {
	// Type returns the discriminator that identifies the protocol.
	// Must match one of the Type* constants above.
	Type() string

	// LoginURL builds the URL the user's browser is redirected to in
	// order to start the authentication flow. The State is opaque to
	// the caller; the caller is responsible for transporting it back
	// to HandleCallback via a state cookie (see pkg/auth/state.go).
	//
	// Implementations MUST embed unguessable nonces / verifiers in the
	// returned URL and the State so the callback can detect CSRF and
	// replay attempts. See threat IDs T6, T7, T9 in the spec.
	LoginURL(ctx context.Context, state State) (string, error)

	// HandleCallback consumes the provider's callback request,
	// validates everything (signature, issuer, audience, expiry,
	// nonce, PKCE verifier as applicable) and returns a
	// ResolvedIdentity describing the authenticated user.
	//
	// The State argument is the State that the caller previously
	// transported via cookie. Implementations MUST treat any
	// mismatch as a fatal authentication failure and MUST NOT
	// continue to user resolution. See threat IDs T6, T18.
	//
	// Implementations MUST NOT return raw tokens, claims, or
	// provider error bodies in the error message; those go to
	// server-side structured logging only. See spec hardening rule
	// "no raw token logging".
	HandleCallback(ctx context.Context, r *http.Request, state State) (ResolvedIdentity, error)
}

Provider is the contract every federated-identity backend must implement. A single Provider instance corresponds to one configured IdP for one osctrl environment.

All methods are context-aware. Implementations MUST honor context cancellation on any outbound network calls (token endpoint, JWKS fetch, etc.) so a slow IdP cannot wedge an HTTP handler.

type ResolvedIdentity

type ResolvedIdentity struct {
	// Subject is the stable, opaque identifier issued by the IdP
	// (typically the `sub` claim in OIDC). Never an email, never a
	// preferred name — those can change. Callers that need to
	// preserve identity across renames MUST use Subject.
	Subject string

	// PreferredUsername is what the user sees and types. Defaults to
	// the OIDC `preferred_username` claim; configurable per-provider
	// to fall back to email or sub. Used as the AdminUser.Username
	// after passing validation.
	PreferredUsername string

	// Email is informational; do NOT use it as a stable identifier
	// (mutable in most IdPs; spoofable in poorly-configured ones —
	// see threat T24).
	Email string

	// Name is the human display name (OIDC `name` claim) or a
	// concatenation of given+family if absent.
	Name string

	// Groups carries the user's IdP group memberships (after the
	// optional protocol-mapper claim shaping). v1 uses this only for
	// the RequiredGroups gate; future versions may map groups to
	// osctrl roles. See spec §"What this design does NOT do".
	Groups []string

	// Raw exposes the underlying provider-specific claim set for
	// debugging and future feature work. Callers MUST NOT read Raw
	// to bypass any of the typed fields above; that would defeat the
	// validation layer. Always nil for SAML when it lands; OIDC sets
	// it after id_token verification.
	Raw map[string]any

	// IDToken is the raw, signed id_token bytes the IdP returned.
	// Already verified at this point (sig + iss + aud + exp + nonce
	// checks all passed). The caller's only legitimate use is
	// id_token_hint on RP-initiated logout — DO NOT use it as an
	// authentication credential anywhere else. Empty for SAML.
	IDToken string
}

ResolvedIdentity is the protocol-neutral output of a successful HandleCallback. Callers translate this into an osctrl AdminUser via the JIT resolution path (cmd/api/handlers/auth_callback.go); see docs/proposals/osctrl-auth-providers-v0.1-spec.md §JIT.

All string fields except Subject may be empty. Subject MUST be stable for the lifetime of the IdP-side user account; callers rely on it as the cross-login identifier.

type State

type State struct {
	// EnvUUID locks this State to a specific osctrl environment.
	// Callbacks for env A using state issued for env B MUST be
	// rejected — see threat T18 (cross-env auth confusion).
	EnvUUID string

	// Nonce is a 256-bit cryptorandom value. For OIDC, this is
	// embedded in the authorize URL via the `nonce` parameter and
	// must match the `nonce` claim on the returned id_token. SAML
	// does not use Nonce (it has no equivalent protocol slot).
	//
	// Defense-in-depth invariant: Nonce and OAuthState MUST be
	// independent random values, NOT the same value. Earlier
	// versions reused a single random value for both slots; a
	// pentest finding (May 2026) called out that a leak of either
	// — e.g. `state` ending up in a Referer log — would
	// simultaneously compromise the other. Two independent values
	// mean two independent leak surfaces.
	Nonce string

	// OAuthState is the OAuth2 / SAML RelayState parameter. For
	// OIDC, this is what the callback's `state` query param must
	// echo (CSRF defense — threat T6). For SAML, this is the
	// RelayState the ACS POST must echo (threat S10). Always a
	// 256-bit cryptorandom value, INDEPENDENT of Nonce.
	OAuthState string

	// Verifier is the PKCE code_verifier (RFC 7636) when the
	// provider has PKCE enabled. Empty otherwise. The callback
	// presents this to the token endpoint along with the code; the
	// IdP recomputes the challenge and compares.
	//
	// Empty Verifier with a PKCE-enabled provider MUST cause
	// HandleCallback to reject — see threat T10.
	Verifier string

	// SAMLRequestID is the AuthnRequest ID minted at LoginURL time,
	// round-tripped through the state cookie, and passed back to
	// ParseResponse as the expected InResponseTo. Closes threat S7
	// (InResponseTo replay/forgery) — without it, crewjam either
	// rejects every response (when nil is passed) or accepts any
	// InResponseTo (when an empty slice is passed). Always empty
	// for OIDC; populated for SAML SP-initiated flows.
	SAMLRequestID string
}

State is the per-login data the caller must round-trip from LoginURL through the user's browser to HandleCallback. It is opaque to the user (transported in an HttpOnly cookie) but its claim shape is part of the contract between the auth handler and the provider.

Implementations may add unexported provider-specific fields by embedding State in a protocol-specific extension type kept inside the provider subpackage.

func ParseStateCookie

func ParseStateCookie(r *http.Request, secret []byte) (State, error)

ParseStateCookie reads the state cookie off the request, verifies the JWT, and returns the decoded State.

Verification enforces, in order:

  1. Cookie present (else ErrStateMissing)
  2. HS256 algorithm pinned (defeats alg-confusion; defense in depth since jwt.ParseWithClaims's key callback also rejects non-HMAC, but explicit is better than implicit for auth code)
  3. Signature valid under `secret`
  4. iss == "osctrl-api"
  5. aud == "osctrl-auth-state" (defeats token confusion T19)
  6. exp > now (clock skew tolerance: 0 — state cookies are short-lived)
  7. nbf <= now (paranoid; should always hold for non-malicious tokens)
  8. EnvUUID + Nonce non-empty (rejects malformed body)

Any single failure returns ErrStateInvalid. The internal failure reason is loggable for ops but never surfaced to the user.

func (State) IsZero

func (s State) IsZero() bool

IsZero reports whether the State is the zero value, useful for callers that need to distinguish "missing state" from "valid state with zero fields".

Directories

Path Synopsis
Package oidc is the OpenID Connect (RFC 6749 + OIDC Core 1.0) implementation of the auth.Provider interface defined in pkg/auth.
Package oidc is the OpenID Connect (RFC 6749 + OIDC Core 1.0) implementation of the auth.Provider interface defined in pkg/auth.
Package saml is the SAML 2.0 Web Browser SSO Profile implementation of the auth.Provider interface defined in pkg/auth.
Package saml is the SAML 2.0 Web Browser SSO Profile implementation of the auth.Provider interface defined in pkg/auth.

Jump to

Keyboard shortcuts

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