OpenID Connect login (AUTH_MODE=oidc)

AUTH_MODE=local keeps the HTTP Basic authentication (unchanged default);
AUTH_MODE=oidc logs in through an OpenID Connect provider with the
authorization code flow and PKCE, standard library only: discovery,
ID token signature (RS/PS/ES) and claims checks, signed session cookie
whose key is kept in DATA_DIR. The UI gets a log out button and reloads
into the login when the session ends.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
cedricandClaude Opus 5.5 committed 2026-10-03 10:39:54 +02:00
1 parent 84f8b9f9ad
commit f30c353b46
9 files changed
+1013 -37

No files matched your search

+14 -1
View File
@@ -14,6 +14,7 @@ const I18N = {
liveUnavailable: 'Live view is not available in LogsQL mode',
settings: 'Settings',
theme: 'Light / dark theme',
logout: 'Log out',
close: 'Close',
rangeAria: 'Time range', severityAria: 'Severity', hostAria: 'Host', appAria: 'Application',
histoAria: 'Log volume over time',
@@ -157,6 +158,7 @@ const I18N = {
liveUnavailable: 'Le direct n\'est pas disponible en mode LogsQL',
settings: 'Paramètres',
theme: 'Thème clair / sombre',
logout: 'Se déconnecter',
close: 'Fermer',
rangeAria: 'Période', severityAria: 'Sévérité', hostAria: 'Hôte', appAria: 'Application',
histoAria: 'Volume de logs dans le temps',
@@ -409,6 +411,8 @@ async function api(url, opts = {}) {
// Known error codes are translated; otherwise the server message is shown.
function apiError(res, text, data) {
// OIDC session expired: reloading the page goes through the login again.
if (res.status === 401 && data && data.code === 'auth') location.reload();
let msg = (data && data.error) || text || res.statusText;
if (data && data.code && I18N[lang]['err_' + data.code]) {
msg = t('err_' + data.code) + (data.detail ? (lang === 'fr' ? ' : ' : ': ') + data.detail : '');
@@ -452,7 +456,7 @@ const list = $('#list');
function applyLang() {
document.documentElement.lang = lang;
for (const el of document.querySelectorAll('[data-i18n]')) el.textContent = t(el.dataset.i18n);
for (const el of document.querySelectorAll('[data-i18n-title]')) el.title = t(el.dataset.i18nTitle);
for (const el of document.querySelectorAll('[data-i18n-title]')) el.title = t(el.dataset.i18nTitle) + (el.dataset.user ? ` (${el.dataset.user})` : '');
for (const el of document.querySelectorAll('[data-i18n-aria]')) el.setAttribute('aria-label', t(el.dataset.i18nAria));
for (const el of document.querySelectorAll('[data-i18n-ph]')) el.placeholder = t(el.dataset.i18nPh);
for (const b of document.querySelectorAll('#langSwitch [data-lang]')) b.setAttribute('aria-checked', String(b.dataset.lang === lang));
@@ -2099,6 +2103,15 @@ $('#range').value = store.get('range', '1h');
if (!$('#range').value) $('#range').value = '1h';
$('#severity').value = store.get('severity', '');
// With OIDC login, show who is logged in and the log out button.
fetch('/auth/me').then((res) => (res.ok ? res.json() : null)).then((me) => {
if (!me || !me.user) return;
const btn = $('#logoutBtn');
btn.hidden = false;
btn.dataset.user = me.user;
btn.title = `${t('logout')} (${me.user})`;
}).catch(() => {});
(async () => {
await loadTags();
loadFacets();
+4
View File
@@ -41,6 +41,10 @@
<svg class="sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
<svg class="moon" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
</button>
<a id="logoutBtn" class="icon-btn" href="/auth/logout" hidden data-i18n-title="logout" data-i18n-aria="logout">
<!-- Log out icon (Lucide "log-out", ISC license) -->
<svg viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/></svg>
</a>
</div>
<div class="progress" aria-hidden="true"></div>
</header>