auth: OIDC/lokal inloggning + device-flow för klienter, sessioner per användare
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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user