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 = "oauth_only" instead signs a person in through a provider that speaks OAuth 2.0 and issues no ID Token, such as X, and lands in the same session. auth.mode = "jwt_only" 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
- Variables
- func AdmitAssurance(x Exchange, requirement Requirement, api bool) bool
- func BootstrapSecretDigest(loginID, secret string) []byte
- func Challenge(w http.ResponseWriter, r *http.Request, requirement Requirement, api bool)
- func ChallengeOn(x Exchange, requirement Requirement, api bool)
- func Ensure(handler http.HandlerFunc, requirement Requirement) http.HandlerFunc
- func EnsureAPI(handler http.HandlerFunc, requirement Requirement) http.HandlerFunc
- func EstablishSession(w http.ResponseWriter, r *http.Request, data SessionData, method string) error
- func EstablishSessionOn(x Exchange, data SessionData, method string) error
- func ForgetAccount(accountID string)
- func GenerateBootstrapSecret() (string, error)
- func IsRecent(r *http.Request, requirement Requirement) bool
- func IsRecentOn(x Exchange, requirement Requirement) bool
- func IssueBootstrapCredential(ctx context.Context, loginID, accountID, purpose string) (string, error)
- func LastProvedAt(ctx context.Context) (time.Time, bool)
- func MaskIdentifier(value string) string
- func MigrationSQL(dialect string) (string, error)
- func ModeUsesJWT(mode string) bool
- func ModeUsesOAuth(mode string) bool
- func ModeUsesOIDC(mode string) bool
- func ModeUsesPasskey(mode string) bool
- func RegisterBackend(name string, factory BackendFactory)
- func ReinstateSubject(ctx context.Context, issuer, identityKey string) error
- func ReinstateToken(ctx context.Context, issuer, tokenID string) error
- func RevokeSubject(ctx context.Context, issuer, identityKey, note string) error
- func RevokeToken(ctx context.Context, issuer, tokenID, note string) error
- func SetAccountActivator(activate AccountActivator)
- func SetAccountLookup(lookup AccountLookup)
- func SetAccountResolver(resolve AccountResolver)
- func SetAllowlistStore(store AllowlistStore)
- func SetBootstrapStore(store BootstrapStore)
- func SetCredentialStore(store CredentialStore)
- func SubjectRevoked(ctx context.Context, issuer, identityKey string) (time.Time, bool, error)
- func TokenRevoked(ctx context.Context, issuer, tokenID string) (time.Time, bool, error)
- type Account
- type AccountActivator
- type AccountLookup
- type AccountResolver
- type AllowlistCandidate
- type AllowlistStore
- type AssuranceConfig
- type AssurancePolicy
- type Backend
- type BackendFactory
- type BearerIdentity
- type BootstrapConfig
- type BootstrapCredential
- type BootstrapStore
- type ClaimConfig
- type Claims
- type Config
- type Credential
- type CredentialStore
- type Exchange
- type FirstEnrollmentStore
- type HintConfig
- type Identity
- type JWTConfig
- type JWTDevConfig
- type JWTRevocationConfig
- type OAuthConfig
- type OIDCConfig
- type PasskeyConfig
- type PresenceConfig
- type ProtectionConfig
- type RecoveryConfig
- type RegistrationConfig
- type Requirement
- type Resources
- type RevocationStore
- type Rules
- type SessionData
- type SessionLifetimeConfig
- type SignInHint
- type Step
Constants ¶
const ( // MethodOIDC labels sessions created by the OIDC flow. MethodOIDC = "oidc" // MethodPasskey labels sessions created by a passkey assertion. MethodPasskey = "passkey" // MethodOAuth labels sessions created by a plain OAuth 2.0 provider login. // Which provider is on the session itself, as SessionData.Provider: the // method says how the identity was proved, and a deployment runs one // provider, so ranking or distinguishing them here would say nothing. MethodOAuth = "oauth" )
Authentication method names recorded on a session and reported by pw.RequestAuthentication. An application compares against these rather than against a literal.
const ( BackendRDB = "rdb" BackendDynamo = "dynamo" BackendFirestore = "firestore" )
Backend names. rdb is the default and the behavior every project had before a second one existed.
const ( ModeOIDCOnly = "oidc_only" ModeOIDCPasskey = "oidc_passkey" ModePasskeyOnly = "passkey_only" ModeOAuthOnly = "oauth_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.
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.
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.
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.
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.
const ( UnauthenticatedRedirect = "redirect" )
Responses to an unauthenticated request on a protected path.
const ( MatchAny = "any" MatchAll = "all" )
Claim match modes.
const ( UserVerificationRequired = "required" UserVerificationPreferred = "preferred" UserVerificationDiscouraged = "discouraged" )
Ceremony user verification levels, as WebAuthn names them.
const ( DiscoverableRequired = "required" DiscoverablePreferred = "preferred" )
Discoverable credential requirements.
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.
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.
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.
const ( PurposeInitialPasskey = "initial_passkey" PurposeRecoveryPasskey = "recovery_passkey" )
Bootstrap credential purposes, from data:account-bootstrap-credential.
const AllowlistTable = "popcornweb_auth_allowlist"
AllowlistTable holds the identities a deployment registered in advance. It is consulted by AdmissionRegistered.
const BootstrapTable = "popcornweb_auth_bootstrap"
BootstrapTable holds the issued credentials that open a first passkey enrollment, for the default BootstrapStore.
const ClaimSubject = "sub"
ClaimSubject is the default identity claim and the only one OpenID Connect guarantees is stable and unique for an issuer.
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.
const MethodBearer = "bearer"
MethodBearer labels a request authenticated by a bearer access token. It never labels a session, because this mode creates none.
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.
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 ¶
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") )
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.
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") )
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ModeUsesJWT ¶ added in v0.5.8
ModeUsesJWT reports whether mode verifies bearer tokens and reads [auth.jwt].
func ModeUsesOAuth ¶ added in v0.5.8
ModeUsesOAuth reports whether mode signs in through a plain OAuth provider and reads [auth.oauth].
func ModeUsesOIDC ¶ added in v0.5.8
ModeUsesOIDC reports whether mode signs in through an OpenID Provider and reads [auth.oidc].
func ModeUsesPasskey ¶ added in v0.5.8
ModeUsesPasskey reports whether mode serves passkeys and reads [auth.passkey].
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 ReinstateToken ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
BearerClaims returns the frozen verified claim set of a bearer request, for a deployment that authorizes from claims rather than from a local account.
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 `` /* 172-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.
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 */
// The key tag is load-bearing. Generation derives a TOML key from the field
// name and splits it at every lower-to-upper boundary, which turns OAuth
// into o_auth — OIDC survives only because it is all caps. The setting a
// deployment writes is [auth.oauth].
OAuth OAuthConfig `` /* 298-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"`
// 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 OAuthConfig ¶ added in v0.5.6
type OAuthConfig struct {
// Provider names the built-in definition. It has no default: a provider is
// the one thing this mode cannot infer, and defaulting to whichever one
// happened to be written first would make the choice for a deployment that
// never made it.
Provider string `enum:"x" help:"OAuth login provider; x is X, formerly Twitter"`
ClientID string `env:"AUTH_OAUTH_CLIENT_ID"`
ClientSecret string `secret:"mask" env:"AUTH_OAUTH_CLIENT_SECRET"`
// RedirectURL is the absolute callback URL registered with the provider.
RedirectURL string `help:"RedirectURL is the absolute callback URL registered with the provider"`
// Scopes replaces the provider's own defaults rather than adding to them,
// so a deployment that states this list owns it. Empty asks for exactly
// what the provider needs to report an account and nothing more, which is
// what a login should ask for.
Scopes []string `help:"scopes to request; empty asks for the provider's minimum login set"`
// IdentityClaim names the claim of the fetched profile that identifies a
// local account, and defaults to "sub", which every provider definition
// fills with that provider's own stable identifier.
//
// A handle is the tempting alternative and the wrong one. X lets a handle
// be renamed, and lets a released one be claimed by somebody else, so an
// account linked to a handle eventually becomes somebody else's account.
// Only a name the provider definition actually emits is accepted, so a
// claim copied out of the provider's own API reference is refused at
// startup rather than at every login.
IdentityClaim string `default:"sub" help:"profile claim that identifies a local account; sub is the provider's own identifier"`
Admission string `default:"authenticated" help:"authenticated, claim, registered, or existing"`
// AutoProvision permits an unknown profile to create an account through the
// registered account resolver.
AutoProvision bool `default:"true" help:"permit an unknown profile to create an account through the registered account resolver"`
// Claim is the admission rule applied when Admission is claim.
Claim ClaimConfig `help:"admission rule applied when admission is claim"`
// RegisteredClaims names the profile claims compared against the allowlist
// table under AdmissionRegistered, and defaults to IdentityClaim alone.
RegisteredClaims []string `help:"claims compared against the allowlist; defaults to identity_claim"`
// AllowLoopbackHTTP permits a request-relative redirect URL on loopback, so
// a developer can run the login against the real provider from localhost
// without registering a URL per machine. The provider endpoints themselves
// stay https whatever this says.
AllowLoopbackHTTP bool `default:"false" help:"permit an http loopback redirect URL during development"`
}
OAuthConfig is the [auth.oauth] binding of ModeOAuthOnly: a login through a provider that authenticates a person over plain OAuth 2.0 and issues no ID Token at all.
The difference from OIDCConfig is what is missing rather than what is added. There is no issuer to discover, because such a provider publishes no metadata document; there is no token to verify, because the only thing the callback receives is an access token addressed to the provider's own API; and there is no logout scope, because the provider offers no way to end the session it holds. What the deployment states instead is one provider name, which selects the endpoints, the scopes, and the shape of the account response together.
Who the access token belongs to is settled by asking the provider, over a request that carries the token and nothing else. That is one round trip more than OIDC needs and one signature fewer to check, and it is the whole of the protocol difference between the two modes.
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"`
// Provider is the OAuth login provider that authenticated this session,
// such as "x". It is empty for every other login method, which is what
// makes the three fields below readable without a second flag: they are
// present exactly when this one is.
//
// Subject is that provider's own account identifier — the X user id, for
// the x provider — and Issuer is the provider's namespace.
Provider string `json:"provider,omitempty"`
// Username is the handle the provider reported, such as an X @name without
// the @. It is stored for display and support, and is not the account link:
// a handle can be renamed, and on X a released one can be claimed by
// somebody else, so it identifies nobody durably. It is a copy taken at
// login and goes stale the moment the user renames themselves.
Username string `json:"username,omitempty"`
// AvatarURL is the profile image the provider reported, when it reported
// one. Like Username it is a copy taken at login.
AvatarURL string `json:"avatar_url,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.
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 ¶
func Hint(w http.ResponseWriter, r *http.Request) (SignInHint, bool)
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.
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 ¶
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.
Source Files
¶
- accessors.go
- accountstate.go
- allowlist.go
- assurance.go
- auth.go
- backend.go
- bootstrap.go
- config.go
- configbind_gen.go
- configregister.go
- endpoints.go
- exchange.go
- guard.go
- hint.go
- httpexchange.go
- identity.go
- jwtconfig.go
- jwtdev.go
- jwtmiddleware.go
- jwtsetup.go
- jwtverify.go
- oauthendpoints.go
- passkey.go
- presence.go
- revocation.go
- schema.go
- store.go
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. |