add SSO session extend flow

Adds an end-to-end SSO session-extension feature: the management server
publishes per-peer session deadlines on every Login/Sync, a new
ExtendAuthSession RPC refreshes the deadline using a fresh JWT without
tearing down the tunnel, and the daemon tracks the deadline locally so
the UI can fire a T-10min warning toast with an interactive "Extend now"
action.
This commit is contained in:
Zoltán Papp
2026-05-20 16:43:14 +02:00
parent 2aea1f7bb5
commit ef6b4f7538
47 changed files with 6506 additions and 2603 deletions
@@ -3,6 +3,7 @@
"tray.status.disconnected": "Getrennt",
"tray.status.daemonUnavailable": "Nicht aktiv",
"tray.status.error": "Fehler",
"tray.session.expiresIn": "Sitzung: {remaining}",
"tray.menu.open": "NetBird öffnen",
"tray.menu.connect": "Verbinden",
@@ -31,6 +32,14 @@
"notify.error.switchProfile": "Wechsel zu {profile} fehlgeschlagen",
"notify.sessionExpired.title": "NetBird-Sitzung abgelaufen",
"notify.sessionExpired.body": "Ihre NetBird-Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
"notify.sessionWarning.title": "Sitzung läuft bald ab",
"notify.sessionWarning.body": "Ihre NetBird-Sitzung läuft in {remaining} ab. Klicken Sie auf Jetzt verlängern, um zu erneuern.",
"notify.sessionWarning.bodyGeneric": "Ihre NetBird-Sitzung läuft bald ab. Klicken Sie auf Jetzt verlängern, um zu erneuern.",
"notify.sessionWarning.extend": "Jetzt verlängern",
"notify.sessionWarning.dismiss": "Verwerfen",
"notify.sessionWarning.failed": "NetBird-Sitzung konnte nicht verlängert werden",
"notify.sessionWarning.successTitle": "NetBird-Sitzung verlängert",
"notify.sessionWarning.successBody": "Ihre Sitzung wurde erneuert.",
"common.cancel": "Abbrechen",
"common.save": "Speichern",
@@ -271,6 +280,7 @@
"sessionAboutToExpire.stay": "Verbunden bleiben",
"sessionAboutToExpire.logout": "Abmelden",
"sessionAboutToExpire.expired": "Sitzung abgelaufen",
"sessionAboutToExpire.extendFailedTitle": "Sitzungsverlängerung fehlgeschlagen",
"peers.search.placeholder": "Nach Peer-Name, DNS oder IP-Adresse suchen",
"peers.filter.all": "Alle",
@@ -3,6 +3,7 @@
"tray.status.disconnected": "Disconnected",
"tray.status.daemonUnavailable": "Not running",
"tray.status.error": "Error",
"tray.session.expiresIn": "Session: {remaining}",
"tray.menu.open": "Open NetBird",
"tray.menu.connect": "Connect",
@@ -31,6 +32,14 @@
"notify.error.switchProfile": "Failed to switch to {profile}",
"notify.sessionExpired.title": "NetBird session expired",
"notify.sessionExpired.body": "Your NetBird session has expired. Please log in again.",
"notify.sessionWarning.title": "Session expires soon",
"notify.sessionWarning.body": "Your NetBird session expires in {remaining}. Click Extend now to renew.",
"notify.sessionWarning.bodyGeneric": "Your NetBird session is about to expire. Click Extend now to renew.",
"notify.sessionWarning.extend": "Extend now",
"notify.sessionWarning.dismiss": "Dismiss",
"notify.sessionWarning.failed": "Failed to extend NetBird session",
"notify.sessionWarning.successTitle": "NetBird session extended",
"notify.sessionWarning.successBody": "Your session has been refreshed.",
"common.cancel": "Cancel",
"common.save": "Save",
@@ -292,6 +301,7 @@
"sessionAboutToExpire.stay": "Stay connected",
"sessionAboutToExpire.logout": "Logout",
"sessionAboutToExpire.expired": "Session expired",
"sessionAboutToExpire.extendFailedTitle": "Extend Session Failed",
"peers.search.placeholder": "Search by peer name, DNS or IP address",
"peers.filter.all": "All",
@@ -3,6 +3,7 @@
"tray.status.disconnected": "Lekapcsolva",
"tray.status.daemonUnavailable": "Nem fut",
"tray.status.error": "Hiba",
"tray.session.expiresIn": "Munkamenet: {remaining}",
"tray.menu.open": "NetBird megnyitása",
"tray.menu.connect": "Csatlakozás",
@@ -31,6 +32,14 @@
"notify.error.switchProfile": "Átváltás sikertelen erre: {profile}",
"notify.sessionExpired.title": "NetBird munkamenet lejárt",
"notify.sessionExpired.body": "A NetBird munkamenet lejárt. Kérjük, jelentkezzen be újra.",
"notify.sessionWarning.title": "Munkamenet hamarosan lejár",
"notify.sessionWarning.body": "A NetBird munkamenet {remaining} múlva lejár. Kattints a Meghosszabbítás gombra a megújításhoz.",
"notify.sessionWarning.bodyGeneric": "A NetBird munkamenet hamarosan lejár. Kattints a Meghosszabbítás gombra a megújításhoz.",
"notify.sessionWarning.extend": "Meghosszabbítás",
"notify.sessionWarning.dismiss": "Elvetés",
"notify.sessionWarning.failed": "A NetBird munkamenet meghosszabbítása sikertelen",
"notify.sessionWarning.successTitle": "NetBird munkamenet meghosszabbítva",
"notify.sessionWarning.successBody": "A munkamenet frissítve.",
"common.cancel": "Mégse",
"common.save": "Mentés",
@@ -271,6 +280,7 @@
"sessionAboutToExpire.stay": "Maradjon csatlakoztatva",
"sessionAboutToExpire.logout": "Kijelentkezés",
"sessionAboutToExpire.expired": "Munkamenet lejárt",
"sessionAboutToExpire.extendFailedTitle": "A munkamenet meghosszabbítása sikertelen",
"peers.search.placeholder": "Keresés társ neve, DNS vagy IP-cím alapján",
"peers.filter.all": "Összes",
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "react-router-dom";
import { Events } from "@wailsio/runtime";
import { Dialogs } from "@wailsio/runtime";
import { ClockIcon } from "lucide-react";
import { Button } from "@/components/Button";
import { ConfirmDialog } from "@/components/ConfirmDialog";
@@ -9,10 +9,14 @@ import { DialogActions } from "@/components/DialogActions";
import { DialogDescription } from "@/components/DialogDescription";
import { DialogHeading } from "@/components/DialogHeading";
import { SquareIcon } from "@/components/SquareIcon";
import { Connection, Profiles as ProfilesSvc, WindowManager } from "@bindings/services";
import {
Connection,
Profiles as ProfilesSvc,
Session,
WindowManager,
} from "@bindings/services";
import { useAutoSizeWindow } from "@/lib/useAutoSizeWindow";
const EVENT_TRIGGER_LOGIN = "trigger-login";
const DEFAULT_SECONDS = 360;
const WINDOW_WIDTH = 360;
@@ -35,6 +39,7 @@ export default function SessionAboutToExpireDialog() {
}, [params]);
const [remaining, setRemaining] = useState(initialSeconds);
const [busy, setBusy] = useState(false);
const expired = remaining <= 0;
useEffect(() => {
@@ -49,10 +54,38 @@ export default function SessionAboutToExpireDialog() {
return () => window.clearInterval(id);
}, [remaining]);
const stay = useCallback(() => {
void Events.Emit(EVENT_TRIGGER_LOGIN);
WindowManager.CloseSessionAboutToExpire().catch(console.error);
}, []);
// Mirrors tray.go::runExtendSession: starts the daemon SSO extend flow,
// opens the browser for the user to sign in, blocks on the daemon until
// the new deadline arrives. Tunnel stays up; success simply closes the
// dialog, failure surfaces a native error dialog and leaves this one
// open so the user can retry or logout.
const stay = useCallback(async () => {
if (busy) return;
setBusy(true);
try {
const start = await Session.RequestExtend({});
const uri = start.verificationUriComplete || start.verificationUri;
if (uri) {
try {
await Connection.OpenURL(uri);
} catch (e) {
console.debug("OpenURL failed during extend", e);
}
}
await Session.WaitExtend({
deviceCode: start.deviceCode,
userCode: start.userCode,
});
WindowManager.CloseSessionAboutToExpire().catch(console.error);
} catch (e) {
await Dialogs.Error({
Title: t("sessionAboutToExpire.extendFailedTitle"),
Message: e instanceof Error ? e.message : String(e),
});
} finally {
setBusy(false);
}
}, [busy, t]);
const logout = useCallback(async () => {
try {
@@ -99,7 +132,7 @@ export default function SessionAboutToExpireDialog() {
size={"md"}
className={"w-full"}
onClick={stay}
disabled={expired}
disabled={expired || busy}
>
{t("sessionAboutToExpire.stay")}
</Button>
@@ -108,6 +141,7 @@ export default function SessionAboutToExpireDialog() {
size={"md"}
className={"w-full"}
onClick={logout}
disabled={busy}
>
{t("sessionAboutToExpire.logout")}
</Button>