Authentication Challenge Policies
Ownership and public configuration
pkg/authchal/config.AuthenticationChallengeConfig owns typed ordered rules and
validation. authchal.Rule and authchal.Ruleset remain aliases, and
authchal.NewRuleset preserves its rule-body signature by calling the parser.
pkg/authchal/parser.NewAuthenticationChallengeConfigFromDirectives(statements)
accepts encoded rule bodies after auth challenges, without the prefix or
braces. It returns *config.AuthenticationChallengeConfig. Its Statements
can be applied through local.IdentityStore.OverwriteUserAuthChallengeRules;
its typed Rules, Validate, and ResolveChallenges support other consumers.
Stored policies with no eligible rule deny login; an empty stored policy clears
explicit selection and restores backend defaults. Validate a replacement before
mutating the user. Policy changes invalidate credential evidence.
Authenticated self-service uses the existing profile operations documented in
the profile authentication-flow API contract.
Keep UI request/response fields, registered/effective policy previews, reset,
and reauthentication guidance there. The profile adapter consumes this same
public rule parser; it must not introduce a second grammar.
pkg/authn/transformer/config.Config owns the established serialized Matchers
and Actions string lists; transformer.Config is its compatible alias.
pkg/authn/transformer/parser.NewUserTransformerConfigFromDirectives(statements)
accepts one transform user block body, without header or braces, and returns
that configuration. Append the result to PortalConfig.UserTransformerConfigs.
CompileUserTransformerConfig owns decoding and validation of typed/persisted
string lists as well; transformer.NewFactory uses its detached RuntimeConfig.
Keep decoding in the parser, not a second implementation in the runtime.
These lower configuration packages avoid parser/runtime import cycles.
Both parsers use cfgutil.DecodeArgs; adapters tokenize and call EncodeArgs.
Reject empty tokens before encoding, raw CR/LF, malformed operator sequences,
unknown directives, and duplicate challenge rules. Errors must not echo input.
Matcher/action order is significant; ordinary repeated actions are intentional.
Parsing starts no network/file activity or workers. Preserve quote boundaries,
caller-owned input, independent results, and JSON serialization/reload behavior.
match realm local
require auth challenges u2f
require auth challenges password totp if u2f not available
require auth challenges password if u2f and totp not available
GitHub identity matchers
The shared transform parser accepts match github id <exact|regex> <value> and
match github org <exact|regex> <value>. Both operators take exactly one value.
ID exact requires a canonical positive unsigned 64-bit decimal integer;
reject signs, zero, leading zeros, fractions and overflow. Organization values
are organization login names, not numeric organization IDs or display names.
Matching is case-sensitive, and Go regular expressions are unanchored unless
the operator supplies ^ and $. A list matches when any organization matches.
Multiple different matchers in one block are ANDed; separate blocks can grant
the same role for alternatives. Existing duplicate-field rejection still applies.
match github id exact 12345678
match github org regex ^(acme|acme-labs)$
action add role authp/admin
Compilation lowers these directives to ordinary ACL conditions over the string
github_id and string-list github_orgs. Preserve the original shorthand in
serialized Config.Matchers; public parsing and typed/JSON configuration must
use the same compiler, without mutating caller input. Adapters must forward the
match github prefix without prepending an implicit exact operator.
Both claims are provider-owned and read-only to transform actions, including
nested writes. Missing claims never satisfy these positive matchers, including
regex .*. Malformed supplied claims return an error before actions run;
JSON-decoded organization string arrays are normalized to the ACL list type.
Portal code preserves these claims only for the server-selected OAuth GitHub
backend, independent of the realm's spelling. Direct factory consumers must
supply trusted provider claims; realm, origin, groups and metadata alone do
not establish GitHub identity. Ordinary role actions cannot manufacture it.
See the OAuth owner's GitHub claim contract
for organization lookup prerequisites, visibility and failure behavior.
parser/github_test.go covers grammar, examples, serialization, matching and
claim immutability. TestGithubTransformProviderBoundary checks backend trust.
TestE2EOAuthGithubTransforms exercises public parsers, serialized configuration,
a real TLS OAuth login, independent portal JWT verification and gatekeeper
allow/deny results, including combined ID and organization conditions.
Custom claim actions
The same transform compiler validates custom claim syntax before portal startup.
parser.ParseCustomFieldValues and parser.ParseCustomNestedFieldValues own the
shared decoded-token grammar used by both compilation and runtime application.
Do not restore independent custom-field decoders in the transformer runtime.
add <field> <value> as string requires one encoded value. Lists require at least
one value and accept as list, as string list, or legacy as string_list.
Nested actions use add nested <key> [<key> ...] with <values> as <type>;
add nested <key> [<key> ...] as map creates an empty object and takes no values.
Reject missing paths/values/types, extra scalar values, unsupported types, and
overwrite on unsupported custom fields. Preserve encoded multiword values,
redacted errors, detached output slices, and nil results on failure.
Substitution expands {claims.field} only in the configured template. Inserted
claim values and existing string/list entries are literal data, never additional
templates. This prevents self-references, recursive expansion and malformed
brace values from looping or panicking. Repeated placeholders in a template
remain supported. Nested custom values retain their established literal behavior.
A nested parent that is a scalar, list, null or nil map returns an error; it must
not panic or replace the parent with an object. Later authentication policy may
not be issued from a failed transformation.
Selection contract
A rule lists a required sequence (password totp) or ordered alternatives
(u2f or totp). Alternatives select the first available single method;
they must not become multiple required checkpoints. An optional suffix
if u2f and totp not available requires every condition method to be absent.
Do not accept partial suffixes or mixed sequences/alternatives. Password is
available under the local/LDAP account contract. Generic mfa selects an MFA
checkpoint; the portal supports TOTP and WebAuthn, not email checkpoints.
The reusable authchal model retains email for existing identity configurations;
transformer policies reject explicit email methods/conditions and do not count
email alone as usable generic MFA.
The first eligible rule across matching transforms wins. Later claim, legacy
require, and deny actions still execute. If any matching transform declares a
policy but none is eligible, deny; do not silently restore password defaults.
A transform that does not match the user imposes no policy.
A selected require auth challenges sequence replaces backend challenges.
This permits U2F-only authentication even when the backend defaults to password
plus MFA. Legacy require mfa, require totp, require u2f, and require password remain additive. Specific factor requirements supersede generic MFA
without losing other explicit factors. Document this administrative precedence
when integrating a new consumer.
identity.User.GetRegisteredAuthMethods supplies enabled, distinct credential
types without collapsing them to generic MFA. requests.User.AuthMethods and
identity.RefreshIdentity.AuthMethods are server-only inventory, not completed
authentication. Carry fresh inventory through identification and transactional
issuance snapshots. Never derive it from a transformed/JWT/client claim.
Portal code uses Factory.TransformWithAuthMethods(claims, inventory), whose
return value is the optional replacement sequence; legacy requirements remain
in claims["challenges"]. transformUser updates the request's selected
challenges. The compatibility Transform entry point consumes auth_methods
from its input map and returns combined selected/additive challenges.
Neither entry point permits arbitrary claim actions to inject challenge policy
or leak the inventory into the token.
Re-evaluate fresh policy in token_issuer.go, token_refresh_runtime.go, and
oidc_runtime.go, under the existing identity transaction. Each adapter must
supply fresh inventory and validate selected plus additive checkpoints against
completed evidence. Direct Basic/API-key issuance cannot claim selected factors
it did not verify; API keys do not substitute for password/MFA checkpoints.
Context-dependent matchers must receive the current request's addr and iss
when policy is re-evaluated. Refresh supplies both from its request context;
the OIDC adapter uses the provider's
request metadata contract.
Do not reuse the original login address for later requests. Token/UserInfo
requests may originate from a relying party's server. iss at transform time
is the current endpoint URL, so use a portal URL prefix for a policy intended
to cover login, sandbox, refresh, and OIDC routes; exact endpoint matches are
intentionally specific to that operation. A stronger current policy requires
fresh authentication, never an upgrade of stored AMR.
Selected OAuth/OIDC snapshot renewal follows the same current-request contract
through cached-user reconstruction. OAuth callback iss uses the established
provider route ending in /; refresh evaluation uses its endpoint URL. Neither
is the snapshot's upstream issuer. The final signed JWT retains the fixed family
issuer. Provider snapshots have no local factor inventory and cannot satisfy a
newly matching requirement. Preserve issuer/address policy denial and request
claim transformations in TestTokenRefreshProviderRequestPolicy,
TestTokenRefreshProviderRequestClaims and their public TLS E2E counterparts.
requests.User.AuthChallengePolicy records an explicit stored policy or a
matching portal replacement; local identification must reset stale markers
and distinguish stored rules from backend defaults. Basic authentication checks
the entire effective sequence after transforms, including additive legacy
requirements. A valid password cannot satisfy TOTP or WebAuthn. Both the public
BasicAuth embedding API and /basic/login/<realm> must enforce this decision;
denied HTTP Basic policy checks return 403, never a successful status.
API keys retain their separate legacy login behavior only when there is no
explicit stored/portal policy and no additive requirement. Any explicit
password/MFA policy, including a saved ["password"], rejects API-key login
because that credential proves none of these checkpoints. Cover the public
APIKeyAuth embedding API and JSON /login equally. Clearing stored rules
restores defaults but does not bypass portal requirements. JSON login must pass
its current HTTP issuer and cancellation context to the internal apiKeyAuth
helper. Calling the public proxy API from that HTTP adapter loses request
metadata and can bypass issuer-dependent rules. Public BasicAuth and
APIKeyAuth have no HTTP request and retain their established authp issuer
at transform time. JSON API-key tokens consequently default their signed iss
to the current HTTP login URL, without its query; this replaces the previous
authp default for that HTTP adapter. Explicit issuer claim transformations
still apply. Keep this distinction explicit when sharing issuance code and
integrating a consumer that validates the token's issuer.
direct_authentication_identity.go holds the local identity transaction across
fresh attribute/inventory loading, transformation, policy checks, and signing
for both public proxy APIs, HTTP Basic login, and encrypted /api/system
authentication assertions. A successful earlier credential
check must not authorize issuance from stale identification data. Preserve the
verification proof across API-key identification; lookup-only evidence is not
authentication. RefreshIdentity.AuthChallengePolicy carries the current stored
policy marker, including an explicit password-only preference. Deliver tokens
and update sessions after the callback releases the identity lock. Injected
identity stores whose kind matches the direct login method receive
these checks too; transaction-capable stores such as SQLite hold their own
identity transaction. Stores without the capability retain their existing
authentication contract. Exact API-key binding and
revocation are owned by local identity transactions.
Remote gatekeepers consume system.AuthResponseMessage.UserData as authenticated
identity data. Apply the same direct policy and authoritative AMR before encrypting
that response; encryption alone does not prove password/MFA policy completion.
The system path supplies the authenticated message's client addr and the current
HTTP endpoint iss to transforms. Strip internal challenge/inventory/UI fields
from successful responses. Invalid API keys return 401, and unmet factors return
403. Preserve exact-key evidence across system-request identification too.
Assign an error status before rendering body-read failures: exceeding the 1 MiB
limit returns 413; other read failures return 400. Never let an unset response
code reach WriteHeader, and keep the body bound for chunked requests too.
Factor-only configuration
Enroll and enable the factor before choosing it. A transform block body for
one local account can select WebAuthn alone:
exact match realm local
exact match sub alice
require auth challenges u2f
Replace the last line with require auth challenges totp for authenticator-code
only login. These replace the backend password/MFA sequence; separately matching
legacy require password actions still add password. The first eligible policy
wins, so an earlier matching replacement can take precedence. Trigger fresh
login after changing policy. User-owned preferences use the profile API's
["u2f"] or ["totp"] rule arrays instead of transform syntax.
These flows still identify the account by username/email. u2f selects the
existing WebAuthn assertion protocol, not a new discoverable-credential login.
See portal MFA for user-presence and
user-verification behavior; do not promise UV-required passkey authentication.
Verified authentication methods
user.Checkpoint.Method is server-only and set exclusively after successful
backend password, TOTP, or WebAuthn verification. recordLoginEvidence collects
and deduplicates these values into User.LoginMethods for all password/MFA
sandbox logins, including refresh/OIDC-disabled and excluded realms. Request fields and
registration/enrollment must not supply method evidence. Enrollment persists a
credential but requires a fresh completed login; it must not issue credentials
from the old sandbox. Preserve the original authentication time.
Use existing mappings password -> pwd, totp -> otp, and u2f -> hwk.
The WebAuthn mapping is the repository's convention, not new device attestation
or physical-hardware certification. Record the concrete factor chosen for a
generic MFA checkpoint. JSON WebAuthn first obtains a server challenge with
challenge_response: webauthn, then submits the signed assertion under the
u2f checkpoint; requesting a challenge does not complete it.
issueSandboxAccessToken and the refresh manager write authoritative amr
after claim transformations, before signing. Ordinary Basic authentication
reports pwd; API-key issuance strips fabricated password/MFA AMR.
user.Claims.AuthenticationMethods validates list input, snapshots values, and
exposes AMR through the signing map and ACL data. pkg/acl recognizes amr as
a string-list field, allowing policies such as match amr hwk. Clones must not
share method slices. Registered credentials alone never justify these claims.
Validation
Read validation scenarios when changing policy
parsing, claim transformation, challenge sequencing, direct authentication,
or issuance. Cover successful factor-only HTML/JSON login and independent AMR
verification; unavailable or wrong factors, stale identity, and a stronger
current policy must prevent issuance. Check refresh and OIDC independently
and together, and preserve direct Basic/API-key/system-request boundaries.
For provider snapshots, evaluate all recognized role contributions before
factor policy and keep transformed roles authoritative through signing.
Cover injected adapters returning root legacy nested role carriers as well as
generic OAuth's flattened claims. Cross-device requester policy must receive
the normalized roles and the requesting address/issuer; the approving provider
login supplies federated evidence and cannot satisfy local factors.
make test TEST_DIR='./pkg/authchal/... ./pkg/authn/transformer/... ./pkg/user ./pkg/acl ./pkg/identity ./pkg/ids/local ./pkg/ids/ldap ./pkg/authn ./internal/tag' COVERAGE_DIR=.coverage/authentication-challenges
Optional HTTP provider evidence
Use sqlite-ticket-provider to maintain the
HTTP login capability's browser-bound federation. It supplies no local registered
factor inventory. The normal transform path selects policy, then direct-policy
validation rejects every unsatisfied checkpoint before signing. Core pins AMR to
federated and origin to the selected realm after transforms. A consumed ticket
is not restored after policy rejection; retry requires a fresh login. Do not infer
password/MFA evidence from issuer-supplied roles or display attributes.