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: darkand served HTML carriesdata-theme="dark" /var/www/auth-portalcontains onlyportal-forest.css(10697bytes) andportal-remember.js(4491bytes)- both files are
root:rootmode0644 /portal-forest.cssreturns200astext/css/portal-remember.jsreturns200asapplication/javascript
- Authelia uses
- 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.csswas 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_pathoverrides onlyfavicon.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:
- React initializes remember-me state to false
- Authelia has no setting for default-checked
- proof: v4.39.20 first-factor state
- 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.zonesatisfy the'self'source
- inline
- Nginx locations:
/portal-forest.cssmaps to/var/www/auth-portal/portal-forest.css/portal-remember.jsmaps 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 HTMLsub_filtercan 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>
- the embedded page contains one
- replacement form:
sub_filter '</title>' '</title>...stylesheet...';inserts the CSS firstsub_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
- two directives competing for the same original
Forest versus Authelia theme names
- accepted Authelia v4.39 theme values:
lightdarkgreyoledauto
- rejected idea:
theme: forestforestis a daisyUI theme name, not an Authelia theme value- Authelia configuration validation rejects it
- applied layering:
- Authelia
theme: darkgives its Material UI components a dark base /portal-forest.csssupplies the daisyUI forest colors and shapes as CSS overrides
- Authelia
- 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 inheading - visible remember-me label and checkbox
- readable
Sign inbutton label
- visible
-
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*-rootclasses only - no generated Emotion class names
- stable element IDs and
-
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"]
- primary:
- live script result:
- observe both the asynchronous React mount and the form’s
disabledto enabled transition MutationObserverwatcheschildListwithsubtreeplus thedisabledattribute- 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
250mswith at least200msbetween clicks - make at most
8click attempts and stop after about10swith 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
- observe both the asynchronous React mount and the form’s
- verified live browser behavior:
- first load reached
checked === truewith Material UIMui-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 passwordand remember me stayed checked
- first load reached
- security consequence:
- checked logins use
remember_me: 1y - remembered sessions bypass
inactivity: 12h - details: Sessions and remember me
- checked logins use
Exact portal rollback
- precondition:
- restore the timestamped
.bak-*file created at install:/etc/nginx/sites-available/auth.loca.zone.bak-20260827T081046Z
- restore the timestamped
- 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-portalare 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
themerollback:- use the configuration
.baksequence in Operator runbook
- use the configuration