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:

  1. Given a routed client request, When the consumer side is evaluated, Then contracts.oidc.results.<clientID>.default holds issuer, clientSecretFile and the /api/oidc/authorization, /api/oidc/token and /api/oidc/userinfo endpoints.
  2. Given a request with public = true, When the provider is evaluated, Then the client uses PKCE S256 and the auth method none.
  3. Given a request with public = false, When the provider is evaluated, Then the client uses client_secret_post.
  4. Given a request with jwtAccessTokens = true, When the provider is evaluated, Then the access tokens are JWTs signed with RS256.

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:

  1. Given a forwardAuth request, When the consumer side is evaluated, Then nginx sends each request to /internal-authelia-authz, which proxies to /api/authz/auth-request on Authelia.
  2. Given an allowed user, When the check passes, Then nginx passes the Remote-User, Remote-Groups, Remote-Name and Remote-Email headers to the upstream.
  3. Given a user without a session, When Authelia answers 401, Then nginx answers 302 to <autheliaUrl>/?rd=<original URL>.
  4. Given adminUI = true and deployAdminUIs = false, When the deployment is evaluated, Then the protection is not deployed.
  5. Given allowedGroups is set, When the provider is evaluated, Then the access rule for the domain carries one subject entry per group.
  6. Given a location in unauthenticatedLocations, When a user without a session requests it, Then nginx sends the request to the upstream without auth_request, with empty Remote-* 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:

  1. 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.
  2. Given no engine, When the node is evaluated, Then Authelia binds with bindDN and the password from <prefix>/secret/ldap-bind-<group>/bindPassword.
  3. Given OpenBao rotates the bind password, When the digest template at /run/openbao-digest/authelia-ldap-bind changes, Then the agent runs systemctl try-restart authelia-main.
  4. Given the ldap contract result, When the node is evaluated, Then Authelia connects to the result url. Where the directory serves LDAPS, this is ldaps://<host>:<tls.port>, and Authelia verifies the certificate against the system trust, which holds the private PKI CA (core/setups/wiring/tls-trust.nix).
  5. Given fediversity.authelia.smtp is not set, When Authelia sends a notification, Then it writes the notification to notifications.txt.

Edge Cases

  • enable = false: apply gives nothing. An unwired template blocks the environment file of the provider.
  • A forwardAuth request sets subdomain and deploymentDomain is empty: evaluation throws.
  • A forwardAuth request sets neither subdomain nor domain: 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 rateLimits buckets refuse the extra requests with 429.

Requirements (mandatory)

Functional Requirements

  • FR-001: The policy MUST give no output when enable is false.
  • FR-002: The policy MUST derive the forward-auth domain as <subdomain>.<deploymentDomain> when subdomain is set, else as domain.
  • FR-003: The policy MUST deploy a protection only when it is enabled and deployAdminUIs is true or adminUI is false.
  • FR-004: The policy MUST generate the vars instance <instanceName> with jwtSecret, storageEncryptionKey, oidcHmacSecret, oidcIssuerPrivateKey (RSA 4096), oidcClientSecret and oidcClientSecretDigest (pbkdf2-sha512), owned by authelia-main with mode 0400.
  • FR-005: The policy MUST declare domain in sslDomains and each deployed forward-auth domain in forwardAuthSslDomains.
  • FR-006: The client request MUST default scopes to openid, profile and email, and authorizationPolicy to one_factor. The oidc contract request MUST carry redirectURIs and scopes, so that the provider registers them and the client reads them from its contract result.
  • FR-007: The provider MUST set default_policy to one_factor and MUST store its state in the routed postgresql database of the authelia component, or in sqlite where no postgresql provider resolves.
  • FR-008: The provider MUST bind to 127.0.0.1, or to 0.0.0.0 when it serves forward auth.
  • FR-009: The provider nginx vhost MUST set X-Forwarded-For to $remote_addr.
  • FR-010: The provider MUST open ports 80, 443 and port (default 9091) 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 want of the oidc contract on the consumer node, with the authelia-oidc provider as the provider, so that the contract result resolves on that node.
  • FR-013: The forwardAuth request MUST contain only data. For each deployed forwardAuth request, the forward-auth consumer side MUST set fediversity.forwardAuth.domains.<upstream> to the served domain. The static module of the UI service MUST set nothing without an entry for its upstream.
  • FR-014: The setups MUST set deployAdminUIs only on the central group (the one group whose roster provides varsBackend).
  • FR-015: On the central group (the one group whose roster provides varsBackend), the setups MUST give the Authelia node the ldap mount of the local OpenBao as its bind engine.
  • FR-016: The forward-auth vhost MUST send each location in unauthenticatedLocations to upstream without auth_request, and MUST set the Remote-User, Remote-Groups, Remote-Name and Remote-Email headers to empty values on these locations.

Key Entities

  • oidc client request: clientID, redirectURI or redirectURIs, scopes, authorizationPolicy, public and jwtAccessTokens. It holds only data.
  • oidc contract result: contracts.oidc.results.<clientID>.default on the consumer node. It holds the request (clientID, redirectURI, redirectURIs, scopes) and the provider data (issuer and 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 its clientID.
  • forwardAuth request: enable, domain, subdomain, adminUI, upstream, authorizationPolicy, allowedGroups, proxyWebsockets, upstreamAuthorizationInclude and unauthenticatedLocations. It holds only data.
  • fediversity.forwardAuth.domains.<upstream>: the served domain on the node of the UI, declared in core/resources/oidc/forward-auth-option.nix. An entry is present only for a deployed forwardAuth request.
  • UI service module: a static module of a component (for example pgweb.nix) that runs the UI service of a forwardAuth request and sets nothing without a result for its upstream.
  • oidc policy: enable, domain, port, issuer (default https://<domain>), cookieDomain, instanceName (default authelia-oidc), deploymentDomain and deployAdminUIs.
  • authelia-oidc provider: the options under fediversity.authelia, among them rateLimits and smtp.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: nix-unit-components-mastodon-oidc, nix-unit-testers-oidc-routing, nix-unit-resources-oidc-consumer and nix-unit-resources-oidc-policy pass.
  • SC-002: A user in authelia-users logs 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 authelia component. The users come from the ldap resource. Mail goes through the smtp resource. The certificate comes from the ssl resource.
  • The sender address of Authelia is noreply@auth.<domain> from core/setups/wiring/mail-domains.nix.

Known Deviations

The code does not fully follow principles V and VII:

  • The consumer side imports the authelia-oidc provider 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.nix says that the central directory address comes from the static group configuration of the hosting provider. It asks to read it from terraform_remote_state.