auth: OIDC/lokal inloggning + device-flow för klienter, sessioner per användare
All checks were successful
build-and-push / build (push) Successful in 18s
release-client / build-release (push) Successful in 48s

Ersätter den delade AGENT_HELM_TOKEN med en riktig auth-modell:

- Webben loggar in med lokala konton ELLER OIDC (Authentik/Keycloak/…), valt
  per server. Inloggning ger en HMAC-signerad cookie; WS autentiseras via cookien.
- Klienten (daemon) kopplas via device-flödet (RFC 8628-likt): `agent-helm connect`
  skriver ut en URL, du loggar in + godkänner i webben, och får ett klient-token
  som identifierar din användare. Token sparas i ~/.config/agent-helm/.
- Sessioner är per användare (web ser bara sina egna; admin ser alla). Flera
  klient-instanser = flera parallella sessioner; --new tvingar ny, annars resume.
- Identitet är utbytbar (lokal/OIDC) men device-flödet + sessionerna är alltid
  agent-helms egna → funkar för vem som helst som hostar detta, med eller utan IdP.
- Bootstrap-admin via env (engångs) eller first-run-setup i UI:t. Klient-creds
  kan listas/återkallas; minimal admin-UI (skapa användare, konfigurera OIDC).

Nya filer: server/store.ts (JSON-store, scrypt, cookies), server/oidc.ts
(auth-code+PKCE), web/lib/api.ts. Verifierat end-to-end lokalt (login → device
→ approve → daemon-WS → web ser sessionen; web utan cookie nekas).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U68YyHsxU91ecsen84WQV4
This commit is contained in:
2026-06-29 01:21:29 +02:00
parent 98c4d13360
commit 94259b3456
12 changed files with 1704 additions and 403 deletions

View File

@@ -13,15 +13,19 @@ export interface SessionMeta {
cols: number;
rows: number;
exitCode?: number;
/** Visningsnamn för ägaren (sätts av servern utifrån klient-tokenet). */
user?: string;
}
/* ----------------------------- Daemon -> Server ---------------------------- */
export interface DaemonHello {
type: "daemon:hello";
/** Klient-token från device-flödet (identifierar användaren), INTE en delad token. */
token: string;
sessionId: string;
title: string;
/** Människovänligt namn på klienten/sessionen (visas i UI). */
clientName: string;
cols: number;
rows: number;
}
@@ -69,7 +73,7 @@ export type ServerToDaemon = ServerInput | ServerApprovalDecision;
export interface WebHello {
type: "web:hello";
token: string;
/** Ingen token här — webben autentiseras via sin inloggnings-cookie på WS-handskaket. */
}
export interface WebSubscribe {
@@ -158,6 +162,63 @@ export type ServerToWeb =
| ServerApproval
| ServerApprovalResolved;
/* ------------------------------- HTTP-API (REST) --------------------------- */
/* Device Authorization Grant (RFC 8628-liknande) + auth-status. Delas så att
* daemon, server och web håller samma format. */
export type AuthMode = "local" | "oidc";
/** GET /api/config — publik info som inloggningsvyn behöver (ingen auth). */
export interface PublicConfig {
authMode: AuthMode;
/** Knapptext för OIDC-login, t.ex. "Logga in med Authentik". */
oidcLabel?: string;
/** Har servern fått sitt första admin-konto? */
setupComplete: boolean;
}
/** GET /api/me — vem är inloggad (via cookie). 401 om ingen. */
export interface MeResponse {
userId: string;
username: string;
displayName: string;
isAdmin: boolean;
}
/** POST /api/device/code — daemon startar device-flödet (ingen auth). */
export interface DeviceCodeRequest {
clientName: string;
}
export interface DeviceCodeResponse {
deviceCode: string;
userCode: string;
/** Sida att öppna i webbläsaren. */
verificationUri: string;
/** Samma sida med koden förifylld. */
verificationUriComplete: string;
/** Sekunder mellan pollningar. */
interval: number;
/** Sekunder tills koden går ut. */
expiresIn: number;
}
/** GET /api/device/info?user_code=… — webben hämtar info inför consent (cookie-auth). */
export interface DeviceInfoResponse {
clientName: string;
status: "pending" | "approved" | "denied" | "expired";
}
/** POST /api/device/token — daemon pollar (ingen auth; device_code ÄR hemligheten). */
export type DeviceTokenStatus = "pending" | "slow_down" | "denied" | "expired" | "ready";
export interface DeviceTokenResponse {
status: DeviceTokenStatus;
/** Endast när status === "ready": */
token?: string;
sessionId?: string;
clientName?: string;
user?: string;
}
/* --------------------------------- Helpers --------------------------------- */
export function parseMessage<T>(raw: string): T | null {