feat(auth): instance-owner gate — owner store, JWT sessions, setup/login UI (onboarding M1+M2)
lib/owner: argon2id credential store, HS256 access/refresh pair with kid, hashed-jti session store, throttler (5 free failures then 4^n backoff). handlers/auth: /api/auth/* + RequireSession/CSRF/GateWebSocket middleware; every /api/* route is now gated (428 in setup mode). --reset-owner recovery. Frontend: AuthScreen (setup/login), useAuth gate hook, gated data hooks + single-flight silent refresh, Owner account section in settings. spec.yaml documents the full surface.
This commit is contained in:
@@ -2,7 +2,7 @@ openapi: 3.0.3
|
||||
info:
|
||||
title: Dcntrlized Maestro API
|
||||
version: 1.0.0
|
||||
description: The API of the Maestro orchestration router. Maestro manages the lifecycle of Dcntrlized service containers and routes inter-service action calls to them.
|
||||
description: The API of the Maestro orchestration router. Maestro manages the lifecycle of Dcntrlized service containers and routes inter-service action calls to them. Every /api/* route except /api/auth/* requires an instance-owner session (cookies issued by the auth endpoints); in setup mode gated routes answer 428 ERR_SETUP_REQUIRED, otherwise 401 ERR_UNAUTHENTICATED. Mutating requests from browser origins are additionally checked against the request host (CSRF).
|
||||
|
||||
components:
|
||||
schemas:
|
||||
@@ -29,6 +29,65 @@ components:
|
||||
message:
|
||||
type: string
|
||||
|
||||
ApiError:
|
||||
type: object
|
||||
description: The uniform API error shape (onboarding theme M1).
|
||||
properties:
|
||||
error:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
example: "ERR_UNAUTHENTICATED"
|
||||
|
||||
AuthStatus:
|
||||
type: object
|
||||
description: The instance-owner gate state; the SPA bootstraps from it.
|
||||
properties:
|
||||
setup_required:
|
||||
type: boolean
|
||||
description: True until the first owner account exists.
|
||||
authenticated:
|
||||
type: boolean
|
||||
username:
|
||||
type: string
|
||||
|
||||
Credentials:
|
||||
type: object
|
||||
required: [username, password]
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: "3-64 chars: lowercase letters, digits and . _ - ; must start alphanumeric."
|
||||
password:
|
||||
type: string
|
||||
minLength: 10
|
||||
|
||||
SessionResponse:
|
||||
type: object
|
||||
description: >-
|
||||
Login/setup/rotation result. The access and refresh tokens ride as
|
||||
HttpOnly SameSite=Lax cookies (maestro_access / maestro_refresh).
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
role:
|
||||
type: string
|
||||
example: "admin"
|
||||
access_ttl_s:
|
||||
type: integer
|
||||
example: 900
|
||||
|
||||
PasswordChange:
|
||||
type: object
|
||||
required: [current_password, new_password]
|
||||
properties:
|
||||
current_password:
|
||||
type: string
|
||||
new_password:
|
||||
type: string
|
||||
minLength: 10
|
||||
|
||||
PendingAuthorization:
|
||||
type: object
|
||||
description: A first-contact authorization request awaiting the instance owner (M2 §3).
|
||||
@@ -447,14 +506,135 @@ paths:
|
||||
'502':
|
||||
description: Failed to connect to upstream registry
|
||||
|
||||
/api/auth/status:
|
||||
get:
|
||||
summary: Instance-owner gate state (public)
|
||||
tags: ["auth"]
|
||||
description: >-
|
||||
The SPA bootstraps from this endpoint. All other /api/* routes are
|
||||
gated by the instance-owner session (onboarding theme M1): in setup
|
||||
mode they answer 428 ERR_SETUP_REQUIRED, otherwise 401
|
||||
ERR_UNAUTHENTICATED without a valid session.
|
||||
responses:
|
||||
'200':
|
||||
description: Gate state
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthStatus'
|
||||
'500':
|
||||
description: Internal server error
|
||||
|
||||
/api/auth/setup:
|
||||
post:
|
||||
summary: Create the first owner account (admin)
|
||||
tags: ["auth"]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Credentials'
|
||||
responses:
|
||||
'200':
|
||||
description: Owner created; session cookies issued
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SessionResponse'
|
||||
'400':
|
||||
description: Invalid credentials (ERR_INVALID_REQUEST / ERR_INVALID_CREDENTIALS)
|
||||
'409':
|
||||
description: Setup already completed (ERR_SETUP_COMPLETED)
|
||||
'429':
|
||||
description: Rate limited (ERR_RATE_LIMITED)
|
||||
|
||||
/api/auth/login:
|
||||
post:
|
||||
summary: Log in as the instance owner
|
||||
tags: ["auth"]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Credentials'
|
||||
responses:
|
||||
'200':
|
||||
description: Session cookies issued
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SessionResponse'
|
||||
'400':
|
||||
description: Invalid JSON payload
|
||||
'401':
|
||||
description: Unknown user or wrong password (ERR_INVALID_CREDENTIALS)
|
||||
'429':
|
||||
description: Rate limited (ERR_RATE_LIMITED) with Retry-After
|
||||
|
||||
/api/auth/refresh:
|
||||
post:
|
||||
summary: Rotate the access/refresh pair
|
||||
tags: ["auth"]
|
||||
description: >-
|
||||
Uses the maestro_refresh cookie. Reuse of an already-rotated jti is a
|
||||
theft signal and revokes every session of the user
|
||||
(ERR_SESSION_REVOKED).
|
||||
responses:
|
||||
'200':
|
||||
description: Fresh pair issued
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SessionResponse'
|
||||
'401':
|
||||
description: Missing/invalid/replayed refresh cookie
|
||||
|
||||
/api/auth/logout:
|
||||
post:
|
||||
summary: Revoke the current refresh token and clear cookies
|
||||
tags: ["auth"]
|
||||
responses:
|
||||
'200':
|
||||
description: Logged out (idempotent, bodyless-safe)
|
||||
|
||||
/api/auth/change-password:
|
||||
post:
|
||||
summary: Change the owner's password (session required)
|
||||
tags: ["auth"]
|
||||
description: Revokes every existing session of the user on success.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PasswordChange'
|
||||
responses:
|
||||
'200':
|
||||
description: Password changed; all sessions revoked
|
||||
'400':
|
||||
description: Invalid payload or wrong current password
|
||||
'401':
|
||||
description: Not authenticated
|
||||
'429':
|
||||
description: Rate limited (ERR_RATE_LIMITED)
|
||||
|
||||
/api/ws:
|
||||
get:
|
||||
summary: WebSocket endpoint for live container events
|
||||
tags: ["containers"]
|
||||
description: Establishes a WebSocket connection that streams container lifecycle events as JSON.
|
||||
description: >-
|
||||
Establishes a WebSocket connection that streams container lifecycle
|
||||
events as JSON. The upgrade is gated by the instance-owner session;
|
||||
anonymous handshakes close with 401 (428 in setup mode).
|
||||
responses:
|
||||
'101':
|
||||
description: Switching Protocols - WebSocket upgrade
|
||||
'401':
|
||||
description: Not authenticated
|
||||
'428':
|
||||
description: Setup required
|
||||
|
||||
/api/peers:
|
||||
get:
|
||||
|
||||
Reference in New Issue
Block a user