feat/opnform-prod #15

Open
Tobias-Wuest wants to merge 6 commits from feat/opnform-prod into main
Owner
No description provided.
Without tls.domains traefik requests a certificate covering every host in
the router rule, so an internal *.int extra domain forces its own
_acme-challenge CNAME. Setting opnform_tls_sans (e.g. *.int.<zone>) reuses
the existing wildcard delegation instead - the pattern the hand-run stacks
on the digitalboard production hosts already use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Newer OpnForm builds bind the OIDC state to a verifier: /api/auth/{slug}/
redirect returns state_verifier, callback.vue reads it from
sessionStorage["oidc_state_verifier:<slug>:<state>"] and replays it as the
X-OIDC-State-Verifier header - consumeState() rejects the callback without
it (400 "Invalid state", user-visible as "Sign-in could not be completed").
The intercept page dropped the verifier, so every SSO login failed on
images newer than the demo rollout. Store it exactly where the SPA looks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ -206,0 +211,4 @@
# hand-run stacks on the digitalboard production hosts).
- traefik.http.routers.{{ opnform_service_name }}.tls.domains[0].main={{ opnform_domain }}
{% for san in opnform_tls_sans %}
- traefik.http.routers.{{ opnform_service_name }}.tls.domains[0].sans={{ san }}
Owner

[Hoch] Multi-SAN-Bug: Diese Zeile schreibt in der Schleife wiederholt denselben Label-Key tls.domains[0].sans. Bei Traefiks Docker-Label-Provider gewinnt der letzte Wert – bei mehreren SANs überleben also alle außer dem letzten nicht, und das Zertifikat wird unvollständig. Traefik erwartet mehrere SANs als kommaseparierten Wert in einem einzelnen Label. Vorschlag: Schleife entfernen und stattdessen - traefik.http.routers.{{ opnform_service_name }}.tls.domains[0].sans={{ opnform_tls_sans | join(',') }} verwenden.

**[Hoch] Multi-SAN-Bug:** Diese Zeile schreibt in der Schleife wiederholt denselben Label-Key `tls.domains[0].sans`. Bei Traefiks Docker-Label-Provider gewinnt der letzte Wert – bei mehreren SANs überleben also alle außer dem letzten nicht, und das Zertifikat wird unvollständig. Traefik erwartet mehrere SANs als kommaseparierten Wert in einem einzelnen Label. Vorschlag: Schleife entfernen und stattdessen `- traefik.http.routers.{{ opnform_service_name }}.tls.domains[0].sans={{ opnform_tls_sans | join(',') }}` verwenden.
@ -46,3 +51,3 @@
}
default_type text/html;
return 200 '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Redirecting to sign-in…</title></head><body style="font-family:sans-serif;text-align:center;padding:3rem;color:#374151"><p id="m">Redirecting to sign-in…</p><script>fetch("/api/auth/{{ opnform_oidc_slug }}/redirect",{method:"POST",headers:{Accept:"application/json"}}).then(function(r){if(!r.ok)throw new Error("HTTP "+r.status);return r.json()}).then(function(d){if(d&&d.redirect_url){window.location.replace(d.redirect_url)}else{throw new Error("no redirect_url")}}).catch(function(e){document.getElementById("m").textContent="Sign-in redirect failed: "+e.message+". Open /login?bypass=1 to use the email form.";});</script></body></html>';
return 200 '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Redirecting to sign-in…</title></head><body style="font-family:sans-serif;text-align:center;padding:3rem;color:#374151"><p id="m">Redirecting to sign-in…</p><script>fetch("/api/auth/{{ opnform_oidc_slug }}/redirect",{method:"POST",headers:{Accept:"application/json"}}).then(function(r){if(!r.ok)throw new Error("HTTP "+r.status);return r.json()}).then(function(d){if(d&&d.redirect_url){if(d.state&&d.state_verifier){try{sessionStorage.setItem("oidc_state_verifier:{{ opnform_oidc_slug }}:"+d.state,d.state_verifier)}catch(e){}}window.location.replace(d.redirect_url)}else{throw new Error("no redirect_url")}}).catch(function(e){document.getElementById("m").textContent="Sign-in redirect failed: "+e.message+". Open /login?bypass=1 to use the email form.";});</script></body></html>';
Owner

[Mittel] Verschluckter sessionStorage-Fehler: Der try{...}catch(e){} speichert den state_verifier still und ignoriert jeden Fehler. Ist sessionStorage nicht verfügbar (Privatmodus, deaktivierter Storage, Quota), läuft die Weiterleitung zum IdP trotzdem durch und der Callback scheitert später mit genau dem 400 „Invalid state", vor dem der Kommentar oben warnt – ohne jede Diagnose. Vorschlag: Fehlerfall dem Nutzer sichtbar machen (analog zum vorhandenen .catch), statt still fortzufahren. Zusätzlich: Diese Zeile hängt an einem internen, undokumentierten UI-Vertrag (sessionStorage-Key-Format) – ein OpnForm-Upgrade kann den Login ohne Testabdeckung brechen; bitte OpnForm-Version pinnen oder Hinweis ergänzen.

**[Mittel] Verschluckter sessionStorage-Fehler:** Der `try{...}catch(e){}` speichert den `state_verifier` still und ignoriert jeden Fehler. Ist `sessionStorage` nicht verfügbar (Privatmodus, deaktivierter Storage, Quota), läuft die Weiterleitung zum IdP trotzdem durch und der Callback scheitert später mit genau dem 400 „Invalid state", vor dem der Kommentar oben warnt – ohne jede Diagnose. Vorschlag: Fehlerfall dem Nutzer sichtbar machen (analog zum vorhandenen `.catch`), statt still fortzufahren. Zusätzlich: Diese Zeile hängt an einem internen, undokumentierten UI-Vertrag (sessionStorage-Key-Format) – ein OpnForm-Upgrade kann den Login ohne Testabdeckung brechen; bitte OpnForm-Version pinnen oder Hinweis ergänzen.
- traefik docker labels: a repeated tls.domains[0].sans key overwrites
  itself (last one wins), so multi-SAN certs silently lost all but the
  last SAN. All SANs now go into one comma-separated label.
- SSO entrypoint: a failing sessionStorage.setItem was swallowed and the
  browser was redirected into a guaranteed 400 Invalid-state callback.
  The failure now aborts visibly through the existing error handler
  (with the bypass hint) before any redirect happens.
- The entrypoint depends on OpnForm's internal sessionStorage contract,
  so api/client images are now digest-pinned to the state verified on
  the digitalboard production host; upgrading = bump digests + retest
  the SSO login.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After sign-out the SPA navigates to /login client-side (history.pushState),
which never reaches the /login SSO intercept and shows OpnForm's
email/password form. opnform_oidc_logout_page (default on, needs
opnform_oidc_sso_entrypoint) serves a static signed-out page at
opnform_oidc_logout_path and injects a small script (sub_filter) into the UI
pages that sends that navigation there. /login?bypass=1 keeps the
break-glass form usable via a sessionStorage flag. Texts/lang configurable.

Images bumped to 2.6.0 digests; the sessionStorage contract of the SSO
entrypoint (oidc_state_verifier:<slug>:<state>) is unchanged in 2.6.0 and
was verified live on sek2-be 2026-10-20.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the first OIDC login OpnForm's client hard-codes a jump from
/auth/<slug>/callback into /forms/create (no setting for it).
opnform_oidc_new_user_landing (default /home, empty = OpnForm behaviour)
redirects exactly that jump via the script already injected for the
signed-out page; a manual "new form" click is untouched. The script is
injected when either option is on.

Live on forms.sek2-be.ch since 2026-10-20.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The certificate main name was always opnform_domain. On sek2-be the
internal router (forms.int.sek2-be.ch) has no own _acme-challenge
delegation and is covered by *.int.sek2-be.ch alone, so the live labels
carry main=*.int.sek2-be.ch without SANs. opnform_tls_main (empty =
opnform_domain) makes that expressible; the SAN label is only rendered
when opnform_tls_sans is non-empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/opnform-prod:feat/opnform-prod
git switch feat/opnform-prod

Merge

Merge the changes and update on Forgejo.
git switch main
git merge --no-ff feat/opnform-prod
git switch feat/opnform-prod
git rebase main
git switch main
git merge --ff-only feat/opnform-prod
git switch feat/opnform-prod
git rebase main
git switch main
git merge --no-ff feat/opnform-prod
git switch main
git merge --squash feat/opnform-prod
git switch main
git merge --ff-only feat/opnform-prod
git switch main
git merge feat/opnform-prod
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
souveredu/digitalboard.core!15
No description provided.