Feature Specification: Resource oidc
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/resources/oidc/default.nix, core/resources/oidc/forward-auth-domain.nix, core/resources/oidc/authelia-oidc-provider.nix, core/resources/oidc/authelia-ldap-backend.nix, core/setups/wiring/resource-routing.nix, core/setups/wiring/tls-trust.nix, core/setups/wiring/mail-domains.nix, core/setups/hosts-common.nix), pull requests #1184, #1205 and #1287, and issue #527.
User Scenarios & Testing (mandatory)
User Story 1 - Register an application as an OIDC client (Priority: P1)
An application such as Mastodon wants single sign-on. It states a client
request with a clientID and its redirect URIs. The policy routes the
request to the authelia-oidc provider. The provider registers the client.
The consumer node gets the issuer, the endpoints and the client secret file.
All of them follow from the issuer. The resource sets the want of the
oidc contract on the consumer node, and a static client module of the
application reads contracts.oidc.results.<clientID>.default. The client
request holds only data.
Why this priority: users log in to each application with one account. Without this resource, each application needs its own accounts.
Independent Test: nix-unit-components-mastodon-oidc and
nix-unit-testers-oidc-routing check the wiring.
nix-unit-resources-oidc-consumer checks that a client request resolves the
oidc contract result on the consumer node, and that the want is plain
data. nix-unit-resources-oidc-policy checks the decisions of apply: which
requests are clients, the name of each protected UI, the admin UI filter, and
the rules and secrets of the Authelia node. No NixOS test runs
Authelia. contracts-oidc-dex tests the contract with another provider.
Acceptance Scenarios:
- Given a routed client request, When the consumer side is evaluated, Then
contracts.oidc.results.<clientID>.defaultholdsissuer,clientSecretFileand the/api/oidc/authorization,/api/oidc/tokenand/api/oidc/userinfoendpoints. - Given a request with
public = true, When the provider is evaluated, Then the client uses PKCES256and the auth methodnone. - Given a request with
public = false, When the provider is evaluated, Then the client usesclient_secret_post. - Given a request with
jwtAccessTokens = true, When the provider is evaluated, Then the access tokens are JWTs signed withRS256.
User Story 2 - Protect an admin UI with forward auth (Priority: P1)
A component serves an admin UI without its own login. It states a
forwardAuth request with a domain and an upstream. The consumer node puts
the UI behind nginx auth_request. Authelia checks the session and the
groups. An anonymous user goes to the Authelia login page.
Why this priority: admin UIs give full control of a service. They must not be open to the internet.
Independent Test: nix-unit-resources-oidc-consumer, and test-lldap-mail for the UI of lldap.
Acceptance Scenarios:
- Given a
forwardAuthrequest, When the consumer side is evaluated, Then nginx sends each request to/internal-authelia-authz, which proxies to/api/authz/auth-requeston Authelia. - Given an allowed user, When the check passes, Then nginx passes the
Remote-User,Remote-Groups,Remote-NameandRemote-Emailheaders to the upstream. - Given a user without a session, When Authelia answers
401, Then nginx answers302to<autheliaUrl>/?rd=<original URL>. - Given
adminUI = trueanddeployAdminUIs = false, When the deployment is evaluated, Then the protection is not deployed. - Given
allowedGroupsis set, When the provider is evaluated, Then the access rule for the domain carries onesubjectentry per group. - Given a location in
unauthenticatedLocations, When a user without a session requests it, Then nginx sends the request to the upstream withoutauth_request, with emptyRemote-*headers, and/keeps the protection.
User Story 3 - Run Authelia against the directory (Priority: P2)
The provider node runs Authelia. Authelia reads its users from the
ldap resource. The policy generates the Authelia secrets and
declares the TLS domain of Authelia.
Why this priority: the clients and the protections from stories 1 and 2 need a running provider.
Independent Test: no NixOS test runs Authelia.
nix-unit-setups-ldap-grant checks the grant of the bind credential.
Acceptance Scenarios:
- Given an OpenBao engine for the directory, When the node is evaluated, Then Authelia binds as
uid=authelia-bind,<userBaseDN>with the password from<mount>/static-cred/authelia-bind. - Given no engine, When the node is evaluated, Then Authelia binds with
bindDNand the password from<prefix>/secret/ldap-bind-<group>/bindPassword. - Given OpenBao rotates the bind password, When the digest template at
/run/openbao-digest/authelia-ldap-bindchanges, Then the agent runssystemctl try-restart authelia-main. - Given the
ldapcontract result, When the node is evaluated, Then Authelia connects to the resulturl. Where the directory serves LDAPS, this isldaps://<host>:<tls.port>, and Authelia verifies the certificate against the system trust, which holds the private PKI CA (core/setups/wiring/tls-trust.nix). - Given
fediversity.authelia.smtpis not set, When Authelia sends a notification, Then it writes the notification tonotifications.txt.
Edge Cases
enable = false:applygives nothing. An unwired template blocks the environment file of the provider.- A
forwardAuthrequest setssubdomainanddeploymentDomainis empty: evaluation throws. - A
forwardAuthrequest sets neithersubdomainnordomain: evaluation throws. - The SMTP relay is down at start: Authelia starts, because the notifier sets
disable_startup_check. - Many password changes in a short time: the
rateLimitsbuckets refuse the extra requests with429.
Requirements (mandatory)
Functional Requirements
- FR-001: The policy MUST give no output when
enableis false. - FR-002: The policy MUST derive the forward-auth domain as
<subdomain>.<deploymentDomain>whensubdomainis set, else asdomain. - FR-003: The policy MUST deploy a protection only when it is enabled and
deployAdminUIsis true oradminUIis false. - FR-004: The policy MUST generate the vars instance
<instanceName>withjwtSecret,storageEncryptionKey,oidcHmacSecret,oidcIssuerPrivateKey(RSA 4096),oidcClientSecretandoidcClientSecretDigest(pbkdf2-sha512), owned byauthelia-mainwith mode0400. - FR-005: The policy MUST declare
domaininsslDomainsand each deployed forward-auth domain inforwardAuthSslDomains. - FR-006: The client request MUST default
scopestoopenid,profileandemail, andauthorizationPolicytoone_factor. Theoidccontract request MUST carryredirectURIsandscopes, so that the provider registers them and the client reads them from its contract result. - FR-007: The provider MUST set
default_policytoone_factorand MUST store its state in the routedpostgresqldatabase of the authelia component, or in sqlite where nopostgresqlprovider resolves. - FR-008: The provider MUST bind to
127.0.0.1, or to0.0.0.0when it serves forward auth. - FR-009: The provider nginx vhost MUST set
X-Forwarded-Forto$remote_addr. - FR-010: The provider MUST open ports
80,443andport(default9091) in the firewall. - FR-011: The client request MUST contain only data. The consumer side MUST give the contract result through
contracts.oidc.results, not as an argument to a function of the request. - FR-012: The consumer side MUST set the
wantof theoidccontract on the consumer node, with theauthelia-oidcprovider as the provider, so that the contract result resolves on that node. - FR-013: The
forwardAuthrequest MUST contain only data. For each deployedforwardAuthrequest, the forward-auth consumer side MUST setfediversity.forwardAuth.domains.<upstream>to the served domain. The static module of the UI service MUST set nothing without an entry for itsupstream. - FR-014: The setups MUST set
deployAdminUIsonly on the central group (the one group whose roster providesvarsBackend). - FR-015: On the central group (the one group whose roster provides
varsBackend), the setups MUST give the Authelia node theldapmount of the local OpenBao as its bind engine. - FR-016: The forward-auth vhost MUST send each location in
unauthenticatedLocationstoupstreamwithoutauth_request, and MUST set theRemote-User,Remote-Groups,Remote-NameandRemote-Emailheaders to empty values on these locations.
Key Entities
- oidc client request:
clientID,redirectURIorredirectURIs,scopes,authorizationPolicy,publicandjwtAccessTokens. It holds only data. - oidc contract result:
contracts.oidc.results.<clientID>.defaulton the consumer node. It holds the request (clientID,redirectURI,redirectURIs,scopes) and the provider data (issuerand the endpoints). - oidc client module: a static module of an application (for example
oidc.nix) that reads the oidc contract result and sets nothing without a result for itsclientID. - forwardAuth request:
enable,domain,subdomain,adminUI,upstream,authorizationPolicy,allowedGroups,proxyWebsockets,upstreamAuthorizationIncludeandunauthenticatedLocations. It holds only data. fediversity.forwardAuth.domains.<upstream>: the served domain on the node of the UI, declared incore/resources/oidc/forward-auth-option.nix. An entry is present only for a deployedforwardAuthrequest.- UI service module: a static module of a component (for example
pgweb.nix) that runs the UI service of aforwardAuthrequest and sets nothing without a result for itsupstream. - oidc policy:
enable,domain,port,issuer(defaulthttps://<domain>),cookieDomain,instanceName(defaultauthelia-oidc),deploymentDomainanddeployAdminUIs. - authelia-oidc provider: the options under
fediversity.authelia, among themrateLimitsandsmtp.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
nix-unit-components-mastodon-oidc,nix-unit-testers-oidc-routing,nix-unit-resources-oidc-consumerandnix-unit-resources-oidc-policypass. - SC-002: A user in
authelia-userslogs in to a client application with the directory password. - SC-003: An anonymous request to a protected admin UI gets a redirect to the Authelia login page.
Assumptions
- The wiring and the setup flow are as in 005. The request and policy model is as in 001.
- The daemon is the
autheliacomponent. The users come from theldapresource. Mail goes through thesmtpresource. The certificate comes from thesslresource. - The sender address of Authelia is
noreply@auth.<domain>fromcore/setups/wiring/mail-domains.nix.
Known Deviations
The code does not fully follow principles V and VII:
- The consumer side imports the
authelia-oidcprovider module to resolve the contract result on the consumer node. A route to a different provider does not change the result on the consumer node. - A TODO in
core/resources/oidc/authelia-ldap-backend.nixsays that the central directory address comes from the static group configuration of the hosting provider. It asks to read it fromterraform_remote_state.