Skip to main content
Version: Enterprise (1.1.0)

Authentication API

Endpoints for user authentication, OTP verification, tenant selection, and token management. All auth routes live under /api/auth/.

How Authentication Works

WebXTerm Enterprise uses Keycloak as the credential store and vsay-auth as the dedicated authentication service in front of the machine backend:

  • vsay-auth calls Keycloak to verify email + password credentials
  • vsay-auth then issues its own signed JWT (HS256) — not a Keycloak OIDC token
  • All API calls use this vsay-auth JWT as a Bearer token
User (email + password) → POST /api/auth/login
→ vsay-auth verifies via Keycloak
→ vsay-auth issues its own JWT
→ Client uses JWT for all API calls
OIDC/OAuth2 Login (Next Enterprise)

For Microsoft and GitHub OIDC/OAuth2 login, see the next Enterprise Edition.


Signup

POST /api/auth/signup

Create a new user account and organization. This creates a dedicated Keycloak realm for the organization and registers the first user as company_admin.

Request Body:

{
"username": "johndoe",
"email": "john@example.com",
"password": "yourpassword",
"first_name": "John",
"last_name": "Doe",
"organization_name": "my-org",
"company_name": "My Company Inc"
}
FieldTypeRequiredNotes
usernamestringYes3–50 characters, globally unique across all organizations
emailstringYesValid email address
passwordstringYesMinimum 8 characters
first_namestringYesUser's first name
last_namestringYesUser's last name
organization_namestringYesBecomes the Keycloak realm name (lowercased, spaces → hyphens)
company_namestringYesDisplay name for the organization

Response 201 Created:

{
"message": "User created successfully. Please check your email to verify your account.",
"user_id": "64f1a2b3c4d5e6f7a8b9c0d1",
"username": "johndoe",
"organization": "My Company Inc"
}

Example:

curl -X POST https://your-webxterm-instance.com/api/auth/signup \
-H "Content-Type: application/json" \
-d '{
"username": "johndoe",
"email": "john@example.com",
"password": "yourpassword",
"first_name": "John",
"last_name": "Doe",
"organization_name": "my-org",
"company_name": "My Company Inc"
}'
First user is company_admin

The user who signs up becomes the company_admin for that organization. They can invite additional users via the admin panel.


Login

POST /api/auth/login

Authenticate with email and password. The response varies depending on:

  • Whether the user belongs to one or multiple organizations (multi-tenancy)
  • Whether OTP is enabled (UI clients only)
  • The client source (ui, cli, or vscode)

Request Body:

{
"email": "john@example.com",
"password": "yourpassword"
}
Source Detection

vsay-auth detects the calling client from the User-Agent header. CLI and VSCode clients skip OTP — OTP is only triggered for ui (browser) logins when enabled by the admin.

Response Scenarios

Scenario A — Direct token (single tenant, OTP disabled or CLI/VSCode)

{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "64f1a2b3c4d5e6f7a8b9c0d1",
"username": "johndoe",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"tenant_id": "my-org",
"tenant_name": "My Company Inc",
"role": "company_admin",
"machine_role": "",
"groups": [],
"email_verified": false,
"api_key": "your_api_key_for_agents"
},
"expires_in": 86400,
"session_token": "eyJhbGciOiJIUzI1NiIs...",
"available_tenants": [
{
"tenant_id": "my-org",
"tenant_name": "My Company Inc",
"username": "johndoe",
"user_id": "64f1a2b3c4d5e6f7a8b9c0d1"
}
]
}

Scenario B — OTP required (UI client, OTP enabled, single tenant)

{
"message": "OTP sent to your email",
"requires_otp": true,
"username": "johndoe",
"tenants": [ { "tenant_id": "my-org", ... } ],
"session_token": "eyJhbGciOiJIUzI1NiIs...",
"otp_expires_in_min": 10
}

Call POST /api/auth/verify-otp with the username and the 6-digit code sent to the user's email.

Scenario C — Tenant selection required (user belongs to multiple organizations)

{
"requires_tenant_selection": true,
"tenants": [
{
"tenant_id": "org-a",
"tenant_name": "Organization A",
"username": "johndoe",
"user_id": "64f1a2b3c4d5e6f7a8b9c0d1"
},
{
"tenant_id": "org-b",
"tenant_name": "Organization B",
"username": "johndoe",
"user_id": "64f1a2b3c4d5e6f7a8b9c0d2"
}
],
"session_token": "eyJhbGciOiJIUzI1NiIs..."
}

Display a workspace picker and call POST /api/auth/select-tenant with the chosen tenant_id.

Example:

curl -X POST https://your-webxterm-instance.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "john@example.com", "password": "yourpassword"}'

Select Tenant

POST /api/auth/select-tenant

Exchange a session_token and a chosen tenant_id for a full JWT. Used when a user belongs to multiple organizations.

Request Body:

{
"session_token": "eyJhbGciOiJIUzI1NiIs...",
"tenant_id": "org-a"
}

Response 200 OK (OTP disabled or CLI/VSCode):

{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": { ... },
"expires_in": 86400,
"session_token": "eyJhbGciOiJIUzI1NiIs...",
"available_tenants": [...]
}

Response 200 OK (OTP enabled, UI client):

{
"message": "OTP sent to your email",
"requires_otp": true,
"username": "johndoe",
"otp_expires_in_min": 10
}

Then call POST /api/auth/verify-otp to complete login.


Verify OTP

POST /api/auth/verify-otp

Verify the 6-digit OTP sent to the user's email and receive a JWT. Called after login or select-tenant returns requires_otp: true.

Request Body:

{
"username": "johndoe",
"otp_code": "847291"
}
FieldTypeRequiredNotes
usernamestringYesThe username value returned in the OTP challenge response
otp_codestringYesExactly 6 digits sent to the user's email

Response 200 OK:

{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "64f1a2b3c4d5e6f7a8b9c0d1",
"username": "johndoe",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"tenant_id": "my-org",
"tenant_name": "My Company Inc",
"role": "company_admin",
"machine_role": "",
"groups": [],
"email_verified": false,
"api_key": "your_api_key_for_agents"
},
"expires_in": 86400,
"session_token": "eyJhbGciOiJIUzI1NiIs...",
"available_tenants": [...]
}

Logout

POST /api/auth/logout

Log out the current user. Requires a valid Bearer token.

Headers:

Authorization: Bearer YOUR_JWT_TOKEN

Response 200 OK:

{
"message": "Logged out successfully"
}

Refresh Token

POST /api/auth/refresh

Get a new JWT token from a still-valid existing token.

Request Body:

{
"token": "YOUR_CURRENT_JWT_TOKEN"
}

Response 200 OK:

{
"token": "eyJhbGciOiJIUzI1NiIs...",
"expires_in": 86400
}

Example:

curl -X POST https://your-webxterm-instance.com/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"token": "YOUR_CURRENT_JWT_TOKEN"}'

JWT Token Structure

Every JWT issued by vsay-auth contains:

{
"user_id": "64f1a2b3c4d5e6f7a8b9c0d1",
"username": "johndoe",
"email": "john@example.com",
"tenant_id": "my-org",
"tenant_name": "My Company Inc",
"role": "company_admin",
"machine_role": "user",
"groups": ["devops"],
"keycloak_id": "kc-uuid-here",
"source": "ui",
"iss": "vsay-auth",
"exp": 1234567890,
"iat": 1234567890
}
ClaimDescription
tenant_idKeycloak realm name / organization identifier
rolesuper_admin, company_admin, or user
sourceClient type: ui, cli, or vscode
issAlways vsay-auth

Using Authentication

HTTP Requests

curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
https://your-webxterm-instance.com/api/machines

WebSocket Terminal Connections

wss://your-webxterm-instance.com/api/terminal/:agent_id/ws?token=YOUR_JWT_TOKEN

Agent Registration

Use your API key (from the api_key field in the login response, or from the Profile page):

sudo vsay-agent configure \
--token YOUR_API_KEY \
--host https://your-webxterm-instance.com \
--linux-user ubuntu

Login Flow Reference

POST /api/auth/login

├─ Single tenant + OTP disabled (or CLI/VSCode)
│ └─ { token, user, expires_in, session_token } ✅ Done

├─ Single tenant + OTP enabled (UI only)
│ └─ { requires_otp: true, username, otp_expires_in_min }
│ └─ POST /api/auth/verify-otp { username, otp_code }
│ └─ { token, user, expires_in } ✅ Done

└─ Multiple tenants
└─ { requires_tenant_selection: true, tenants, session_token }
└─ POST /api/auth/select-tenant { session_token, tenant_id }
├─ OTP disabled → { token, user, expires_in } ✅ Done
└─ OTP enabled → { requires_otp: true, username }
└─ POST /api/auth/verify-otp ✅ Done