auth

package
v0.5.5 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

README

plugin/auth

Browser authentication and bearer-token API authentication. Importing the package registers the [auth] configuration binding and the framework extensions that authenticate requests and guard protected paths.

Slot Middleware
pw.SlotSession resolves the session cookie
pw.SlotAuthentication serves the login, callback, and logout paths
pw.SlotGuard rejects unauthenticated requests to protected paths
import _ "github.com/shibukawa/popcornweb/plugin/auth"

Nothing is installed unless auth.enabled is true, so an imported but disabled package costs one configuration binding.

Modes

  • oidc_only uses Authorization Code with S256 PKCE, state, and nonce against one configured issuer.
  • oidc_passkey uses OIDC to establish the account and permits later passkey login.
  • passkey_only bootstraps the first passkey from an administrator-issued one-time credential.
  • jwt_only verifies an access token from Authorization: Bearer … on every request. It mounts no login endpoint and creates no session or cookie. This is the API-server mode.

jwt_only is not one of the browser-oriented pw init --auth values. Use pw init --preset=api-server, or configure an existing project by hand. See the authentication guide.

Tables

The tables this package owns are prefixed popcornweb_ and are created by the migration MigrationSQL publishes, which a project carries under MigrationName at whatever version was free when the file was written:

  • popcornweb_authstate — single-use state, nonce, and PKCE verifier of a pending login, consumed by the callback
  • popcornweb_auth_allowlist — identities registered before their first login, consulted only under registered admission

sessionstore/sqlite owns popcornweb_session through its own migration. Startup verifies every one of them and refuses to serve when one is missing, naming the migration to apply, so a forgotten migration fails immediately rather than during a login.

Flow

GET /auth/login begins authorization and stores the opaque transaction key in a short-lived cookie scoped to the callback path. The state, nonce, and PKCE verifier never reach the browser.

GET /auth/callback consumes that cookie once, exchanges the code, verifies the ID Token, applies admission, resolves the account, and rotates the session. Every admission failure produces one response shape, so the endpoint does not report whether an account exists.

POST /auth/logout revokes the stored session, expires the cookie, and then ends the provider session through RP-initiated logout. It requires a same-origin submission. Clearing only the local cookie would leave the provider signed in, so the next login returns the same user without asking and the sign out appears to have done nothing. auth.oidc.provider_logout = false keeps the logout local, for a provider shared with applications that must stay signed in.

Discovery runs on the first login rather than at startup, so the application starts even when the provider is not up yet, and a failed discovery is not cached.

Which claim identifies an account

auth.oidc.identity_claim names the verified claim that identifies a local account. It defaults to sub, the only claim OpenID Connect guarantees is stable and unique per issuer.

A subject is generated by the provider, so a deployment that provisions people in advance rarely knows one. Directories are commonly given their own stable identifier for exactly this reason — an employee number, a staff id — and that claim is what an operator can register, hand out, and reconcile against an HR system:

[auth.oidc]
identity_claim = "employee_number"

auth.Identity then carries KeyClaim and Key, which the account resolver links on and which the login session records. The subject stays available.

The chosen claim becomes the account link, so it must be stable for the life of the account and unique within the issuer. A value that is reissued to another person hands them the first person's account, and changing the setting after accounts exist orphans every account linked by the previous claim.

A login whose token does not carry the configured claim, or carries it in an unusable shape, is refused rather than falling back to the subject: a silent fallback would create a second account for the same person. A string is used as it is, an integer as its literal text, and every other JSON shape — including a fractional number — is refused rather than normalized.

Admission and accounts

auth.oidc.admission decides whether a verified identity may enter: authenticated admits every identity the issuer verifies, claim admits a verified claim match, registered admits an identity listed in popcornweb_auth_allowlist, and existing admits only an identity the resolver already knows and forbids provisioning.

registered is the closed-deployment mode. An operator inserts one row per permitted identity, naming a claim and its expected value:

INSERT INTO popcornweb_auth_allowlist (issuer, claim, value, note)
VALUES ('https://issuer.example', 'employee_number', 'E-10231', 'first operator');

auth.oidc.registered_claims selects the compared claims and defaults to the configured identity_claim alone, because that is the value a deployment knows in advance. List further claims to also recognize someone registered by another attribute, such as an email address during a migration. A lookup failure is reported as an error rather than a denial, so a database outage cannot silently change who may log in.

SetAccountResolver installs the application resolver. It receives the verified identity — issuer, lookup claim and value, subject, and claims — plus whether policy permits provisioning, and returns the local account or ErrUnknownIdentity. Without a resolver, a stable opaque identifier is derived from the issuer and the lookup claim and value.

Development-only settings

auth.oidc.allow_loopback_http permits an http issuer on loopback, which is what contrib/devidp serves. Together with session.cookie.secure = false it belongs to development configuration only.

pw dev starts that provider and injects AUTH_OIDC_ISSUER, AUTH_OIDC_CLIENT_ID, and AUTH_OIDC_CLIENT_SECRET, so a project commits no issuer or credential.

See examples/oidclogin for a working application.

Documentation

Overview

Package auth adds browser authentication and bearer-token API authentication to a Popcorn Web application. Importing it registers the auth configuration binding and the framework extensions that authenticate requests and guard protected paths.

import _ "github.com/shibukawa/popcornweb/plugin/auth"

Browser modes use OIDC, passkeys, or both and establish a session in the backend session.backend selects. auth.mode = "jwt_only" instead verifies an Authorization bearer token on every request and creates no session or login endpoint.

session.backend = cookie warns outside dev rather than refusing: a login this package can end on demand needs a record on the server, and a browser keeps what it was given. In dev that is the right trade and nothing is said.

This package imports no storage plugin. It asks pw for the configured backend, so an application links the storage it configured and no more. General server backends need blank imports; cookie and both development intent modes are built into pw.

What it reads through pw and what it no longer does

The settings and the environment come from popcornweb/pwconfig, and the request state from popcornweb/pwruntime, so the decisions in this package are reachable from a build serving on either transport — which is what popcornweb/plugin/auth/authfast then does.

What is still pw's is what is genuinely the net/http runtime's: the extension registry this registers into, the session manager it drives, and the connection group a session's storage is pinned to. Each is a layer of its own, and each has to move before this package can be linked without pw.

Index

Constants

View Source
const (
	// MethodOIDC labels sessions created by the OIDC flow.
	MethodOIDC = "oidc"
	// MethodPasskey labels sessions created by a passkey assertion.
	MethodPasskey = "passkey"
)

Authentication method names recorded on a session and reported by pw.RequestAuthentication. An application compares against these rather than against a literal.

View Source
const (
	BackendRDB       = "rdb"
	BackendDynamo    = "dynamo"
	BackendFirestore = "firestore"
)

Backend names. rdb is the default and the behavior every project had before a second one existed.

View Source
const (
	ModeOIDCOnly    = "oidc_only"
	ModeOIDCPasskey = "oidc_passkey"
	ModePasskeyOnly = "passkey_only"
	ModeJWTOnly     = "jwt_only"
)

Authentication modes.

ModeJWTOnly is deliberately absent from the api:cli-init capability catalog: it authenticates an API caller that already holds a token from an authorization server this framework does not run, so scaffolding it would scaffold a dependency the project does not have. See decision:jwt-only-mode-not-scaffolded.

View Source
const (
	// RevocationOff accepts every verified token until it expires.
	RevocationOff = "off"
	// RevocationToken revokes one token by its jti claim.
	RevocationToken = "token"
	// RevocationSubject revokes every token issued to an identity before a
	// stamp, which is what a compromised account needs and what enumerating
	// jti values cannot do.
	RevocationSubject = "subject"
	// RevocationBoth is the ordinary answer; neither form substitutes for the
	// other.
	RevocationBoth = "both"
)

Revocation modes of ModeJWTOnly. There is no default: a deployment states whether it can revoke a token, because the permissive answer must not arrive as one nobody typed.

View Source
const (
	// RevocationRefuse fails closed, which is the default.
	RevocationRefuse = "refuse"
	// RevocationAdmit keeps serving while the store is down, which makes
	// revocation advisory for the duration. It is an incident lever rather
	// than a deployment posture.
	RevocationAdmit = "admit"
)

What a revocation lookup does when the store cannot answer.

View Source
const (
	// DiscoveryOIDC reads /.well-known/openid-configuration.
	DiscoveryOIDC = "oidc"
	// DiscoveryOAuth reads the RFC 8414 authorization server metadata.
	DiscoveryOAuth = "oauth"
	// DiscoveryManual takes auth.jwt.jwks_uri and fetches no metadata.
	DiscoveryManual = "manual"
)

How the signing keys of the issuer are found.

View Source
const (
	// AdmissionAuthenticated admits every identity the configured issuer
	// verifies.
	AdmissionAuthenticated = "authenticated"
	// AdmissionClaim admits an identity whose configured claim matches.
	AdmissionClaim = "claim"
	// AdmissionExisting admits only an identity an application resolver
	// already knows. It forbids provisioning.
	AdmissionExisting = "existing"
	// AdmissionRegistered admits only an identity a deployment registered in
	// advance in the framework-owned allowlist table. It is the mode for a
	// closed deployment whose users are known before their first login.
	AdmissionRegistered = "registered"
)

Admission policies applied to a verified OIDC identity.

View Source
const (
	UnauthenticatedRedirect     = "redirect"
	UnauthenticatedUnauthorized = "unauthorized"
)

Responses to an unauthenticated request on a protected path.

View Source
const (
	MatchAny = "any"
	MatchAll = "all"
)

Claim match modes.

View Source
const (
	UserVerificationRequired    = "required"
	UserVerificationPreferred   = "preferred"
	UserVerificationDiscouraged = "discouraged"
)

Ceremony user verification levels, as WebAuthn names them.

View Source
const (
	DiscoverableRequired  = "required"
	DiscoverablePreferred = "preferred"
)

Discoverable credential requirements.

View Source
const (
	RegistrationDisabled      = "disabled"
	RegistrationOIDC          = "oidc"
	RegistrationInvite        = "invite"
	RegistrationAdministrator = "administrator"
	RegistrationOpen          = "open"
)

Registration policies. They decide how an account comes into existence in a deployment that does not get one from an identity provider.

View Source
const (
	RecoveryOIDC          = "oidc"
	RecoveryAdministrator = "administrator"
	RecoveryApplication   = "application"
)

Recovery authorities. A deployment names one before it enables registration, because an unrecoverable account is a support incident rather than a policy.

View Source
const (
	LogoutScopeReconfirm = "reconfirm"
	LogoutScopeGlobal    = "global"
)

Logout scopes decide what a logout does to the provider session. There is no local-only scope: see OIDCConfig.LogoutScope.

View Source
const (
	PurposeInitialPasskey  = "initial_passkey"
	PurposeRecoveryPasskey = "recovery_passkey"
)

Bootstrap credential purposes, from data:account-bootstrap-credential.

View Source
const AllowlistTable = "popcornweb_auth_allowlist"

AllowlistTable holds the identities a deployment registered in advance. It is consulted by AdmissionRegistered.

View Source
const BootstrapTable = "popcornweb_auth_bootstrap"

BootstrapTable holds the issued credentials that open a first passkey enrollment, for the default BootstrapStore.

View Source
const ClaimSubject = "sub"

ClaimSubject is the default identity claim and the only one OpenID Connect guarantees is stable and unique for an issuer.

View Source
const CredentialTable = "popcornweb_passkey_credential"

CredentialTable holds the passkey credentials of the default CredentialStore. A deployment that installs its own store owns its own table and this one is neither created nor verified.

View Source
const MethodBearer = "bearer"

MethodBearer labels a request authenticated by a bearer access token. It never labels a session, because this mode creates none.

View Source
const MigrationName = "init_popcornweb_auth"

MigrationName is the stable name of the migration a project carries for the tables this package owns, without a version. See rdb.MigrationName for why the version belongs to the project rather than to the package.

View Source
const RevocationTable = "popcornweb_auth_revocation"

RevocationTable holds the tokens and identities this deployment has withdrawn.

A row is a positive statement that something is revoked. Absence means not revoked, so a lookup that cannot run is not an absence: it is an unknown, and policy:token-revocation fails closed on it.

Variables

View Source
var (
	// ErrAccessDenied rejects a verified identity that admission or account
	// state does not admit. It is answered with one enumeration-safe response.
	ErrAccessDenied = errors.New("auth: access denied")
	// ErrUnknownIdentity is returned by an account resolver that found no local
	// account for a verified identity.
	ErrUnknownIdentity = errors.New("auth: unknown identity")
)
View Source
var (
	// ErrNoCredential reports a request that carried no bearer token. It is not
	// a failure on its own: an unprotected path serves it anonymously.
	ErrNoCredential = errors.New("auth: no bearer credential")
	// ErrInvalidToken reports a credential that did not verify.
	ErrInvalidToken = errors.New("auth: invalid bearer token")
	// ErrRevokedToken reports a verified token that was withdrawn.
	ErrRevokedToken = errors.New("auth: revoked bearer token")
)

errBearer categories. They are deliberately coarse: a caller learns that the credential was not accepted, never which check rejected it, because the difference between "wrong audience" and "expired" is a probing oracle.

View Source
var (
	// ErrUnknownCredential means no stored credential carries that ID.
	ErrUnknownCredential = errors.New("auth: unknown passkey credential")
	// ErrUnknownBootstrap means no issued bootstrap credential carries that
	// login ID, or the one that does is spent.
	ErrUnknownBootstrap = errors.New("auth: unknown bootstrap credential")
)
View Source
var ErrNoAssurance = errors.New("auth: assurance is unavailable")

ErrNoAssurance reports that assurance could not be evaluated because the auth plugin is not running. It is returned rather than assumed, because assuming either answer would be wrong: assuming satisfied opens the guard, and assuming unsatisfied sends an anonymous deployment into a login it has no endpoint for.

Functions

func AdmitAssurance

func AdmitAssurance(x Exchange, requirement Requirement, api bool) bool

AdmitAssurance evaluates the requirement and, when it is unmet, writes the response that starts the step-up. It reports whether the handler may run.

It is exported because the frame that wraps a handler is each transport's own, while what the requirement means is this package's.

func BootstrapSecretDigest

func BootstrapSecretDigest(loginID, secret string) []byte

BootstrapSecretDigest is the digest a BootstrapStore persists. Issuing code calls it once with the generated secret and never stores the secret itself.

func Challenge

func Challenge(w http.ResponseWriter, r *http.Request, requirement Requirement, api bool)

Challenge writes the response an unmet requirement produces and starts the step-up. Pass api = true from a route whose caller is not a browser.

func ChallengeOn

func ChallengeOn(x Exchange, requirement Requirement, api bool)

ChallengeOn is Challenge over the transport seam.

func Ensure

func Ensure(handler http.HandlerFunc, requirement Requirement) http.HandlerFunc

Ensure wraps a page route: a request whose proof is older than the requirement is redirected into re-proof and returns to this operation.

Guarding a read route is user experience rather than a boundary, because a client can post directly to the write route. Guard both, and give the write the more generous window so a user who reads, fills a long form, and submits does not lose the input to a guard the read had just satisfied.

func EnsureAPI

func EnsureAPI(handler http.HandlerFunc, requirement Requirement) http.HandlerFunc

EnsureAPI wraps an API route: an unmet requirement answers 401 with a problem document naming the window, so a client can start the step-up itself.

This is a separate function rather than an option with a default because the two failure modes are not symmetric. Answering an API call with a redirect is followed transparently by an XHR, so the client receives 200 and an HTML login page instead of an error; answering a page with 401 is merely ugly. A route that forgot to pass an option would take the silent failure.

func EstablishSession

func EstablishSession(w http.ResponseWriter, r *http.Request, data SessionData, method string) error

EstablishSession creates the login session of an account this application authenticated through a flow the framework does not own, and writes its cookie to w. It rotates whatever session the browser already held, exactly as the built-in login endpoints do.

It authenticates nobody: the caller has already decided that this request belongs to this account, and is responsible for having verified something before deciding it. Nothing a remote client sends can reach this function.

method labels the session for data:request-authentication and appears as pw.RequestAuthentication(ctx).Method. Use MethodOIDC, MethodPasskey, or an application-defined name.

func EstablishSessionOn

func EstablishSessionOn(x Exchange, data SessionData, method string) error

EstablishSessionOn is EstablishSession over the transport seam, for an application serving on a transport whose request is not a *http.Request.

func ForgetAccount

func ForgetAccount(accountID string)

ForgetAccount makes a suspension, deletion, or credential change take effect immediately in this process rather than within the revalidation interval.

Call it from whatever suspends or removes an account. It is not required for correctness: without it the change still lands, within the interval. It is also process-local, so a deployment running several instances still waits the interval on the others, and that interval is what the deployment can promise.

It never grants: forgetting an account can only cause it to be read again.

func GenerateBootstrapSecret

func GenerateBootstrapSecret() (string, error)

GenerateBootstrapSecret returns a high-entropy single-use secret. A deployment shows it once at issuance and stores only its digest. A user-chosen temporary password is not accepted anywhere, so this is the only way a secret comes into existence.

func IsRecent

func IsRecent(r *http.Request, requirement Requirement) bool

IsRecent reports whether the requirement is met and writes nothing. Use it with Challenge when the window depends on something only the handler knows, such as a payment amount. It is split from Challenge because a function that both returns a decision and writes a response hides control flow.

func IsRecentOn

func IsRecentOn(x Exchange, requirement Requirement) bool

IsRecentOn is IsRecent over the transport seam.

func IssueBootstrapCredential

func IssueBootstrapCredential(ctx context.Context, loginID, accountID, purpose string) (string, error)

IssueBootstrapCredential generates a single-use secret, stores only its digest, and returns the secret for the administrator to deliver out of band. The raw secret is returned exactly once and is never stored or logged.

It is the framework side of flow:passkey-only-registration; deciding who may call it, and through which channel the secret travels, stays with the application.

func LastProvedAt

func LastProvedAt(ctx context.Context) (time.Time, bool)

LastProvedAt reports when the identity of the request was last actually proved, so a page can warn before the user commits to a long form. The bool is false for a request carrying no session.

func MaskIdentifier

func MaskIdentifier(value string) string

MaskIdentifier renders an identifier for a login screen without showing the whole of it, because a shared browser shows it to whoever comes next.

An issuer is deliberately not maskable: a login screen either offers the button or does not, so there is no partial form of it. Whether an issuer may be remembered at all is HintConfig.Enabled and its lifetime, not a rendering choice.

func MigrationSQL

func MigrationSQL(dialect string) (string, error)

MigrationSQL returns the goose migration that creates the tables this package owns under one engine: the single-use OIDC correlation records and the pre-registration allowlist.

func RegisterBackend

func RegisterBackend(name string, factory BackendFactory)

RegisterBackend records a storage backend under its configuration name.

A backend package registers itself from init, so a project links the backend it runs and no other. Registering the same name twice is a programming error rather than a configuration one.

func ReinstateSubject

func ReinstateSubject(ctx context.Context, issuer, identityKey string) error

func ReinstateToken

func ReinstateToken(ctx context.Context, issuer, tokenID string) error

ReinstateToken and ReinstateSubject remove a revocation issued in error.

Reinstating is not an undo that hides what happened: the entry is deleted, so every unexpired token it was refusing works again at the next request.

func RevokeSubject

func RevokeSubject(ctx context.Context, issuer, identityKey, note string) error

RevokeSubject withdraws every token issued to an identity before now.

It is the broad act, for a compromised account: enumerating the outstanding token identifiers is exactly what nobody can do. The identity works again once it authenticates afresh, because the stored stamp is compared against the token's iat.

func RevokeToken

func RevokeToken(ctx context.Context, issuer, tokenID, note string) error

RevokeToken withdraws one access token by its jti, for the running application.

It is the narrow act: a credential leaked and the identity that holds it is otherwise fine.

func SetAccountActivator

func SetAccountActivator(activate AccountActivator)

SetAccountActivator installs the application activation step of flow:passkey-only-registration. Without one the framework persists the credential and consumes the bootstrap credential, and the application is responsible for whatever "active" means to it.

func SetAccountLookup

func SetAccountLookup(lookup AccountLookup)

SetAccountLookup installs the application account lookup. Without one a passkey session carries the account identifier alone, which is enough to authenticate and authorize but shows the user no name.

func SetAccountResolver

func SetAccountResolver(resolve AccountResolver)

SetAccountResolver installs the application account resolver. Call it from main before pw.Run. Without a resolver the framework derives a deterministic account identifier from the verified issuer and subject, which suits a deployment that keeps no local account table.

func SetAllowlistStore

func SetAllowlistStore(store AllowlistStore)

SetAllowlistStore installs the application allowlist store. Call it from main before pw.Run. Installing one means the framework creates and verifies no table for this capability, exactly as SetCredentialStore does.

func SetBootstrapStore

func SetBootstrapStore(store BootstrapStore)

SetBootstrapStore installs the application bootstrap credential store.

func SetCredentialStore

func SetCredentialStore(store CredentialStore)

SetCredentialStore installs the application credential store. Call it from main before pw.Run. Without one the framework uses its own table, because persisting a sign counter atomically is protocol correctness rather than application domain, and getting it wrong has no symptom until an attack.

func SubjectRevoked

func SubjectRevoked(ctx context.Context, issuer, identityKey string) (time.Time, bool, error)

func TokenRevoked

func TokenRevoked(ctx context.Context, issuer, tokenID string) (time.Time, bool, error)

TokenRevoked and SubjectRevoked report the current state and the stamp, for an administrative view that must not guess. They read the store rather than the request cache.

Types

type Account

type Account struct {
	// ID is the stable opaque application identifier. It must not be an email
	// address.
	ID          string
	DisplayName string
	Email       string
	// Suspended blocks session creation for an account that exists but may not
	// log in.
	Suspended bool
}

Account is the local account a verified identity resolved to. Applications own account storage; this is only what the session needs.

type AccountActivator

type AccountActivator func(ctx context.Context, accountID string) error

AccountActivator activates a provisional account. It runs inside the transaction that persists the first passkey, so the account never becomes active without a credential and never gains a credential without becoming active.

type AccountLookup

type AccountLookup func(ctx context.Context, accountID string) (Account, error)

AccountLookup returns the account of a stable identifier. A passkey login resolves a credential to an account ID, which AccountResolver cannot answer because it starts from a verified external identity instead.

type AccountResolver

type AccountResolver func(ctx context.Context, identity Identity, provision bool) (Account, error)

AccountResolver resolves or provisions the local account of a verified identity. Returning ErrUnknownIdentity means no local account exists, which admission then interprets according to policy.

provision reports whether policy permits creating an account during this login.

type AllowlistCandidate

type AllowlistCandidate struct {
	Claim string
	Value string
}

AllowlistCandidate is one verified claim of a login offered to the store as a possible pre-registration match.

type AllowlistStore

type AllowlistStore interface {
	Registered(ctx context.Context, issuer string, candidates []AllowlistCandidate) (bool, error)
}

AllowlistStore answers the pre-registration question of the registered admission mode.

Registered receives every compared claim the verified identity carries, so one login is one lookup whatever backs the store. A lookup failure is an error and never a denial: reporting an outage as "not registered" would turn it into a silent access change.

type AssuranceConfig

type AssuranceConfig struct {
	// Policy is an array of tables rather than a map, because configbind binds
	// statically and a map key is not a declared field:
	//
	//	[[auth.assurance.policy]]
	//	name = "admin"
	//	max_age = "15m"
	Policy   []AssurancePolicy `help:"named freshness windows a handler can require by name"`
	Hint     HintConfig        `help:"what the login screen may remember about the last user of a browser"`
	Presence PresenceConfig    `help:"end a session when nobody is at the keyboard, rather than when no request arrives"`
}

AssuranceConfig holds the named freshness windows a handler requires by name, so the same handler code serves a consumer deployment with a long window and an internal one with a short window.

type AssurancePolicy

type AssurancePolicy struct {
	Name   string        `help:"name a handler passes to auth.Policy"`
	MaxAge time.Duration `help:"how old a proof may be; zero means prove again for this operation"`
	// Confirm refuses to count the login that started the session, so only a
	// re-proof this guard asked for satisfies the policy.
	//
	// Signing in and confirming an operation are different acts. Without this,
	// a window wide enough to be usable lets a sign-in stand in for the
	// confirmation: someone who signed in a minute ago to read their dashboard
	// would reach a transfer without ever being asked about it.
	Confirm bool `default:"false" help:"require a re-proof this guard asked for; a login never counts"`
}

AssurancePolicy names one freshness window. A zero MaxAge is meaningful and means prove again for this operation; it is not an unset field.

type Backend

type Backend struct {
	// OpenState opens the ceremony store of one namespace.
	OpenState func(ctx context.Context, namespace string) (authstate.RawStore, error)
	// Allowlist, Credentials, and Bootstrap are the account-side stores. Each
	// may be nil when the selected mode never reads it.
	Allowlist   AllowlistStore
	Credentials CredentialStore
	Bootstrap   BootstrapStore
}

Backend is one storage implementation of the four stores this package owns.

The ceremony store is opened per namespace rather than supplied whole, because this package keeps three kinds of ceremony record and two of their types are unexported: a backend package could not name them to build a typed store, so it supplies raw storage and the codec is added back here.

type BackendFactory

type BackendFactory func(ctx context.Context, config Config, resources Resources) (Backend, error)

BackendFactory opens one backend. It is registered under a name, and auth.backend selects which name is used.

type BearerIdentity

type BearerIdentity struct {
	// AccountID is the local account the identity resolved to.
	AccountID string
	// Account is the resolver's summary of that account.
	Account Account
	// Identity is the verified external identity, including its claims.
	Identity Identity
	// IssuedAt and ExpiresAt are the verified iat and exp.
	IssuedAt  time.Time
	ExpiresAt time.Time
}

BearerIdentity is what a handler reads about the caller behind a bearer request. It is the verified identity plus the account admission resolved.

It carries no token body: policy:access-token-verification excludes it, and a handler that needs to call another service must obtain its own credential rather than replay this one.

func Bearer

func Bearer(ctx context.Context) (BearerIdentity, bool)

Bearer returns the verified caller behind a bearer request.

It reports false for a browser session, because the two modes publish different things: a session has an account summary that outlives the request, and a bearer request has a token that does not.

type BootstrapConfig

type BootstrapConfig struct {
	// IssueTTL is how long an issued secret stays redeemable, measured from
	// issuance. It spans delivery, so it is the longer of the two.
	IssueTTL time.Duration `default:"24h" secret:"show" help:"how long an issued secret stays redeemable"`
	// EnrollmentTTL is how long the enrollment stays open, measured from a
	// successful redemption. It spans one ceremony, so it is short.
	//
	// It is not a session lifetime: what a redemption grants is a ticket that
	// authorizes exactly one registration, and the request stays unauthenticated
	// until that registration finishes.
	EnrollmentTTL time.Duration `default:"10m" secret:"show" help:"how long an enrollment stays open after a redemption"`
	// MaxAttempts bounds how many redemptions may be tried before the
	// credential is spent, whether or not any of them was close.
	MaxAttempts int `default:"5" secret:"show" help:"redemption attempts before the credential is spent"`
}

BootstrapConfig bounds the issued credential that opens a first passkey enrollment. See policy:bootstrap-credential-security.

The two durations bound consecutive phases and are deliberately different lengths: one covers a person receiving a secret out of band, the other covers them finishing a ceremony at the keyboard. Neither is a secret, so both are shown in the startup summary despite sitting under a credential heading.

type BootstrapCredential

type BootstrapCredential struct {
	LoginID           string
	AccountID         string
	SecretDigest      []byte
	Purpose           string
	IssuedAt          time.Time
	ExpiresAt         time.Time
	AttemptsRemaining int
	ConsumedAt        time.Time
}

BootstrapCredential is an issued login ID and secret that opens exactly one passkey enrollment. It is not a reusable password.

type BootstrapStore

type BootstrapStore interface {
	Issue(ctx context.Context, credential BootstrapCredential) error
	// Find returns an unconsumed credential, or ErrUnknownBootstrap. It reports
	// nothing about why a lookup failed, so a caller cannot enumerate accounts.
	Find(ctx context.Context, loginID string) (BootstrapCredential, error)
	// RecordAttempt decrements the remaining attempts atomically and returns
	// what is left, so a parallel guess cannot spend the same budget twice.
	RecordAttempt(ctx context.Context, loginID string) (int, error)
	// Consume marks the credential spent. It participates in the transaction
	// CredentialStore.Save opened when the context carries one.
	Consume(ctx context.Context, loginID string, at time.Time) error
}

BootstrapStore persists issued bootstrap credentials.

type ClaimConfig

type ClaimConfig struct {
	// Path is a JSON Pointer into the verified ID Token claims.
	Path   string `help:"JSON Pointer into verified claims"`
	Values []string
	Match  string `default:"any" help:"any or all"`
}

ClaimConfig is the admission rule of AdmissionClaim. Its keys hang off auth.enabled rather than the admission policy: falsy names the single value that means off, and here every policy except claim would have to be named.

type Claims

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

Claims is the read-only verified claim set of an ID Token.

func BearerClaims

func BearerClaims(ctx context.Context) (Claims, bool)

BearerClaims returns the frozen verified claim set of a bearer request, for a deployment that authorizes from claims rather than from a local account.

func (Claims) Raw

func (c Claims) Raw(name string) (json.RawMessage, bool)

Raw returns the undecoded JSON of one claim.

func (Claims) String

func (c Claims) String(name string) (string, bool)

String returns a string claim by name.

type Config

type Config struct {
	Enabled bool `default:"false"`
	// Backend names the storage of the four stores this package owns: the
	// ceremony records, the admission allowlist, the passkey credentials, and
	// the issued bootstrap credentials. They move together, because they are
	// one deployment's authentication state and splitting them across two
	// engines gains nothing.
	Backend string `default:"rdb" enum:"rdb,dynamo" dependon:".enabled" help:"storage backend of the authentication tables: rdb or dynamo"`
	// Mode selects which login methods this deployment offers, and with them
	// which of the OIDC, Passkey, and JWT sections below are in force. The enum
	// is what makes those sections' conditions checkable: a mistyped mode there
	// would hide a whole subtree from the startup summary silently and forever,
	// and generation rejects a value that is not listed here.
	Mode string `` /* 149-byte string literal not displayed */
	// LoginPath starts the provider flow.
	LoginPath    string `default:"/auth/login" dependon:".enabled" help:"path that starts the provider flow"`
	CallbackPath string `default:"/auth/callback" dependon:".enabled"`
	LogoutPath   string `default:"/auth/logout" dependon:".enabled"`
	// PostLoginPath is the local path a completed login lands on.
	PostLoginPath string `default:"/" dependon:".enabled" help:"path a completed login lands on"`
	// RecentAuthMaxAge bounds how long a completed authentication still counts
	// as recent enough to add or remove a login method.
	RecentAuthMaxAge time.Duration `` /* 126-byte string literal not displayed */
	// SharedDevice declares that the browsers reaching this deployment are
	// shared, which couples the settings that would otherwise leave one user
	// visible to the next. It fixes the logout scope to global and withholds
	// the select_account prompt, because that prompt exists to surface exactly
	// what this mode hides.
	//
	// Any one of those alone achieves nothing: with the local hint disabled
	// but the provider session alive, the next visitor still sees the previous
	// account in the provider's own account picker.
	//
	// It reduces disclosure and does not eliminate it. The common end of a
	// session on a shared device is abandonment rather than logout, and no
	// relying party can end a provider session it was not asked to end.
	SharedDevice bool               `` /* 131-byte string literal not displayed */
	Assurance    AssuranceConfig    `dependon:".enabled"`
	Protection   ProtectionConfig   `dependon:".enabled"`
	Registration RegistrationConfig `dependon:".enabled"`
	Recovery     RecoveryConfig     `dependon:".enabled"`
	Bootstrap    BootstrapConfig    `dependon:".enabled" summary:"omit"`
	// The three login-method sections name the modes they belong to, so a
	// summary reports the methods this deployment offers rather than all of
	// them. The lists restate usesOIDC, usesPasskey, and usesJWT below; those
	// predicates decide what is built, and these decide what is reported, so
	// the two have to agree. The enabled switch is not repeated: Mode answers
	// to it, and a condition on Mode inherits that gate transitively.
	OIDC    OIDCConfig    `` /* 462-byte string literal not displayed */
	Passkey PasskeyConfig `dependon:".mode=oidc_passkey,passkey_only"`
	JWT     JWTConfig     `dependon:".mode=jwt_only"`
}

Config is the auth runtime binding. It is registered when this package is imported.

type Credential

type Credential struct {
	CredentialID []byte
	AccountID    string
	UserHandle   []byte
	// PublicKey is the normalized COSE key. PublicKeyX and PublicKeyY are the
	// same key as curve points: the relying party verifies with the points and
	// cross-checks them against the COSE blob, so a corrupted row fails closed
	// instead of verifying against something else.
	PublicKey      []byte
	PublicKeyX     []byte
	PublicKeyY     []byte
	Algorithm      int
	SignCount      uint32
	BackupEligible bool
	BackupState    bool
	Transports     []string
	Label          string
	CreatedAt      time.Time
	LastUsedAt     time.Time
}

Credential is the stored passkey credential of one account.

A passkey login resolves a credential ID before it knows an account, which is the opposite of the question AccountResolver answers, so credentials need a lookup of their own.

type CredentialStore

type CredentialStore interface {
	// Find returns the credential of an ID, or ErrUnknownCredential.
	Find(ctx context.Context, credentialID []byte) (Credential, error)
	// ListByAccount supplies excludeCredentials and allowCredentials.
	ListByAccount(ctx context.Context, accountID string) ([]Credential, error)
	// Save persists a new credential and runs within in the same transaction,
	// so a first enrollment can also activate the account and consume the
	// bootstrap credential as one unit. within may be nil.
	Save(ctx context.Context, credential Credential, within func(ctx context.Context) error) error
	// UpdateOnAssertion persists the accepted counter and backup state of a
	// completed assertion.
	//
	// An implementation moves the stored counter forward or changes nothing,
	// in one conditional statement rather than a read and a write, and
	// tolerates an incoming count of zero from an authenticator that keeps
	// none. It reports ErrUnknownCredential when nothing was updated, which
	// covers both a missing credential and a counter that did not advance.
	UpdateOnAssertion(ctx context.Context, credentialID []byte, signCount uint32, backupState bool, usedAt time.Time) error
	// Delete removes one credential of an account.
	Delete(ctx context.Context, accountID string, credentialID []byte) error
}

CredentialStore persists passkey credentials.

The framework never writes a credential outside a store call. An error fails the ceremony closed; it is never downgraded to a warning, because a counter that silently fails to persist is exactly what a cloned authenticator needs.

type Exchange

type Exchange interface {
	// Cookies, SetCookie, and Context are the session's three, and a login is a
	// session, so the endpoints reach the manager through this value directly.
	session.Carrier

	// Method is the request method.
	Method() string
	// Path is the request path with percent-encoding decoded.
	Path() string
	// RawPath is the path as it arrived, before decoding. It is read only to
	// refuse an encoded separator, which is invisible in the decoded form.
	RawPath() string
	// Target is the path and query as sent, which is what a return path records
	// so a completed login lands back on the whole request.
	Target() string
	// Query returns one query parameter, or the empty string.
	Query(name string) string
	// FormValue returns one submitted form field, or the empty string.
	FormValue(name string) string
	// Header returns one request header, or the empty string.
	Header(name string) string
	// HeaderValues returns every value of one request header.
	//
	// It exists for Authorization alone, and only because a second one is
	// refused rather than merged: which of two a proxy forwards is not this
	// application's decision to guess, and a reader that saw only the first
	// could not tell there had been two.
	HeaderValues(name string) []string
	// Body reads the request body, refusing one longer than limit. The refusal
	// is an error rather than a truncation, so an oversized document is
	// answered as one rather than as malformed JSON.
	Body(limit int64) ([]byte, error)

	// IsTLS reports whether this hop arrived over TLS, which is the fact
	// internal/requestorigin outranks every forwarded header with.
	IsTLS() bool
	// RemoteAddress is the peer, which decides whether a forwarded header is
	// evidence of anything.
	RemoteAddress() string
	// Host is the authority the request named, which the origin is built from.
	Host() string

	// SetHeader sets one response header.
	SetHeader(name, value string)
	// Write commits a status and a body.
	Write(status int, body []byte)
	// Problem answers with the framework's problem document, rendered through
	// whatever error page this deployment registered.
	Problem(err error)
	// Redirect sends the browser elsewhere, through the framework's own helper
	// rather than the transport's: an update request is a fetch, so a 303 would
	// be followed and its target applied as a region set for the wrong page.
	Redirect(location string, status int)

	// RecordAuthentication publishes the verified authentication result, so
	// everything after this frame reads it from the request.
	RecordAuthentication(pwruntime.Authentication)
	// AttachSession publishes a session the endpoints resolved themselves,
	// which is what a caller with no session middleware above it needs.
	AttachSession(session.Resolved)
}

An Exchange is everything the authentication endpoints need from the transport carrying one request.

It exists for the reason session.Carrier does, one layer up. A session turned out to touch three operations, so an interface of three let a second transport carry one without a second copy of the rotation rules. The endpoints touch more than three — they read a query, take a form field, decode a body, expire a cookie, and answer with a redirect or a problem — but the same argument applies with more force, because what would be duplicated is a login: two implementations of when a transaction cookie is consumed, or of which failures answer 403 rather than 400, are two chances to leave a hole in one of them.

So every rule below this line is written once against this interface, and each transport supplies the thirty-odd lines that read its own request value.

The cookie and header currency is net/http's, exactly as Carrier's is, and for the same reason: http.Cookie is a data struct describing a cookie rather than implementing one, and a transport that spells cookies differently translates at its own edge.

func HTTPExchange

func HTTPExchange(w http.ResponseWriter, r *http.Request) Exchange

HTTPExchange carries the authentication endpoints over net/http.

It is exported because the endpoints are reachable from outside this package — Hint takes a writer and a request, and an application that mounts its own login page calls it — and because a test that drives one endpoint directly needs the same value the middleware builds.

type FirstEnrollmentStore

type FirstEnrollmentStore interface {
	// SaveFirstCredential applies the enrollment. spend consumes the bootstrap
	// credential that authorized it and activate promotes the provisional
	// account; either may be nil.
	SaveFirstCredential(ctx context.Context, credential Credential, spend, activate func(context.Context) error) error
}

FirstEnrollmentStore is implemented by a credential store that cannot make a first enrollment one unit of work.

The three writes of a passkey-only registration are spending the bootstrap credential, persisting the credential, and activating the account. A store with transactions applies them together and needs only Save. A store without them receives the two callbacks separately, so it can fix an order whose every partial outcome is safe rather than being handed one opaque callback it cannot sequence.

The framework prefers this method when the installed store offers it.

type HintConfig

type HintConfig struct {
	Enabled bool   `default:"false" help:"remember who last signed in, to shorten the next sign-in"`
	Name    string `default:"pw_hint" dependon:".enabled" help:"cookie name"`
	// Secret seals the cookie. The contents never reach the client, so the
	// hint may hold a login identifier; what must not leak is what the login
	// screen renders, which is masked instead.
	Secret string `secret:"mask" env:"AUTH_HINT_SECRET" dependon:".enabled" help:"base64 secret of at least 256 bits that seals the hint"`
	// PreviousSecrets keep a rotation readable.
	PreviousSecrets []string `secret:"mask" dependon:".enabled" help:"retired secrets kept readable during a rotation"`
	// TTL is the absolute bound and IdleTimeout the one measured from the last
	// successful login. The pair is the session's own shape, for the same
	// reasons, and is deliberately not inherited from it: a hint outlives a
	// session by design.
	//
	// Setting TTL to zero is a valid answer. It means this browser may
	// remember nothing, which is what a shared terminal wants and what
	// SharedDevice sets for a whole deployment.
	TTL         time.Duration `default:"720h" dependon:".enabled" help:"how long a hint may live at all"`
	IdleTimeout time.Duration `default:"336h" dependon:".enabled" help:"how long since the last successful login a hint survives"`
}

HintConfig controls whether an ended session leaves a non-authoritative note of who was signed in, so the next sign-in is shorter.

It is off by default. A deployment that has not thought about shared devices therefore drops a browser straight from signed-in to anonymous, where the login screen offers no account and no issuer.

The hint grants nothing. A request carrying one is unauthenticated, and the path guard denies it exactly as it denies any other.

type Identity

type Identity struct {
	Issuer  string
	Subject string
	// KeyClaim names the claim a deployment identifies accounts by, and Key is
	// its verified value. They default to the "sub" claim and the subject.
	//
	// A directory that issues its own stable identifier, such as an employee
	// number, usually knows that value before anyone logs in, which the
	// subject never is. auth.oidc.identity_claim selects it.
	KeyClaim string
	Key      string
	Claims   Claims
	// TokenID is the verified jti of a bearer access token, and empty for a
	// browser login. It names one token, which is what the token form of
	// policy:token-revocation withdraws.
	TokenID string
	// IssuedAt and ExpiresAt are the verified iat and exp of a bearer access
	// token, and zero for a browser login.
	//
	// IssuedAt is what a subject-form revocation compares against: a token
	// minted after the identity was revoked is admitted, so revoking an
	// identity ends its outstanding tokens without ending the identity.
	IssuedAt  time.Time
	ExpiresAt time.Time
}

Identity is a verified external identity. Every field comes from a verified ID Token; the mutable display claims are copied for rendering and never identify the account link.

type JWTConfig

type JWTConfig struct {
	// Issuer is the exact iss claim this deployment accepts. Key discovery
	// starts here, so it is also where the trust in a signing key comes from.
	Issuer string `env:"AUTH_JWT_ISSUER" help:"exact iss claim value this deployment accepts"`
	// Audience is what this API is called by the authorization server. It has
	// no default: a token verified without an audience check was minted for
	// some other service and would be accepted here anyway.
	Audience []string `help:"aud value naming this API; required"`
	// AudienceMatch decides how a multi-valued aud is compared. any is
	// ordinary, because an access token names every resource it may reach.
	AudienceMatch string `default:"any" enum:"any,all" key:"audience_match" help:"any or all"`
	// Algorithms is the exact verification allowlist. It never comes from the
	// token header. An HMAC algorithm is refused outright: the verification key
	// arrives from a public JWKS, so accepting one would let a published key be
	// used as a shared secret.
	//
	// It is required rather than defaulted. Which signatures this deployment
	// trusts is not a question to inherit an answer to, and the answer is one
	// line: algorithms = ["RS256"].
	Algorithms []string `help:"exact verification algorithm allowlist; required, e.g. [\"RS256\"]"`
	// RequiredTokenType is the typ header this deployment demands. RFC 9068
	// names at+jwt, and demanding it is what keeps an ID Token from being
	// replayed here as an access token.
	//
	// Setting it empty accepts an absent typ, for an issuer predating RFC 9068.
	// That is an explicit act with a cost: the audience becomes the only thing
	// separating the two token kinds, so it must be one the issuer does not put
	// in its ID Tokens.
	RequiredTokenType string `default:"at+jwt" key:"required_token_type" help:"typ header to demand; empty accepts an absent typ"`
	// RequiredScopes are the scope values every request must carry. It is its
	// own field rather than a claim rule because scope is a space-delimited
	// string and a generic claim comparison would match the whole value.
	RequiredScopes []string `key:"required_scopes" help:"scope values every request must carry"`
	// Discovery selects where the signing keys are found.
	Discovery string `default:"oidc" enum:"oidc,oauth,manual" help:"oidc, oauth, or manual"`
	// JWKSURI is read only under DiscoveryManual.
	JWKSURI string `key:"jwks_uri" help:"signing key set, for manual discovery"`
	// Leeway absorbs clock skew between this host and the issuer.
	Leeway time.Duration `default:"30s" help:"clock skew allowance"`
	// MaxTokenLifetime bounds exp minus iat. It is required, because it is also
	// how long a subject-form revocation entry must be kept: this application
	// cannot know how long the issuer mints for, so the deployment says.
	MaxTokenLifetime time.Duration `key:"max_token_lifetime" help:"longest exp-minus-iat accepted; required"`
	// MaxTokenBytes bounds the compact token before it is decoded.
	MaxTokenBytes int `default:"8192" key:"max_token_bytes" help:"largest compact token accepted"`
	// JWKSRefreshCooldown bounds how often an unknown kid may cause a refresh,
	// so a stream of forged kid values cannot be amplified into traffic against
	// the issuer.
	JWKSRefreshCooldown time.Duration `default:"1m" key:"jwks_refresh_cooldown" help:"shortest interval between unknown-kid refreshes"`
	// AllowLoopbackHTTP permits an http issuer on loopback and relaxes the
	// same-origin rule on the discovered key set. Development only.
	AllowLoopbackHTTP bool `default:"false" key:"allow_loopback_http" help:"permit an http loopback issuer during development"`
	// IdentityClaim names the verified claim that identifies a local account.
	IdentityClaim string `default:"sub" key:"identity_claim" help:"verified claim that identifies a local account"`
	// Admission decides whether a verified identity may enter this application.
	// Verification proves who the caller is; this decides whether they belong.
	Admission string `help:"authenticated, claim, registered, or existing; required"`
	// AutoProvision permits an unknown verified identity to create an account.
	// It defaults false here and true under OIDC, because a browser login is a
	// person arriving and a bearer request is a machine already running.
	AutoProvision bool `default:"false" key:"auto_provision" help:"permit an unknown verified identity to create an account"`
	// Claim is the admission rule applied when Admission is claim.
	Claim ClaimConfig `help:"admission rule applied when admission is claim"`
	// RegisteredClaims names the claims compared against the allowlist table
	// under AdmissionRegistered.
	RegisteredClaims []string `key:"registered_claims" help:"claims compared against the allowlist; defaults to identity_claim"`
	Revocation       JWTRevocationConfig
	Dev              JWTDevConfig
}

JWTConfig is the bearer-token binding of ModeJWTOnly. It describes one authorization server this deployment trusts and one resource this deployment is.

Several fields carry no default on purpose. Each of them has a permissive answer, and a permissive answer that arrives as a default is one nobody decided: the audience would be "any resource", the admission rule "everyone the issuer knows", and the revocation mode "cannot revoke". Startup names the missing key instead.

type JWTDevConfig

type JWTDevConfig struct {
	TrustUnverifiedTokens bool `default:"false" key:"trust_unverified_tokens" help:"development only: admit a token without verifying it"`
}

JWTDevConfig turns off token verification under `pw dev`.

This is the one setting that turns authentication off, so it is reachable only when four independent locks are open at once: the pwdev build mode, a runtime environment that is not staging or production, this field, and a request that arrived from loopback. A binary built without the pwdev mode refuses to start when it sees the field rather than ignoring it, because a security setting that is silently dropped reads as configured security.

type JWTRevocationConfig

type JWTRevocationConfig struct {
	// Mode is off, token, subject, or both, and carries no default. Selecting a
	// form is what turns its requirements on: the token form makes jti
	// mandatory, rather than a second switch that can disagree with it.
	Mode string `help:"off, token, subject, or both; required in jwt_only"`
	// OnUnavailable is refuse or admit. It defaults to refuse, because a store
	// that cannot answer has not said the token is valid.
	OnUnavailable string `default:"refuse" enum:"refuse,admit" key:"on_unavailable" help:"refuse or admit when the store cannot answer"`
	// MaxPropagationDelay bounds a per-process cache of revocation answers. It
	// defaults to zero, which is no cache: a revocation that takes effect at
	// the next request is the answer nobody has to reason about.
	MaxPropagationDelay time.Duration `key:"max_propagation_delay" help:"how stale a cached revocation answer may be; zero disables the cache"`
}

JWTRevocationConfig decides whether a token can be withdrawn before it expires, and what happens when the store that knows cannot be reached.

type OIDCConfig

type OIDCConfig struct {
	Issuer       string `env:"AUTH_OIDC_ISSUER"`
	ClientID     string `env:"AUTH_OIDC_CLIENT_ID"`
	ClientSecret string `secret:"mask" env:"AUTH_OIDC_CLIENT_SECRET"`
	RedirectURL  string
	Scopes       []string
	// EndpointHosts restricts which hosts the issuer's discovery document may
	// point its endpoints at. Empty accepts whatever the document names.
	//
	// The document decides where this deployment sends the authorization code
	// and its client secret, and the only value checked against configuration is
	// the issuer field the document reports about itself. Naming the hosts turns
	// that into something the deployment asserts.
	//
	// It is empty by default because federated endpoints are ordinary rather than
	// suspicious — Google's issuer is accounts.google.com while its token
	// endpoint is oauth2.googleapis.com — so a same-origin rule would refuse a
	// working provider. The issuer's own host never needs listing, and a host is
	// matched exactly, without wildcards.
	EndpointHosts []string `help:"hosts the discovery document may point endpoints at; empty accepts any"`
	// IdentityClaim names the verified claim that identifies a local account.
	// It defaults to "sub".
	//
	// A deployment that provisions users in advance usually cannot know a
	// subject yet, and instead adds its own stable identifier to the
	// directory, such as an employee number. That claim must be stable for the
	// life of the account and unique within the issuer: it becomes the account
	// link, so a reissued or reused value hands one person another person's
	// account.
	IdentityClaim string `default:"sub" help:"verified claim that identifies a local account"`
	Admission     string `default:"authenticated" help:"authenticated, claim, registered, or existing"`
	// AutoProvision permits an unknown verified identity to create an account
	// through the registered account resolver.
	AutoProvision bool `` /* 133-byte string literal not displayed */
	// Claim is the admission rule applied when Admission is claim.
	Claim ClaimConfig `help:"admission rule applied when admission is claim"`
	// RegisteredClaims names the verified claims compared against the
	// allowlist table under AdmissionRegistered. It defaults to IdentityClaim
	// alone, because that is the value a deployment registers in advance.
	RegisteredClaims []string `help:"claims compared against the allowlist; defaults to identity_claim"`
	// LogoutScope decides what a logout does to the session the provider
	// holds, which is not the session this application owns.
	//
	// reconfirm revokes the local session, sends the provider nothing, and
	// marks the next authorization to carry prompt, so the provider still
	// demands proof while every other relying party sharing it is untouched.
	//
	// global additionally ends the provider session through the discovered end
	// session endpoint, which signs the user out of every application sharing
	// that provider. It is what a shared device wants and what a personal one
	// rarely does.
	//
	// There is no local-only value: revoking only the local session leaves the
	// next login silent, so the sign-out looks like it did nothing. That was
	// the failure reconfirm exists to fix.
	LogoutScope string `default:"reconfirm" enum:"reconfirm,global" help:"what a logout does to the provider session: reconfirm or global"`
	// ProviderLogout is removed and survives only to fail loudly. configbind
	// ignores a key no field declares, so deleting the field outright would
	// leave every scaffolded project silently running reconfirm while its
	// configuration still read as a global sign-out.
	//
	// The default is inverted to false so presence is detectable: an untouched
	// project binds false and starts, and one carrying provider_logout = true
	// is refused with LogoutScope named. A leftover false meant the rejected
	// local scope, whose nearest surviving behavior is the new default.
	//
	// Delete this once no configuration in the wild carries the key.
	ProviderLogout bool `default:"false" help:"removed; use auth.oidc.logout_scope"`
	// AllowGlobalLogoutRequest lets the logout request escalate to global, for
	// a deployment offering both a sign-out and a sign-out-everywhere control.
	// A request may only escalate: a forced downgrade would leave the provider
	// session alive after the user asked to leave it.
	AllowGlobalLogoutRequest bool `default:"false" help:"permit a logout request to escalate to a global sign-out"`
	// AllowLoopbackHTTP permits an http issuer on localhost. It exists for
	// local development against a loopback identity provider and must stay
	// false everywhere else.
	AllowLoopbackHTTP bool `default:"false" help:"permit an http loopback issuer during development"`
}

OIDCConfig describes the relying-party registration and admission policy.

type PasskeyConfig

type PasskeyConfig struct {
	// Path is the base path of the ceremony endpoints. The five endpoints hang
	// off it, so one setting keeps them consistent and keeps them all reachable
	// past the guard.
	Path string `default:"/auth/passkey" help:"base path of the ceremony endpoints"`
	// RPID scopes every credential. It is a domain, never an IP literal,
	// because an IP address cannot be an RP ID.
	RPID    string   `key:"rp_id" help:"relying party domain; localhost during development"`
	RPName  string   `key:"rp_name" help:"relying party display name"`
	Origins []string `help:"origin the browser reaches this deployment on"`
	// UserVerification and Discoverable are the ceremony requirements the
	// relying party asks the authenticator for.
	UserVerification string `default:"required" help:"required, preferred, or discouraged"`
	Discoverable     string `default:"preferred" help:"required or preferred"`
}

PasskeyConfig is the WebAuthn relying-party registration of this deployment.

type PresenceConfig

type PresenceConfig struct {
	Enabled bool `default:"false" help:"accept presence reports from the browser"`
	// Interval is how often the browser is expected to report. It bounds the
	// endpoint's rate and sets the pace the scaffolded script ticks at.
	Interval time.Duration `default:"1m" dependon:".enabled" help:"how often a browser reports"`
	// AbsentAfter ends the session once no interaction has been reported for
	// this long. It measures a person rather than a request, which is the whole
	// point of the signal.
	AbsentAfter time.Duration `default:"30m" dependon:".enabled" help:"end the session after this long with no interaction"`
}

PresenceConfig turns on the endpoint a browser reports human presence to.

Idle expiry otherwise measures time since the last HTTP request, which is a proxy for presence that fails in both directions: a page holding a live connection reconnects on its own and keeps an unattended browser signed in, while a person reading one page for longer than the timeout issues no request at all and is signed out mid-work.

type ProtectionConfig

type ProtectionConfig struct {
	Include []string `help:"protected path pattern"`
	Exclude []string `help:"public path pattern"`
	// Unauthenticated is redirect or unauthorized.
	Unauthenticated string `default:"redirect" help:"redirect or unauthorized"`
}

ProtectionConfig selects the paths that require an authenticated request.

type RecoveryConfig

type RecoveryConfig struct {
	Policy string `help:"oidc, administrator, or application"`
}

RecoveryConfig names the authority that restores access to an account whose credentials are gone.

type RegistrationConfig

type RegistrationConfig struct {
	Policy string `help:"disabled, oidc, invite, administrator, or open"`
}

RegistrationConfig names how a deployment admits a new account. It carries no default, because a mode that needs it must not inherit an answer nobody chose.

type Requirement

type Requirement interface {
	// contains filtered or unexported methods
}

A Requirement states how recently the identity of a request must have been proved. It is a type rather than a time.Duration because the sources of a window are plural and Go has no overloading: a literal in the code, a name a deployment tunes, or a wall-clock deadline computed per request.

There is deliberately no minimum authentication strength. The framework cannot rank the methods it mounts: in a mode with one method an ordering has nothing to order, and in a mode with two neither is stronger in general, so any default would be a claim the framework is not entitled to make.

func Confirmed

func Confirmed(d time.Duration) Requirement

Confirmed admits only a re-proof this guard asked for, within d. A login, however recent, never satisfies it.

Use it where the act matters rather than the clock: moving money, deleting a tenant, exporting a customer list. Somebody who signed in a minute ago to read their dashboard has not agreed to any of those.

A zero duration means confirm for this attempt, and two consecutive operations then require two confirmations.

func Default

func Default() Requirement

Default is the window of auth.recent_auth_max_age, which is also what the passkey enrollment guard has always used.

func Dynamic

func Dynamic(compute func(*http.Request) time.Duration) Requirement

Dynamic computes the window per request, for a deadline that no fixed duration expresses. An internal system re-confirming after every midnight returns the time elapsed since the most recent midnight, so a proof older than that boundary no longer counts.

func DynamicOn

func DynamicOn(compute func(Exchange) time.Duration) Requirement

DynamicOn is Dynamic over the transport seam, for a deployment whose requests are not *http.Request.

func MaxAge

func MaxAge(d time.Duration) Requirement

MaxAge admits any proof no older than d, including the login that started the session. Use it where recency is the point: a session that has been sitting open all afternoon should not reach an administration area, but the person who just signed in already proved who they are.

func Policy

func Policy(name string) Requirement

Policy reads the window from auth.assurance.policy, so the same handler code serves a consumer deployment with a long window and an internal one with a short window. An undefined name is refused at startup by Config.validate rather than at the request that needed it.

type Resources

type Resources struct {
	// DB is the session-group database handle, or nil when middleware.rdb is
	// not enabled.
	DB *sql.DB
	// DBDriver is the engine the DSN resolved to, empty when DB is nil.
	DBDriver string
}

Resources are what the framework has already opened by the time a backend is asked for its stores. A backend that needs none of them ignores them, which is what a store reading its client from the request context does.

type RevocationStore

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

RevocationStore reads and writes the withdrawal list.

It is exported because revoking is an application act: the framework knows how to refuse a revoked token, but only the application knows that an account was compromised.

type Rules

type Rules struct {
	// Protected reports whether a path requires an authenticated caller. A nil
	// value protects nothing.
	Protected func(path string) bool
	// LoginURL is where an unauthenticated browser is sent, carrying a
	// validated local return path. An empty result answers 401 instead.
	LoginURL func(path string) string
	// Redirect sends a browser to LoginURL; without it an unauthenticated
	// request is answered 401 whatever it accepts.
	Redirect bool
	// BearerRealm names the scheme a 401 asks for, per RFC 6750. Empty sends no
	// challenge.
	BearerRealm string
}

Rules are the resolved path-protection policy, in a form that names no transport and no identity provider.

It is a value rather than a middleware because the frame that applies it is each transport's own — the second one ships pwfast.Guard, which takes exactly these four — while deciding which paths are protected and where an unauthenticated visitor is sent is this package's. A deployment without an authentication plugin writes one of these by hand.

func Protection

func Protection() Rules

Protection returns the resolved path-protection policy of the running deployment, or the zero value when no authentication runtime is installed or it protects nothing.

It is how a transport that assembles its own chain reaches the same decision the net/http guard makes, rather than reading the configuration a second time.

type SessionData

type SessionData struct {
	AccountID string `json:"account_id"`
	Issuer    string `json:"iss"`
	Subject   string `json:"sub"`
	// AuthenticatedAt is when the current authentication strength was
	// established. A rotation after a login or a re-proof refreshes it.
	AuthenticatedAt time.Time `json:"authenticated_at"`
	// Method is the unordered label of what proved this session, such as oidc
	// or passkey. Nothing ranks one above another: the framework cannot order
	// the methods it mounts, so an ordering would be a deployment claim.
	Method string `json:"method,omitempty"`
	// KeyClaim and Key record which verified claim identified the account and
	// its value, so a handler can show or audit the link without repeating the
	// configuration.
	KeyClaim    string `json:"key_claim,omitempty"`
	Key         string `json:"key,omitempty"`
	DisplayName string `json:"name,omitempty"`
	Email       string `json:"email,omitempty"`
	// ProviderAuthTime is the verified auth_time of the identity provider: the
	// moment it last actively authenticated this person, which is not the
	// moment the login landed here. A provider may satisfy an authorization
	// request from a single sign-on session established much earlier, so
	// freshness is measured from this and only falls back to the session's own
	// AuthenticatedAt when the provider reported nothing.
	//
	// It lives here rather than on the session record because it is an OpenID
	// Connect concept: a passkey-only deployment has no provider at all, and
	// the generic session package must not learn what an issuer is.
	ProviderAuthTime int64 `json:"provider_auth_time,omitempty"`
	// StepUpAt records that a zero-window re-proof completed, which is the only
	// thing that can satisfy a per-operation requirement.
	//
	// It lives in the session rather than in a cookie so it cannot be forged. It
	// is spent by the guard that accepts it, under one lock with the read that
	// accepted it, so two operations arriving together cannot share one proof —
	// see zeroWindowAdmission. The short window is the backstop for a proof
	// nobody spent, such as one the user abandoned, rather than the mechanism.
	StepUpAt int64 `json:"step_up_at,omitempty"`
}

SessionData is the payload stored in the login session. It holds no token body, no provider secret, and no raw cookie material.

It is one registered session slot, stored exactly like an application's own: the session package holds the bytes and this package holds their meaning. Everything about how well and how recently the subject was proved lives here rather than on the session record, because a record holding a shopping cart for an anonymous browser has no authentication time to report.

func Session

func Session(ctx context.Context) (SessionData, bool)

Session returns the validated login session of the request.

func User

func User(ctx context.Context) (SessionData, bool)

User returns the stored account summary of the request. Handlers use it to render an authenticated page; authorization decisions must still consult application policy.

type SessionLifetimeConfig

type SessionLifetimeConfig = sessionconfig.SessionLifetimeConfig

SessionLifetimeConfig is the [auth.session] binding, declared in popcornweb/sessionconfig so that pw can read it without importing this package.

It is bound here rather than there because a lifetime is authentication's statement: linking this package is what makes the keys exist, and a deployment with no authentication has no framework session lifetime at all. The alias must stay an alias, because the configuration registry is keyed by reflect.Type and a defined type would be a different one.

type SignInHint

type SignInHint struct {
	// DisplayName and LoginID are for rendering. Mask what you render: the
	// disclosure risk is what the next person on the device reads, not what
	// the sealed cookie holds.
	DisplayName string `json:"name,omitempty"`
	LoginID     string `json:"login_id,omitempty"`
	// Issuer is the provider of the last successful login, so a login screen
	// offering several can skip its picker.
	Issuer string `json:"iss,omitempty"`
	// LastLoginAt bounds the hint by inactivity, independently of the absolute
	// bound the cookie itself carries.
	LastLoginAt int64 `json:"at,omitempty"`
}

SignInHint is what the login screen may show about the last person who used this browser. It is not authentication: a request carrying one is anonymous, and policy:authenticated-path-protection denies it exactly as it denies any other.

Its value is what no protocol can supply. An identity provider can name which of its own accounts a returning visitor holds, through the select_account prompt, but it knows nothing about the other providers a deployment offers. Which issuer was used last is therefore local knowledge or no knowledge, and a passkey deployment has no provider to ask at all.

func Hint

Hint returns the sign-in hint of the request, for a login screen to render. The bool is false when the deployment keeps no hint, when this browser has none, or when the one it had has expired.

func HintOn

func HintOn(x Exchange) (SignInHint, bool)

HintOn is Hint over the transport seam.

type Step

type Step func(x Exchange, next func())

A Step is the transport-free authentication frame.

It finalizes the request authentication, serves the login endpoints, and calls next for every request neither of those answered. Each transport wraps one in its own middleware shape, which is the whole of what differs.

func Endpoints

func Endpoints() Step

Endpoints returns the authentication step of the runtime this process already installed, or nil when auth is disabled or no setup has run.

It is how a second transport serves the same login as the first without a second startup. Calling Setup again would open a second set of stores, start a second expiry sweep, and leave the first runtime's serving closures pointed at storage nothing sweeps; reading what is installed shares one runtime, which is what a deployment serving two transports actually has.

func Setup

func Setup(ctx context.Context) (Step, error)

Setup validates the configuration, opens the state this package owns, and returns the authentication step, or nil when auth is disabled.

An application normally never calls it: importing this package registers the framework extension that does. It is exported for a transport that assembles its own chain rather than reading the extension registry, which is what the fasthttp half does. Calling it twice replaces the runtime, so one process calls it once.

Directories

Path Synopsis
Package authfast serves popcornweb/plugin/auth over the fasthttp transport.
Package authfast serves popcornweb/plugin/auth over the fasthttp transport.
Package authfaste2e drives the authentication endpoints over fasthttp, against a real identity provider and a real database.
Package authfaste2e drives the authentication endpoints over fasthttp, against a real identity provider and a real database.
Package authfastjwte2e drives auth.mode = "jwt_only" over fasthttp.
Package authfastjwte2e drives auth.mode = "jwt_only" over fasthttp.
Package authtest installs an authenticated request context without a server, a database, or a ceremony.
Package authtest installs an authenticated request context without a server, a database, or a ceremony.
Package passkeye2e holds the end-to-end test of the passkey ceremony endpoints.
Package passkeye2e holds the end-to-end test of the passkey ceremony endpoints.
Package passkeyonlye2e holds the end-to-end test of the passkey_only mode.
Package passkeyonlye2e holds the end-to-end test of the passkey_only mode.

Jump to

Keyboard shortcuts

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