oidc

package
v0.9.2 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

derive.go holds the IDENTITY-DERIVATION seam: everything that turns an ID token's claims into WHO this human is (deriveRole, Config.RoleMap, the legacy operator allowlist) and their group snapshot (sessionGroups). Pure claims-in, identity-out — nothing here touches HTTP, cookies, or the OAuth2 exchange, so it is unit-testable without a signed token or a fake IdP.

Package oidc implements human SSO for Wardyn via OpenID Connect (Dex-compatible).

CSRF is two-layered: a random 128-bit state value in an HttpOnly SameSite=Lax cookie, compared to the IdP callback; and SameSite=Lax on every session cookie. SameSite doesn't stop a same-site host from PLANTING a cookie, so under SecureCookies every cookie carries the __Host- prefix — the browser refuses a Domain= one, and the unprefixed name is never read. A PKCE code_challenge (S256) is verified by the token endpoint, holding even if the state check is bypassed (a mix-up attack). Known gap: the nonce is verified in the ID token but not bound to the device (mitigated by state+PKCE).

Sessions live entirely in a signed HttpOnly SameSite=Lax cookie: sub, email, role, expiry, HMAC-SHA256 signed with the key passed to New, never logged and never leaving the process. A cookie with no role decodes as NO session, forcing a fresh re-derivation rather than granting an undefined role.

Middleware accepts a valid session cookie (sets HumanPrincipal on the context) or falls through to the next handler (the integrator's own bearer-auth path).

The wardyn_session cookie codec: the signed payload's format version, and the encode/decode/HMAC trio around it. Split from oidc.go by seam (the file-size gate), exactly as context.go was; no behaviour lives here that oidc.go's package doc does not describe.

user_type_derive.go is the user-type half of role derivation. A role-map value names a TARGET: one of the two admin tiers, or a user type id (the user tier on that type). deriveRole picks the tier by rank; pickUserType picks the highest-priority CUSTOM type among the matches, with the built-in "standard" type as the tie floor that never wins and never ties.

Index

Constants

View Source
const (
	// SubjectTokenInvalid: token failed to verify (signature, issuer, expiry, alg=none included) or its claims didn't decode.
	SubjectTokenInvalid = "subject_token_invalid"
	// SubjectTokenAudience: token was neither issued TO the portal nor FOR this deployment at the portal's request.
	SubjectTokenAudience = "subject_token_audience"
	// SubjectTokenNoIssuedAt: token carries no iat, so the session cutoff (SessionRevocations) has nothing to compare against.
	SubjectTokenNoIssuedAt = "subject_token_no_iat"
	// SubjectTokenUnbound: an Entra token issued to the portal names an identity (tenant and object
	// id) that no sign-in here has bound to a principal and no person was set up by.
	SubjectTokenUnbound = "subject_token_unbound"
)

Subject-token refusals VerifySubjectToken names itself; a refusal from shared admission (reserved subject, email-domain gate, role/user-type derivation) uses that step's own auth_error code instead.

View Source
const (
	RoleAdmin         = "admin"
	RoleSecurityAdmin = "security_admin"
	RoleUser          = "user"
)

Wardyn roles a session can carry. See Session.Role / Config.RoleMap.

RoleSecurityAdmin is the THIRD tier: governs security posture (approvals, audit, permission grants) WITHOUT the super admin's reach into other people's runs or credentials. NOT a rung below RoleAdmin — internal/api keeps two predicates (isOperator = super only, isSecurityOperator = super OR security admin) since security_admin ⊄ admin at the role-snapshot stamp sites. MAPPED TIER ONLY: reachable only through the merged role map, never an email-allowlist twin, and cmd/wardynd REFUSES it as a fallthrough default.

View Source
const (
	DenialEmailVerifiedAbsent = "email_verified_absent"
	DenialEmailUnverified     = "email_unverified"
	DenialEmailDomain         = "email_domain"
)

The email policy refusals reported to onDenied (#155): the same codes the browser sees as auth_error and a portal's token exchange records as its refusal reason. Literals, equal to authErrorEmailVerifiedAbsent, authErrorEmailUnverified and authErrorEmailDomain.

View Source
const (
	// DenialNoRole: the map is non-empty, nothing matched, no default role set.
	DenialNoRole = "no_role"
	// DenialUserTypeAmbiguous: two or more custom user types tie at top priority.
	DenialUserTypeAmbiguous = "user_type_ambiguous"
	// DenialUserTypeUnknown: a matched value names a user type that doesn't exist.
	DenialUserTypeUnknown = "user_type_unknown"
)

Why a derivation refused a sign-in. Each is also the auth_error code the callback redirects with, so the sign-in screen and the audit row name the same cause.

View Source
const DenialIdentityDeactivated = "identity_deactivated"

DenialIdentityDeactivated is the auth_error code admit returns, and the reason it reports to onDenied, for a sign-in whose identity is deactivated or purged. The callback shows the browser sign_in_refused instead (the page does not say the identity is deactivated); a portal's token exchange records this code.

View Source
const DenialReservedPrincipal = "reserved_principal"

DenialReservedPrincipal reports a sign-in refused because a subject names an identity that isn't a person. Not an auth_error code — the browser sees the generic authErrorSignInRefused, since nothing the user can do clears it.

View Source
const RemovedRoleMember = "member"

RemovedRoleMember is the pre-0.8 name of RoleUser. 0.8 accepted it as the user tier on the built-in "standard" type; 0.9 treats it as any other unknown role value. It stays a reserved word so it can never parse as a user type id, and it is the one value RemovedRoleHint explains.

View Source
const SessionCodecVersion = 2

SessionCodecVersion is the wardyn_session payload's format version, stamped by encodeSession and REQUIRED to match by decodeSession.

It exists for one field that cannot be tolerantly decoded: GroupsTruncated. A 0.6 cookie carries no such key, so a JSON decode reads it as false — the FAIL-OPEN answer (see the field). A human whose group snapshot was already truncated at their last login would keep a cookie asserting "complete" and silently shed the group-assigned governance profile walling them in. Nothing in-band distinguishes "0.6 cookie" from "0.7 cookie, not truncated", so the payload carries a version and a mismatch is simply not a session.

THE COST IS ONE EXTRA LOGIN, once, per human holding a pre-0.7 cookie — folded into the same re-login PF-12 already documents for the pre-0.6 snapshot. A mismatch behaves like the empty-Role case below: Middleware falls through with no principal, the browser is bounced to sign in, and CallbackHandler mints a current cookie. Of the three options — this, tolerant-decode-false, or a permanent second guess — it alone is neither a silent widening nor a permanent one.

Version 2 (0.8) renamed the non-admin tier "member" to "user" and added Session.UserType: one more re-login, and no cookie carrying the old tier word, or no type, is ever half-trusted.

Compared EXACT, not `<`: under `<`, an old binary in a rolling upgrade accepts a new cookie and reads its unknown fields as zero — the same fail-open this constant prevents. A mixed-version rollout costs logins, not containment.

EXPORTED because a hand-minted payload is otherwise unverifiable: anything building a session cookie outside encodeSession (this module's own tests, an embedder's harness) must stamp the current version or produce a cookie the very next request rejects.

View Source
const SessionRoleStampStale = "role_stamp_stale"

SessionRoleStampStale is the rejection reason for a session whose role stamp is older than Config.RoleStampTTL.

Variables

View Source
var ErrIdentityBindingMismatch = errors.New("oidc: identity bound to another principal")

ErrIdentityBindingMismatch is Issue's refusal of a sign-in whose identity is bound to another principal than the one its session would carry.

View Source
var ErrIdentityConflict = errors.New("oidc: identity clashes with another binding")

ErrIdentityConflict is Issue's refusal of a sign-in whose identity clashes with another binding (the same principal under a different object id).

View Source
var ErrIdentityDeactivated = errors.New("oidc: identity deactivated")

ErrIdentityDeactivated is Issue's refusal of a deactivated or purged identity.

View Source
var ErrInvalidSession = errors.New("oidc: invalid session cookie")

ErrInvalidSession is returned when the cookie is present but tampered, malformed, or mis-keyed.

View Source
var ErrNoSession = errors.New("oidc: no session cookie")

ErrNoSession is returned by decodeSession when no session cookie is present.

Roles is the closed set of recognized role values, in rank order, pinned against the DDL parity guard.

Functions

func AuthorityEpochFromContext added in v0.8.6

func AuthorityEpochFromContext(ctx context.Context) (int64, bool)

AuthorityEpochFromContext is the authority epoch of the verified session cookie (Middleware) or of the sign-in being issued (WithAuthorityEpoch); false when neither put one on ctx, as on the API-token and admin-token lanes.

func CookieName added in v0.8.0

func CookieName(secure bool, name string) string

CookieName is the name a console cookie is written and read under. With secure cookies on it carries the __Host- prefix, so the browser holds it host-only: no sibling host under the console's registrable domain (a relayed sandbox page among them) can plant a Domain= cookie the console would read as its session or its sign-in state. The plain name is never read in that posture, not even as a fallback, because the plain name is exactly what a sibling can plant. Over plain HTTP (secure off) a browser refuses every __Host- cookie, so the plain name is the only one that works.

func CookiePath added in v0.8.0

func CookiePath(secure bool, basePath string) string

CookiePath is the Path of every console cookie. __Host- requires Path=/, so with secure cookies on it is "/" even under WARDYN_BASE_PATH: a neighbouring application on the console's own host then receives the cookies too, which grants it nothing it lacks already, since it shares the console's origin and can call the API with them attached. Over plain HTTP the cookies stay scoped to the base path.

func CutSessions added in v0.8.6

func CutSessions(ctx context.Context, rev SessionRevocations, sub string) error

CutSessions cuts sub's browser sessions. A rev that cannot cut without touching tokens (a test double) falls back to RevokeSub, which ends the person's tokens and keys too: the safe direction.

func EmailFromContext added in v0.5.0

func EmailFromContext(ctx context.Context) string

EmailFromContext returns the email claim of the verified session, or "" when absent (possible when AllowedEmailDomains is empty). internal/api resolves the viewer/operator role from this, since the opaque "sub" can't be matched against an admin-writable allowlist.

func ExpiryFromContext added in v0.6.0

func ExpiryFromContext(ctx context.Context) time.Time

ExpiryFromContext returns when the verified session expires, or the zero time when absent. No refresh — the session dies outright — so the console surfaces it as an advance warning rather than a surprise 401.

func GroupsFromContext added in v0.6.0

func GroupsFromContext(ctx context.Context) []string

GroupsFromContext returns the login-time group snapshot — the subjects a `group` capability grant matches.

NIL AND EMPTY MEAN DIFFERENT THINGS: empty non-nil means the IdP sent no usable group identity, so grants genuinely don't apply. Nil means no SSO session, or a pre-0.6 cookie predating this field — internal/api surfaces that as groups_snapshot_stale rather than "no groups".

func GroupsTruncatedFromContext added in v0.7.0

func GroupsTruncatedFromContext(ctx context.Context) bool

GroupsTruncatedFromContext reports whether the group snapshot is PARTIAL (hit the cookie byte cap). A caller must treat true exactly like a nil snapshot — no group-scoped decision can be made from it.

func IsRevoked added in v0.9.0

func IsRevoked(ctx context.Context, rev SessionRevocations, sub, email string, issue Issue) (bool, error)

IsRevoked is rev.IsSessionRevoked for a credential issued at issue: ordered by sequence when it recorded one and rev orders cuts, otherwise by time as before.

func IssueSeq added in v0.9.0

func IssueSeq(ctx context.Context, rev SessionRevocations) (int64, error)

IssueSeq is the revocation sequence a credential issued now records, or 0 (compare on time) when rev does not order cuts. A failure is returned: an issuer must not mint a credential it cannot order.

func IssuedAtFromContext added in v0.9.0

func IssuedAtFromContext(ctx context.Context) time.Time

IssuedAtFromContext returns when the verified session was signed in (its role-stamp time), or the zero time when absent.

func MemberModeFromContext added in v0.7.4

func MemberModeFromContext(ctx context.Context) bool

MemberModeFromContext reports whether this session is "view as member" — an admin treated as a member for the rest of the session. RoleFromContext already answers member when true, so authorization needs nothing from this; it exists only for the console banner and the credential doors (API-token mint refuses; SSH keys are accepted capped).

func MemberPreviewNoCredential added in v0.7.5

func MemberPreviewNoCredential(ctx context.Context) bool

MemberPreviewNoCredential reports whether this session is in the NO-CREDENTIAL posture of member mode — "view as a new member who has not signed in". Published as `MemberMode && MemberModeNoCredential`, so it implies MemberModeFromContext.

Authorizes and clamps nothing: it only moves whether a per-user model-credential read answers "absent" (previewHidesOwnCredential), letting every already-fail-closed path reach the not-signed-in state.

func NameFromContext added in v0.7.1

func NameFromContext(ctx context.Context) string

NameFromContext returns the display-name claim, or "" when absent. Display only — every decision keys on the sub/email, never this.

func ObjectIDFromContext added in v0.8.4

func ObjectIDFromContext(ctx context.Context) string

ObjectIDFromContext returns the Entra object id the session was signed in with, or "" when there is none: a non-Entra issuer, or a session written before the field existed. Identity input only — never log it or put it in a response or an audit row.

func ParseRoleMap added in v0.5.0

func ParseRoleMap(csv string) (map[string]string, error)

ParseRoleMap parses WARDYN_OIDC_ROLE_MAP: comma-separated "value=role" pairs, matched case-insensitively against an ID token's roles/groups/email. Empty/blank input returns a nil map (derivation disabled) and no error; non-empty input yielding no usable entry is an error, never a silent nil — nil means "everyone is admin".

func PrincipalFromContext

func PrincipalFromContext(ctx context.Context) string

PrincipalFromContext returns the human principal set by Middleware (the OIDC "sub" claim), or "" if no SSO session is present. internal/api's principalFromRequest calls this first, falling back to the admin-token path.

func RemovedRoleHint added in v0.9.0

func RemovedRoleHint(v string) string

RemovedRoleHint is the sentence appended to a bad-role error when v is RemovedRoleMember, "" for any other value.

func RevocationSeqFor added in v0.9.0

func RevocationSeqFor(ctx context.Context, sub string) int64

RevocationSeqFor is that sequence when the admitting credential was issued to sub, and 0 otherwise: a credential's sequence says nothing about another person's cuts, and 0 means "compare on time".

func RoleFromContext added in v0.5.0

func RoleFromContext(ctx context.Context) string

RoleFromContext returns the EFFECTIVE Wardyn role, or "" when there is no SSO session. Effective, not stamped: in "view as member" mode this always answers RoleUser, clamped once in contextWithPrincipal — the stamped role is never visible outside this package, which only derives/carries the role; internal/api enforces it.

func SessionRejectedFromContext added in v0.6.0

func SessionRejectedFromContext(ctx context.Context) string

SessionRejectedFromContext returns why Middleware rejected a presented session cookie: "invalid_session", "expired_session", "revoked_session", "identity_deactivated" (the identity was deactivated, or a suspension passed the cookie's authority epoch), "role_stamp_stale" when RoleStampTTL is set, or "session_revocation_unavailable" (fail-closed) when Revocations is wired; "" when no cookie was presented or it decoded fine.

func SplitMappingTarget added in v0.8.0

func SplitMappingTarget(v string) (role, userType string, ok bool)

SplitMappingTarget splits a role-map value into the tier and user type it names: admin/security_admin carry no type, "user" maps to the built-in "standard" type, and any other well-formed, unreserved id is the user tier on that type. ok is false for anything else, "member" included.

func StampedUserTypeFromContext added in v0.8.0

func StampedUserTypeFromContext(ctx context.Context) string

StampedUserTypeFromContext returns the user type stamped at sign-in, which the user view does not change: the view's fallback when no chosen type is remembered. "" when there is no SSO session.

func UserTypeFromContext added in v0.8.0

func UserTypeFromContext(ctx context.Context) string

UserTypeFromContext returns the resolved user type id, or "" when there is no SSO session: the sign-in type, or the user-view type while that's on.

func UserTypeIDReserved added in v0.8.0

func UserTypeIDReserved(id string) bool

UserTypeIDReserved reports whether id is a word a role-map value already means: the three tiers, the removed "member", and "denied".

func UserTypeIDWellFormed added in v0.8.0

func UserTypeIDWellFormed(id string) bool

UserTypeIDWellFormed reports whether id has the shape the user_types table accepts: lowercase ASCII words joined by single hyphens, at most 63 bytes.

func UserViewDroppedFromContext added in v0.8.0

func UserViewDroppedFromContext(ctx context.Context) string

UserViewDroppedFromContext returns the type whose deletion turned this session's user view off (Session.UserViewDropped), or "".

func UserViewDroppedNameFromContext added in v0.8.0

func UserViewDroppedNameFromContext(ctx context.Context) string

UserViewDroppedNameFromContext returns that same type's cached display name (Session.UserViewDroppedName), or "" when there is none — either no drop is recorded, or the cookie predates this field. Pairs with UserViewDroppedFromContext's id.

func ValidMappingTarget added in v0.8.0

func ValidMappingTarget(v string) bool

ValidMappingTarget reports whether v may be a role-map value or WARDYN_OIDC_DEFAULT_ROLE: a tier or a well-formed user type id. Whether the type EXISTS is a store question, answered at sign-in.

func ValidRole added in v0.5.0

func ValidRole(s string) bool

ValidRole reports whether s is a recognized role value; all callers fail closed on a typo. WARDYN_OIDC_DEFAULT_ROLE validates through validDefaultRole (cmd/wardynd), stricter than this: it additionally refuses RoleSecurityAdmin.

func WithAuthorityEpoch added in v0.8.6

func WithAuthorityEpoch(ctx context.Context, epoch int64) context.Context

WithAuthorityEpoch carries the authority epoch a sign-in was issued under to the login-grant sink.

func WithRevocationSeq added in v0.9.0

func WithRevocationSeq(ctx context.Context, sub string, seq int64) context.Context

WithRevocationSeq carries the revocation sequence the credential admitting this request recorded at issue, with the principal it was issued to.

func WithSessionRejected added in v0.8.6

func WithSessionRejected(ctx context.Context, reason string) context.Context

WithSessionRejected is how another credential lane (an API token whose owner is deactivated) hands the reason it refused to the same auth.fail emit a rejected cookie reaches.

Types

type Authenticator

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

Authenticator provides OIDC login, callback, logout, and session-check handlers.

func New

func New(ctx context.Context, cfg Config, hmacKey []byte) (*Authenticator, error)

New constructs an Authenticator by performing OIDC discovery against cfg.IssuerURL. hmacKey signs session cookies and is never logged.

func (*Authenticator) AttachLoginGrantSink added in v0.7.10

func (a *Authenticator) AttachLoginGrantSink(sink LoginGrantSink)

AttachLoginGrantSink joins the sink to this Authenticator, called once at boot before anything is served; a nil sink detaches, restoring the unwidened login.

func (*Authenticator) AttachPersonKeying added in v0.8.2

func (a *Authenticator) AttachPersonKeying(k PersonKeying)

AttachPersonKeying joins k to this Authenticator; nil detaches.

func (*Authenticator) CallbackHandler

func (a *Authenticator) CallbackHandler(w http.ResponseWriter, r *http.Request)

func (*Authenticator) CallbackHandlerWithDenials added in v0.8.0

func (a *Authenticator) CallbackHandlerWithDenials(reserved func(sub string) bool, onDenied func(r *http.Request, reason string)) http.HandlerFunc

CallbackHandlerWithDenials is CallbackHandler with a reserved-subject check (DenialReservedPrincipal) and sign-in denials (including DenialUserTypeAmbiguous, DenialUserTypeUnknown) reported to onDenied so internal/api can audit them as auth.fail. This package stays store- and audit-agnostic; internal/api decides which principals are reserved.

func (*Authenticator) ChartRoleMap added in v0.7.0

func (a *Authenticator) ChartRoleMap() map[string]string

ChartRoleMap returns a COPY of the boot-time role map so a handler can never mutate boot config.

func (*Authenticator) ClientID added in v0.8.0

func (a *Authenticator) ClientID() string

ClientID is this deployment's own client id at the identity provider — what a portal's access token carries in aud when it was requested for Wardyn.

func (*Authenticator) DefaultRole added in v0.7.0

func (a *Authenticator) DefaultRole() string

DefaultRole returns Config.DefaultRole, the fallthrough when the merged map matched nothing. Empty means "deny".

func (*Authenticator) DefaultRoleIsAdmin added in v0.8.0

func (a *Authenticator) DefaultRoleIsAdmin() bool

DefaultRoleIsAdmin reports whether Config.DefaultRole resolves to admin, kept here so this package, not a caller, owns what counts as "the admin role".

func (*Authenticator) DefaultRoleOutcome added in v0.8.0

func (a *Authenticator) DefaultRoleOutcome(userTypes []types.UserType) (role, userType string, ok bool)

DefaultRoleOutcome reports what an UNMATCHED sign-in would actually derive from Config.DefaultRole, validated the same way deriveRole validates a matched default, so a display surface never claims a target a real sign-in would refuse. ok is false when DefaultRole is unset/malformed or names a type the store lacks, except on the admin tier, which falls to "standard" instead, matching deriveRole.

func (*Authenticator) DropUserView added in v0.8.0

func (a *Authenticator) DropUserView(w http.ResponseWriter, r *http.Request) (context.Context, error)

DropUserView turns the user view off when its type no longer exists: the cookie re-signs with view bits cleared and the type recorded in UserViewDropped (so GET /me can say why), returning the context republished from that session with the admin's real tier. Only GET /me may serve its own request from that context — every other request meeting a deleted type is refused, not re-evaluated.

func (*Authenticator) EnforcesEmailVerified added in v0.8.4

func (a *Authenticator) EnforcesEmailVerified() bool

EnforcesEmailVerified reports whether sign-in requires a verified email: a domains allowlist is set or Config.RequireEmailVerified is on. Without either, email_verified is not enforced, a distinction the People page badge otherwise can't express.

func (*Authenticator) HasOperatorEmails added in v0.7.0

func (a *Authenticator) HasOperatorEmails() bool

HasOperatorEmails reports whether Config.LegacyAdminEmails is non-empty — the People page needs this to explain why a row already resolves to admin independent of anything it manages.

func (*Authenticator) IsOperatorEmail added in v0.7.0

func (a *Authenticator) IsOperatorEmail(v string) bool

IsOperatorEmail reports whether v matches an entry on Config.LegacyAdminEmails — the People page write boundary uses this to refuse a row up front rather than discover it inert at sign-in. Delegates to emailInList, the exact match deriveRole's own allowlist check uses.

func (*Authenticator) Issuer added in v0.7.0

func (a *Authenticator) Issuer() string

Issuer returns the PUBLIC OIDC issuer URL, from which the People page derives a human-facing provider name (e.g. an Entra tenant -> "Microsoft Entra ID") for the SSO chip.

func (*Authenticator) KeysPeopleByObjectID added in v0.8.2

func (a *Authenticator) KeysPeopleByObjectID() bool

KeysPeopleByObjectID reports whether a person may be set up by Entra object id here: the issuer is Entra ID.

func (*Authenticator) LoginHandler

func (a *Authenticator) LoginHandler(w http.ResponseWriter, r *http.Request)

LoginHandler initiates the OIDC authorization code flow. It generates a random state and nonce, stores them in HttpOnly SameSite=Lax cookies, and redirects the user to the IdP authorization endpoint.

func (*Authenticator) LogoutHandler

func (a *Authenticator) LogoutHandler(w http.ResponseWriter, r *http.Request)

LogoutHandler is SignOut for a route that has no use for its outcome.

func (*Authenticator) MergedMapEmpty added in v0.7.0

func (a *Authenticator) MergedMapEmpty(rows []RoleMapping) bool

MergedMapEmpty reports whether the ACTUAL merged role map is empty — needed instead of a raw row count, which diverges whenever a stored row is shadowed and contributes nothing.

func (*Authenticator) Middleware

func (a *Authenticator) Middleware(next http.Handler) http.Handler

Middleware sets HumanPrincipal on the context for a valid session cookie, or falls through to next without one so the integrator's own bearer-auth path can handle it — composed as oidc.Middleware(adminAuth(handler)). A cookie presented but rejected (tampered/expired) has its reason ride the context (SessionRejectedFromContext) so the eventual 401 can name it.

func (*Authenticator) PreviewRole added in v0.7.0

func (a *Authenticator) PreviewRole(ctx context.Context, roles, groups []string, email string) (Derivation, error)

PreviewRole runs the SAME derivation CallbackHandler would, so the console's People page can show "who would this row make an admin" before that person signs in. err is non-nil only when the store read failed; it never falls back to an env-only preview (same fail-closed reason as login).

func (*Authenticator) PreviewRoleAgainst added in v0.7.0

func (a *Authenticator) PreviewRoleAgainst(rows []RoleMapping, userTypes []types.UserType, roles, groups []string, email string) Derivation

PreviewRoleAgainst is PreviewRole's PURE twin: runs the identical mergeRoleMaps + deriveRole derivation against a CALLER-SUPPLIED candidate rows slice, so the write-boundary lockout guard can ask "would the acting admin still derive admin AFTER this proposed write" without a second store round trip. No store read, no error return — compare PreviewRole, which reads Config itself.

func (*Authenticator) RecordAttach added in v0.8.2

func (a *Authenticator) RecordAttach(r *http.Request, sess Session)

RecordAttach reports sess's attach to PersonKeying.Attached when admission resolved it to a person set up by object id. Each door calls it once it has no refusal left to make, so a refused sign-in or exchange records none.

func (*Authenticator) SessionBounds added in v0.9.0

func (a *Authenticator) SessionBounds(r *http.Request) (sub string, expiry, issuedAt time.Time, ok bool)

SessionBounds reads the request's session cookie when its signature verifies and returns its subject, expiry and sign-in time. It makes none of Middleware's expiry, role-stamp or revocation checks: a caller uses it only to bound a credential the request presents beside the cookie.

func (*Authenticator) SetUserView added in v0.8.0

func (a *Authenticator) SetUserView(w http.ResponseWriter, r *http.Request, on bool, typeID, typeName string, noCredential bool) (stampedRole string, err error)

SetUserView flips this request's session into or out of the user view ("view as member" until 0.8), writes the re-encoded cookie to w, and returns the session's STAMPED role — what the view pauses. typeID is Session.UserViewType, pre-validated by the caller; any switch clears UserViewDropped. The stamped role is returned because RoleFromContext is already clamped to member while the mode is on. Decodes the CURRENT cookie, not a Session (context values would drop IssuedAt); a bad cookie surfaces decodeSession's own error as a 4xx.

ONE exception: a STAMPED member turning the mode ON gets no cookie at all — downstream readers key on the flag, not the tier (a banner this human can't back, a refused token mint, capped SSH keys). OFF always re-signs and never CLEARS the session, so toggling off leaves the human signed in. noCredential is "view as a member who hasn't signed in", stored as `on && noCredential` so OFF clears it by construction.

typeName caches typeID's display name (Session.UserViewTypeName) purely so a LATER DropUserView can still name the type once its row is gone — see that field's own doc. The caller resolves it; this function never reads the store.

func (*Authenticator) SignOut added in v0.9.0

func (a *Authenticator) SignOut(w http.ResponseWriter, r *http.Request) (cutSub string, err error)

SignOut clears the Wardyn session cookie, cuts the signer's browser sessions server-side so a copy of the cookie stops working too, and redirects to the console root. It returns the sub it cut or tried to cut ("" when it cut nobody) and the cut's error.

The cut is the one sign-out everywhere writes (CutSessions): it ends every browser session of the person, not only this one, and leaves their API tokens and SSH keys alone. It is written only for a request that Middleware accepted as that same live session (PrincipalFromContext names the cookie's sub): a cookie that merely verifies is not enough, since an expired, revoked or role-stale copy still carries a valid signature, and a request that also presents a bearer credential skips the rejected-cookie answer. Such a cookie is cleared and redirected like any other, and cuts nobody.

A cut that cannot be written answers 503, with the cookie already cleared, so the caller does not read an unconfirmed sign-out as a confirmed one.

The sign-in state, nonce, PKCE and widened-login cookies are cleared too, so a login this browser started before signing out (a popup still at the identity provider) fails its callback's state check afterwards.

func (*Authenticator) UserViewStampedRole added in v0.8.4

func (a *Authenticator) UserViewStampedRole(r *http.Request) string

UserViewStampedRole is the role stamped on this request's signed session while the user view is ON, and "" otherwise — outside the view, with no cookie, or with one that does not verify. It is the only reader of the stamped role beside SetUserView: the context's role is already clamped to user in the view, so the console's "is this a super admin" bit cannot come from there. It reads and verifies the cookie exactly as SetUserView does and never writes one.

func (*Authenticator) UserViewSuperAdmin added in v0.8.4

func (a *Authenticator) UserViewSuperAdmin(r *http.Request) (superAdmin, ok bool)

UserViewSuperAdmin answers, for a request in the user view, whether the role stamped on its session is admin (true) or security_admin (false); ok is false for anyone else, including a stamped user. The comparison lives here, beside the stamp, so callers never re-derive a tier.

func (*Authenticator) VerifySubjectToken added in v0.8.0

func (a *Authenticator) VerifySubjectToken(r *http.Request, raw, portalClientID string, reserved func(string) bool) (Session, string)

VerifySubjectToken verifies raw against this deployment's issuer/key set and admits its subject through the SAME decision the sign-in callback makes, so a portal can never act for someone who couldn't sign in, nor get a role or group snapshot a sign-in wouldn't derive.

The audience rule is the one thing sign-in doesn't share: the token must be either an access token issued for Wardyn (aud holds ClientID) at the portal's request (azp is portalClientID), or issued to the portal itself (aud is exactly portalClientID; azp, if present, agrees). A token for any other client is refused, and a portal registered under Wardyn's own client id matches nothing — else every Wardyn ID token would double as a subject token for it.

On Entra ID, `sub` is pairwise per app registration, so a token issued to the portal names the person under the portal's sub, not Wardyn's. Its principal is the one the person's own sign-in bound to the token's (issuer, tid, oid), else a person set up by that object id; with neither, the exchange is refused (boundPortalSubject).

The returned Session carries the token's iat as IssuedAt (compared against the session cutoff) and exp as Expiry. denied is non-empty on any refusal, with a zero Session. The caller records an attach to a person set up by object id with RecordAttach once it has admitted the exchange.

type Config

type Config struct {
	// IssuerURL is the OIDC provider's public issuer — the browser redirect target and "iss" claim.
	IssuerURL string
	// InternalIssuerURL, when set, is where wardynd itself reaches the IdP for server-side calls,
	// solving split-horizon where the browser and control plane see different hostnames.
	InternalIssuerURL string
	// ClientID is the OAuth2 client identifier registered with the IdP.
	ClientID string
	// ClientSecret is the OAuth2 client secret; never logged. Optional: empty registers a public
	// client (PKCE S256 is still sent on every login), not a weaker registration.
	ClientSecret string
	// RedirectURL is the callback URL registered with the IdP; must be <wardynd-base>/auth/callback.
	RedirectURL string
	// BasePath is WARDYN_BASE_PATH ("" = host root): prefix of every console redirect and cookie Path.
	BasePath string
	// AllowedEmailDomains, when non-empty, restricts login to emails whose domain exactly matches
	// one of the listed values case-insensitively, AND requires email_verified=true. Entra tokens
	// typically omit email_verified, fail-closing every Entra login — prefer Entra App Roles instead.
	AllowedEmailDomains []string
	// RequireEmailVerified (WARDYN_OIDC_REQUIRE_EMAIL_VERIFIED, default false) applies the same
	// email_verified gate AllowedEmailDomains implies, without a domain list: an ID token with no
	// email_verified claim counts as unverified and is refused, false is refused, true passes.
	RequireEmailVerified bool
	// ExtraScopes is WARDYN_OIDC_EXTRA_SCOPES (CSV), appended to the fixed "openid profile email"
	// request. Validated at boot against discovery scopes_supported, so an unadvertised scope
	// refuses boot by name instead of locking every human out at login with invalid_scope.
	ExtraScopes []string
	// RoleMap maps a case-insensitive claim/email value to RoleAdmin or RoleUser (WARDYN_OIDC_ROLE_MAP,
	// parsed by ParseRoleMap). Any RoleAdmin match wins. Empty (default) keeps every human RoleAdmin.
	RoleMap map[string]string
	// DefaultRole is the role a human gets when RoleMap is non-empty but nothing matched: RoleAdmin,
	// RoleUser, or "" (default) to DENY the login.
	DefaultRole string
	// LegacyAdminEmails (WARDYN_OIDC_OPERATOR_EMAILS) is the sole admin-tier source when no RoleMap
	// is set; an email on it always derives RoleAdmin, top precedence like a RoleMap match.
	LegacyAdminEmails []string
	// SecureCookies, when true, marks every cookie Secure (HTTPS only) and adds __Host-/Path=/. MUST
	// be true exactly when the connection is TLS-protected; false is required for plain-HTTP demo
	// deployments, since a Secure cookie is never sent over plain HTTP and login would silently break.
	SecureCookies bool
	// Revocations is the pg-backed revoke-a-human-now lever: sessions are stateless signed cookies
	// with nothing to delete, so this tracks a per-principal (and global) cutoff time and Middleware
	// treats any session issued at-or-before it as invalid. nil (default) means never checked.
	Revocations SessionRevocations

	// RoleMappings is the console-managed role-mapping store, merged with RoleMap on each login
	// (RoleMap wins a duplicate key). A read error DENIES the login rather than falling back to the
	// env-only map, which could widen access under DefaultRole=admin. nil is env-only derivation.
	RoleMappings RoleMappingSource

	// UserTypes is the store of user types, read once per login beside RoleMappings. A read error
	// denies the login. nil means only "standard" exists.
	UserTypes UserTypeSource

	// RoleStampTTL is WARDYN_ROLE_STAMP_TTL: the oldest a session's role stamp may be, measured from
	// the session's IssuedAt (the cookie's role is fixed then; only a sign-in changes it). An older
	// session is rejected as "role_stamp_stale" and its owner signs in again. A cookie without an
	// IssuedAt is older than any TTL. Zero is off. A session also ends at Expiry (the ID token's
	// expiry, or SessionTTL from sign-in when that is set), so this only matters when it is shorter.
	RoleStampTTL time.Duration

	// SessionTTL is WARDYN_OIDC_SESSION_TTL: when set, a console session lasts this long from
	// sign-in instead of ending at the ID token's expiry. Zero (the default) keeps the ID token's
	// expiry. Boot refuses more than 24 hours. Nothing re-checks the identity provider during a
	// session, so a longer session should be paired with RoleStampTTL.
	SessionTTL time.Duration

	// OnLogin, when set, is called synchronously after an APPROVED login with what that login
	// established (LoginFacts). A failure inside it must never fail the login; nil is a no-op.
	OnLogin func(ctx context.Context, facts LoginFacts)

	// Identities, when set, is the per-identity gate (identity_gate.go): admission refuses a
	// deactivated identity on every issuer, issuance records the identity row and stamps the
	// session with its authority epoch, and a Revocations that implements IdentityRevocations
	// refuses a cookie whose epoch a later suspension passed. nil is no gate.
	Identities IdentityGate
}

Config holds the OIDC client configuration. All fields except AllowedEmailDomains and ClientSecret (C2: optional, for a public client) are required.

type Derivation added in v0.8.0

type Derivation struct {
	Role     string
	UserType string
	Matches  []Match
	Denial   string
	// Tied is the custom types that tied at the top priority, sorted.
	Tied []string
	// Unknown is the named types that do not exist, sorted.
	Unknown []string
}

Derivation is what a sign-in derives: the tier, the user type, and every value that contributed. When Denial is set the sign-in is refused and Role and UserType are empty; Matches, Tied and Unknown still say why, which is what the People page's preview renders.

func (Derivation) OK added in v0.8.0

func (d Derivation) OK() bool

OK reports whether the derivation lets the sign-in through. The zero value (what an errored preview returns) is not OK.

type IdentityGate added in v0.8.6

type IdentityGate interface {
	// Refused reports, without writing, whether the identity a sign-in names is deactivated or
	// purged. An error fails the sign-in closed.
	Refused(ctx context.Context, ref IdentityRef) (bool, error)
	// Issue upserts and binds the sign-in's identity row, re-checks its deactivation and returns
	// the authority epoch the session cookie must carry, all under the row's lock so a suspension
	// either precedes it (ErrIdentityDeactivated) or leaves the cookie's epoch stale. An error
	// fails the sign-in closed.
	Issue(ctx context.Context, f LoginFacts) (epoch int64, err error)
	// Bound reports, without writing, the principal the identity row keyed by ref's issuer, tenant
	// and object id is bound to: "" when there is no such row or it is bound to no one yet. An error
	// fails the caller closed.
	Bound(ctx context.Context, ref IdentityRef) (string, error)
}

IdentityGate is the per-identity half of admission, for every issuer. nil (the default) is no gate: sign-ins record no identity row and carry no epoch.

type IdentityRef added in v0.8.6

type IdentityRef struct {
	Issuer, TenantID, ObjectID, Principal string
}

IdentityRef is who a sign-in names, as the gate's read-only admission check needs it.

type IdentityRevocations added in v0.8.6

type IdentityRevocations interface {
	SessionStatus(ctx context.Context, sub, email string, issue Issue, epoch int64) (SessionStatus, error)
}

IdentityRevocations is SessionRevocations that also reads the owner's identity rows in the same statement, so the identity check adds no round trip. epoch < 0 asks only whether the identity is deactivated or purged; otherwise a row whose authority epoch is past epoch refuses too.

type Issue added in v0.9.0

type Issue struct {
	At  time.Time
	Seq int64
}

Issue is where a credential sits against the revocation cuts: the wall-clock time it was issued and, when the issuer recorded one, its revocation sequence (RevocationOrder). A zero Seq is a credential issued before the sequence existed, or by a replica that predates it, and is compared on At alone. Migration 0176 ordered the API tokens and SSH keys that existed, so in practice these are session cookies, which expire with the session.

type LoginFacts added in v0.8.6

type LoginFacts struct {
	// Sub is the principal the session carries.
	Sub      string
	Role     string
	UserType string
	Groups   []string
	// GroupsTruncated reports Groups is a partial snapshot.
	GroupsTruncated bool
	// Issuer is the verified token's issuer.
	Issuer string
	// TenantID and ObjectID are Entra ID's tenant and object id, set together and only on an Entra
	// sign-in whose token carries both; empty for every other issuer.
	TenantID, ObjectID string
	// Email is the token's email claim as sent, possibly empty.
	Email string
}

LoginFacts is what an approved sign-in established, handed to Config.OnLogin: who signed in as whom, under which issuer, and the role, user type and group snapshot derived for the session.

type LoginGrant added in v0.7.10

type LoginGrant struct {
	// Generation is the sink's durable erasure snapshot taken before exchange.
	Generation int64
	// RefreshToken is the durable half of the grant; empty if the provider
	// issued none (`offline_access` not asked for, or not granted).
	RefreshToken string
	// Scope is the GRANTED scope string, verbatim — may differ from what was
	// requested; only the sink knows what it was hoping for.
	Scope string
	// Expiry is the access token's expiry, as reported by the exchange.
	Expiry time.Time
}

LoginGrant is what the token exchange returned besides identity. It carries no access token (stale within minutes) — the refresh token is the durable half, and only the party that stores it may redeem it.

type LoginGrantSink added in v0.7.10

type LoginGrantSink interface {
	// LoginGrantGeneration snapshots the sink before the exchange establishes
	// an owner. An error skips capture while the login proceeds.
	LoginGrantGeneration(ctx context.Context) (int64, error)
	// LoginScopes returns extra scopes to request, or nil. Called once per
	// authorization request.
	LoginScopes(ctx context.Context) []string
	// CaptureLoginGrant is offered the grant once login is APPROVED — past
	// every denial branch, so a refused login never yields a credential.
	// subject is the verified id_token subject the session is about to be
	// issued for.
	CaptureLoginGrant(ctx context.Context, subject string, grant LoginGrant)
}

LoginGrantSink is the seam: what to ADD to the authorization request, and what to DO with the grant. Both are best-effort by contract — a storage failure in CaptureLoginGrant must not undo an already-decided login. An implementation MUST NOT panic or block for long: it runs inside a browser's login redirect, with a human waiting.

type Match added in v0.7.0

type Match struct {
	// Value is the claim/email value that matched, exactly as carried. Empty for a default match.
	Value string `json:"value"`
	// Role is the tier this value maps to; UserType is set only when Role is the user tier.
	Role     string      `json:"role"`
	UserType string      `json:"user_type,omitempty"`
	Source   MatchSource `json:"source"`
}

Match is one claim/email value that contributed to a derived role, with where it came from. deriveRole returns every match found, not only the winner, so a caller can show WHY. json tags are POST /access/preview's wire shape.

type MatchSource added in v0.7.0

type MatchSource string

MatchSource names which config source produced a Match: a role-map row, the operator-email allowlist, or DefaultRole's fallthrough.

const (
	MatchSourceMapRow            MatchSource = "map_row"
	MatchSourceOperatorAllowlist MatchSource = "operator_allowlist"
	MatchSourceDefaultRole       MatchSource = "default_role"
)

type PersonKeying added in v0.8.2

type PersonKeying interface {
	// PrincipalFor is the principal s signs in as: the person keyed by exactly s.Issuer,
	// s.TenantID and s.ObjectID if there is one and s.Sub names no one yet, else s.Sub. refused is true when s.Sub itself
	// names a person keyed by an object id this token does not carry. An email never enters it.
	PrincipalFor(ctx context.Context, s Subject) (principal string, refused bool, err error)
	// Attached records an admitted sign-in that resolved to such a person, naming both parties.
	Attached(r *http.Request, s Subject, principal string)
}

PersonKeying resolves an Entra ID sign-in to a person an admin set up by object id before their first sign-in (#1195). Entra's `sub` is pairwise — per application registration, unknowable until that first sign-in — so such a person is keyed by (issuer, tid, oid) instead. Consulted only when KeysPeopleByObjectID; every other issuer signs in as its `sub`, exactly as before.

type RevocationOrder added in v0.9.0

type RevocationOrder interface {
	RevocationSeq(ctx context.Context) (int64, error)
	// IsSessionRevokedSeq is IsSessionRevoked for a credential that recorded seq (> 0): revoked when a
	// matching cut drew a greater sequence, with no comparison of clocks.
	IsSessionRevokedSeq(ctx context.Context, sub, email string, seq int64) (bool, error)
}

RevocationOrder is a SessionRevocations that orders cuts by a logical sequence, which a step of the host clock cannot reorder. RevocationSeq is the value a credential issued now records: a cut made after it returns draws a greater one, and a cut whose value it does not cover has committed before it was read.

type RoleMapping added in v0.7.0

type RoleMapping struct {
	Value    string
	Role     string
	UserType string
}

RoleMapping is one console-managed value=>role row. Value is EXPECTED already canonical (the API write boundary owns that); mergeRoleMaps does not trust it blindly — a non-canonical row or invalid Role is DROPPED AND LOGGED rather than stored verbatim.

type RoleMappingSource added in v0.7.0

type RoleMappingSource interface {
	// ListRoleMappings: a non-nil error DENIES the login rather than falling back to env-only,
	// which could WIDEN access under WARDYN_OIDC_DEFAULT_ROLE=admin.
	ListRoleMappings(ctx context.Context) ([]RoleMapping, error)
}

RoleMappingSource is the console's store-backed role-mapping source, read once per login and merged with the chart's WARDYN_OIDC_ROLE_MAP. nil (the default) disables the console source.

type Session

type Session struct {
	// V is the payload format version (SessionCodecVersion); any other value, including an absent
	// key, is not a session.
	V     int    `json:"v"`
	Sub   string `json:"sub"`
	Email string `json:"email"`
	// Name is the IdP's display-name claim, for the console header ONLY — gates nothing, keys
	// nothing, never logged. omitempty: an absent key falls back to email (fail-safe).
	Name string `json:"name,omitempty"`
	// ObjectID is the Entra object id (`oid`) the ID token named, stamped only on an Entra sign-in.
	// It is an identity INPUT (ObjectIDFromContext: an own Azure DevOps token is bound to the person
	// by it), never logged and never echoed. omitempty: a cookie without it is still a session and
	// the reader falls back to the person row or the email, so its absence is never a refusal.
	ObjectID string    `json:"oid,omitempty"`
	Role     string    `json:"role"`
	Expiry   time.Time `json:"expiry"`
	// UserType is stamped beside the tier at sign-in; everyone carries "standard" until the role
	// map names custom ones.
	UserType string `json:"ut"`
	// IssuedAt is when CallbackHandler minted this cookie, compared against a revoke cutoff by
	// IsSessionRevoked. omitempty: an absent key decodes to the zero time either way.
	IssuedAt time.Time `json:"iat,omitempty"`
	// AuthorityEpoch is the identity's authority epoch when this cookie was minted (Config.Identities).
	// A suspension raises the epoch, so a cookie minted before it is refused at its next request, and
	// stays refused after a reactivation. omitempty: a cookie without it is epoch 0.
	AuthorityEpoch int64 `json:"ae,omitempty"`
	// RevocationSeq is the revocation sequence read just before the sign-in's identity was admitted
	// (RevocationOrder). A revocation cut with a greater sequence ends this session, whatever the
	// clock says. omitempty: a cookie without it was issued by a replica that predates it and is
	// compared on IssuedAt, as before.
	RevocationSeq int64 `json:"rseq,omitempty"`
	// Groups is the LOGIN-TIME SNAPSHOT of the human's group identity (normalized union of the ID
	// token's "roles"/"groups" claims), against which a `group`-subject grant matches. Nothing
	// refreshes it — a group added at the IdP reaches Wardyn on the next login. NO omitempty,
	// deliberately: nil vs empty is the only signal for groups_snapshot_stale (nil = "never asked").
	Groups []string `json:"groups"`
	// GroupsTruncated reports Groups is a PARTIAL snapshot (hit maxSessionGroupsBytes). AUTHORIZATION
	// INPUT, not telemetry: without it the ceiling resolver would silently widen instead of refusing.
	GroupsTruncated bool `json:"groups_truncated,omitempty"`
	// MemberMode is the "view as member" flag. CLAMP INPUT only — Role stays the sign-in-derived
	// truth; the effective role is only ever LOWERED, keeping a cookie flag from being a privilege
	// primitive. omitempty: absent decodes to false (fail-safe); a rolling k8s upgrade can let an
	// old replica answer as admin for that window (documented ceiling, not proof of refusal).
	MemberMode bool `json:"mm,omitempty"`
	// MemberModeNoCredential is the mode's second posture: "view as a NEW member, not yet signed
	// in". Clamps the ROLE only — Sub (ownership, secrets, audit rows) stays the admin's own.
	MemberModeNoCredential bool `json:"mmnc,omitempty"`
	// UserViewType is the user type the user view looks through; the view always clamps the tier
	// to user regardless.
	UserViewType string `json:"uvt,omitempty"`
	// UserViewTypeName caches that type's display name from the instant the
	// view entered it (SetUserView). Meaningful only with MemberMode, same as
	// UserViewType. It exists ONLY so DropUserView can still name the type in
	// UserViewDroppedName after its row is gone — the row's deletion is WHY
	// the drop fires, so nothing can look the name up again at that point.
	UserViewTypeName string `json:"uvtn,omitempty"`
	// UserViewDropped names the type whose deletion turned the view off, so GET /me can say why
	// until the next switch clears it.
	UserViewDropped string `json:"uvd,omitempty"`
	// UserViewDroppedName is that same type's display name, copied from
	// UserViewTypeName the instant DropUserView fires, before it is cleared —
	// see UserViewTypeName's doc for why this is the only chance to record it.
	UserViewDroppedName string `json:"uvdn,omitempty"`
	// contains filtered or unexported fields
}

Session is the content of the wardyn_session cookie, signed and stored client-side. Role is always non-empty in a cookie this package issues — CallbackHandler denies rather than write an undefined role — and decodeSession treats an empty Role (pre-0.5, or corrupt) as no session.

type SessionCutter added in v0.8.6

type SessionCutter interface {
	// CutSessions invalidates every current browser session for sub, matched against both identities.
	CutSessions(ctx context.Context, sub string) error
}

SessionCutter is SessionRevocations that can also end a person's browser sessions without ending their API tokens and SSH keys. A cut is read by IsSessionRevoked and by SessionStatus for a credential that carries an epoch (a session cookie), never by SessionStatus for one that does not (epoch < 0: an API token, an SSH key). A mover keeps the tokens whose group snapshot does not hold the group they left, so the group removal cuts with this and revokes the tokens it means to one by one.

type SessionRevocations added in v0.6.0

type SessionRevocations interface {
	// IsSessionRevoked reports whether a session for this human, issued at issuedAt, must be treated
	// as revoked, by a revoke targeting either identity or the reserved "" (global) sub, whichever
	// cutoff is later. A zero issuedAt is always revoked once any matching cutoff exists (fail
	// closed). Checked against BOTH identities since sub and email can diverge (Entra's opaque
	// per-app sub): sub compares exactly, email case-insensitively.
	//
	// ponytail: no in-process cache; add a short-TTL cache keyed on sub if latency shows up.
	IsSessionRevoked(ctx context.Context, sub, email string, issuedAt time.Time) (bool, error)
	// RevokeSub invalidates every CURRENT session for sub, matched against both identities.
	RevokeSub(ctx context.Context, sub string) error
	// RevokeAll invalidates every CURRENT session for every principal.
	RevokeAll(ctx context.Context) error
}

SessionRevocations is the store the revoke-a-human-now admin action reads and writes. Scoped to the stateless OIDC session cookie — distinct from internal/identity's per-run SPIFFE denylist.

type SessionStatus added in v0.8.6

type SessionStatus int

SessionStatus is what a credential's owner check answers.

const (
	SessionLive SessionStatus = iota
	SessionRevoked
	SessionDeactivated
)

The three answers: usable, cut off by a revocation cutoff, or refused by the identity row.

func CheckSession added in v0.8.6

func CheckSession(ctx context.Context, rev SessionRevocations, sub, email string, issue Issue, epoch int64) (SessionStatus, error)

CheckSession is the one owner check every credential lane asks: the revocation cutoff and, when rev can answer it, the owner's identity. A rev that cannot (a test double) checks the cutoff only.

type Subject added in v0.8.2

type Subject struct {
	Issuer, Sub, TenantID, ObjectID string
}

Subject is who a verified token names, as PersonKeying reads it: the issuer and raw `sub` every token carries, and Entra ID's tenant (`tid`) and tenant-stable object id (`oid`) claims.

type UserTypeSource added in v0.8.0

type UserTypeSource interface {
	ListUserTypes(ctx context.Context) ([]types.UserType, error)
}

UserTypeSource is the store of user types, read once per login. A non-nil error DENIES the login: a type that can't be looked up can't be shown to exist, and falling back to "standard" could widen a narrower type. Config.UserTypes wires it; nil means only the built-in type exists.

Jump to

Keyboard shortcuts

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