Portal customization

flowchart TD
    U[Authelia embedded index.html] --> N[Nginx uncompressed response]
    N --> S[Two distinct sub_filter matches]
    S --> C[/portal-forest.css]
    S --> J[/portal-remember.js]
    C --> B[Forest-dark visible login form]
    J --> R[Existing remember checkbox starts checked]

Status

  • live on 2026-08-27:
    • Authelia uses theme: dark and served HTML carries data-theme="dark"
    • /var/www/auth-portal contains only portal-forest.css (10697 bytes) and portal-remember.js (4491 bytes)
    • both files are root:root mode 0644
    • /portal-forest.css returns 200 as text/css
    • /portal-remember.js returns 200 as application/javascript
  • verified HTML injection:
    • sub_filter '</title>' inserts <link rel="stylesheet" href="/portal-forest.css">
    • sub_filter '</head>' inserts <script src="/portal-remember.js"></script>
  • historical cleanup stopgap:
    • /portal-cleanup.css was replaced and deleted on 2026-08-27
    • before replacement it hid the app bar, sign-in heading, remember-me row, reset-password link, Authelia footer, and submit-button label

Why this is an Nginx injection

  • deployed portal shape:
    • Authelia serves its bundled React SPA from the upstream container image
    • this host has no editable portal source tree in that deployment
  • built-in asset override limit:
    • server.asset_path overrides only favicon.ico, static/media/logo.png, and locale JSON routes
    • it does not provide arbitrary custom CSS or JavaScript support
    • proof: v4.39.20 asset routes
  • upstream checkbox limit:
  • support status:
    • the forest stylesheet and default-check script are unsupported-by-upstream workarounds
    • an Authelia upgrade can change DOM IDs, class names, bundle markup, or behavior
    • inspect the login form after every upgrade

Why the files are external and same-origin

  • live Authelia response policy includes:
    • default-src 'self'
    • script-src 'self'
    • style-src 'self' plus a generated nonce
  • consequence:
    • inline <script> has no nonce and would be blocked
    • inline <style> cannot be treated as a durable supported injection
    • external files from https://auth.loca.zone satisfy the 'self' source
  • Nginx locations:
    • /portal-forest.css maps to /var/www/auth-portal/portal-forest.css
    • /portal-remember.js maps to /var/www/auth-portal/portal-remember.js

Why the live vhost uses two distinct substitutions

  • response preparation:
    • proxy_set_header Accept-Encoding ""; asks Authelia for uncompressed HTML
    • sub_filter can then see the literal source tokens
  • verified v4.39.20 source tokens:
    • the embedded page contains one </title> inside <head>
    • the embedded page contains one </head>
  • replacement form:
    • sub_filter '</title>' '</title>...stylesheet...'; inserts the CSS first
    • sub_filter '</head>' '...script...</head>'; inserts the JavaScript last
    • both inserted tags are before the closing head
    • sub_filter_once on; applies once to each distinct search string
  • reason:
    • two directives competing for the same original </head> token are not a safe chain
    • replacement output is not a guaranteed second input pass
    • matching two tokens already present in the upstream response makes both insertions deterministic

Forest versus Authelia theme names

  • accepted Authelia v4.39 theme values:
    • light
    • dark
    • grey
    • oled
    • auto
  • rejected idea:
    • theme: forest
    • forest is a daisyUI theme name, not an Authelia theme value
    • Authelia configuration validation rejects it
  • applied layering:
    • Authelia theme: dark gives its Material UI components a dark base
    • /portal-forest.css supplies the daisyUI forest colors and shapes as CSS overrides
  • reference:

Forest surface contract

  • visual base:

    • daisyUI 5 forest dark tokens are copied verbatim into CSS custom properties
    • dark page background, bordered base card, green primary actions, pill fields, and rounded checkbox
    • no depth shadow and no noise layer
  • restored from the cleanup stopgap:

    • visible Sign in heading
    • visible remember-me label and checkbox
    • readable Sign in button label
  • deliberately retained as simple single-user chrome:

    • app bar and language selector hidden
    • reset-password link hidden
    • Authelia footer hidden
    • register, settings, method, and second-factor headings hidden
  • selector policy:

    • stable element IDs and .Mui*-root classes only
    • no generated Emotion class names
  • historical compatibility:

    • the forest stylesheet explicitly neutralized the old hidden title, remember row, and blank button label during the transition
    • the installed vhost no longer links the deleted cleanup stylesheet

Checkbox behavior contract

  • upstream selector:
    • primary: input#remember-checkbox[value="rememberMe"]
    • script fallback: #form-login input[type="checkbox"]
  • live script result:
    • observe both the asynchronous React mount and the form’s disabled to enabled transition
    • MutationObserver watches childList with subtree plus the disabled attribute
    • act only after the checkbox exists, is enabled, and is still unchecked
    • fire a real click() so Material UI and React state stay synchronized
    • verify that the click committed; retry every 250ms with at least 200ms between clicks
    • make at most 8 click attempts and stop after about 10s with one warning
    • disarm permanently the first time the checkbox is observed checked
    • never fight a user who unticks the box after that disarm
    • leave the row visible
  • verified live browser behavior:
    • first load reached checked === true with Material UI Mui-checked
    • the remember-me row was visible with display: flex
    • the button label was Sign in
    • the button background computed to oklch(0.68628 0.185 148.958)
    • the body background computed to oklch(0.18522 0.007 17.911)
    • a real bad-credential submit returned Incorrect username or password and remember me stayed checked
  • security consequence:

Exact portal rollback

  • precondition:
    • restore the timestamped .bak-* file created at install: /etc/nginx/sites-available/auth.loca.zone.bak-20260827T081046Z
  • approval:
    • ASK FIRST — this restores an Nginx config and reloads a system service
  • restore, validate, reload:
sudo -n install -m 0644 -o root -g root /etc/nginx/sites-available/auth.loca.zone.bak-20260827T081046Z /etc/nginx/sites-available/auth.loca.zone &&
sudo -n /usr/sbin/nginx -t &&
sudo -n systemctl reload nginx
  • result:
    • the restored vhost stops injecting the forest CSS and remember script
    • it references the historical cleanup stylesheet, which was deleted and is not recreated by this rollback
    • the two remaining files under /var/www/auth-portal are inert when no HTML references them
  • asset deletion or recreation:
    • not part of rollback
    • would be a separate destructive-filesystem ask-first stop
  • session and theme rollback: