GET /api/conf no longer returns smtp.pass / oauth.jwtSecret in cleartext (masked to ********). POST treats a blank or ******** secret submission as "keep the stored value," so editing the From address or token lifetimes no longer requires re-entering or leaks the SMTP password / JWT secret. The /conf form fields carry a leave-unchanged hint. Storage stays in OpenBao at secret/sso-manager/conf (unchanged); no theta-suite policy change needed. Co-Authored-By: Claude <noreply@anthropic.com>
31 KiB
SSO Manager API Documentation
Looking for a plainer explanation of what API tokens are and when you'd want one, instead of a full endpoint reference? See API Tokens (in-app) or concepts-api-tokens.md (repo).
Overview
API documentation for the SSO Manager Node application. Provides endpoints for authentication, user management, group management, token management, notifications, and OAuth 2.0 / OpenID Connect.
Base URL: https://your-domain.com
Content Type: application/json (all request and response bodies)
Authentication
Getting a Token
Login via POST /api/auth/login to receive an auth token.
Using the Token
Include the token in every protected request:
auth-token: <token>
Protected Routes
/api/user/*— user management/api/group/*— group management/api/token/*— token management/api/notification/*— notifications/api/oauth/client/*— OAuth client management
Permission Groups
| Group | Grants |
|---|---|
app_sso_admin |
Full user/group/notification management |
app_sso_oauth_admin |
Register and manage OAuth clients |
app_sso_invite |
Invitation management |
| Group owner | Manage membership of that specific group |
Self-service: users can always read and modify their own account without admin membership.
Rate Limits
| Endpoint | Limit |
|---|---|
POST /api/auth/login |
10 requests / 15 min |
POST /api/auth/resetpassword |
5 requests / hour |
POST /api/auth/otp/request |
5 requests / 15 min |
POST /api/auth/otp/verify |
10 requests / 15 min |
POST /api/auth/invite/* |
20 requests / hour |
Limits are per IP. The server must be behind a trusted proxy for these to apply correctly.
Authentication Endpoints
Base path: /api/auth
Login
POST /api/auth/login — No auth required
Request:
{ "uid": "username", "password": "user_password" }
Response:
{ "login": true, "token": "auth_token_string", "message": "username logged in!" }
Logout
ALL /api/auth/logout — No auth required
Response:
{ "message": "Bye" }
Username Suggestions
Returns available username suggestions based on name and optional date of birth.
GET /api/auth/username-suggestions — No auth required
Query Parameters:
givenName— First namesn— Last namedob(optional) — Date of birth (used to generate year-variant suggestions)
Response:
{ "suggestions": ["jsmith", "jsmith1990", "johnsmith"] }
Request Password Reset
POST /api/auth/resetpassword — No auth required
Request:
{ "mail": "user@example.com" }
Response:
{ "message": "If the email address is in our system, you will receive a message." }
The same response is returned whether or not the email exists. Reset tokens expire after 24 hours.
Complete Password Reset
POST /api/auth/resetpassword/:token — No auth required
URL Parameters: token — from the reset email
Request:
{ "password": "new_password", "confirm": "new_password" }
Response:
{ "message": "Password has been changed." }
The token is invalidated after a successful reset.
Request OTP Code
Send a one-time login code via email or SMS.
POST /api/auth/otp/request — No auth required
Request:
{ "login": "username_or_email", "method": "email" }
method must be email or sms. For sms, the account must have a mobile number on file.
Response:
{ "message": "Code sent", "method": "email", "expires_at": 1234567890000 }
Verify OTP Code
Verify a one-time code and receive an auth token. Also marks the corresponding contact method (email or phone) as verified on the account.
POST /api/auth/otp/verify — No auth required
Request:
{ "login": "username_or_email", "code": "123456" }
Response:
{ "login": true, "token": "auth_token_string" }
Returns 401 if the code is invalid or expired.
Accept Invite (with Email Verification)
Create an account from an invite token after verifying email.
POST /api/auth/invite/:token/:mailToken — No auth required
URL Parameters: token — invite token, mailToken — email verification token
Request:
{
"uid": "username",
"password": "user_password",
"givenName": "First Name",
"sn": "Last Name",
"mail": "user@example.com"
}
Response:
{ "user": "username", "token": "auth_token_string" }
Send Invite Verification Email
Send an email verification link for an invite token.
POST /api/auth/invite/:token — No auth required
URL Parameters: token — invite token
Request:
{ "mail": "user@example.com" }
Response:
{ "message": "sent" }
Impersonate User (Admin)
Creates a temporary password for a target user and returns it. Allows admins to log in as another user for support purposes.
POST /api/auth/impersonate/:uid — Auth required, app_sso_admin
URL Parameters: uid — target username
Response:
{
"uid": "target_username",
"temp_password": "random_temp_password",
"expires_at": 1234567890000
}
The temp password is short-lived. Any previous impersonation session for the same target is revoked first.
End Impersonation (Admin)
Revoke all active impersonation sessions for a user.
DELETE /api/auth/impersonate/:uid — Auth required, app_sso_admin
URL Parameters: uid — target username
Response:
{ "message": "Impersonation ended for username", "revoked": 1 }
User Management Endpoints
Base path: /api/user
All endpoints require authentication.
List All Users
GET /api/user/ — app_sso_admin required
Query Parameters:
detail(optional) — return full user objects instead of a minimal list
Response:
{
"results": [
{ "uid": "username", "mail": "user@example.com", "givenName": "First", "sn": "Last" }
]
}
Create User
POST /api/user/ — app_sso_admin required
Request:
{
"uid": "username",
"password": "user_password",
"givenName": "First Name",
"sn": "Last Name",
"mail": "user@example.com"
}
Response:
{ "results": { "uid": "username", "dn": "uid=username,ou=people,dc=..." } }
Admin-created accounts have password_must_change set automatically.
Get Current User
GET /api/user/me — Any authenticated user
Returns the full user object for the authenticated user, including onboarding state.
Response:
{
"uid": "username",
"mail": "user@example.com",
"givenName": "First",
"sn": "Last",
"dn": "uid=username,ou=people,dc=...",
"onboardingRequired": "yes",
"onboardingNeeds": ["tos", "dob", "password"],
"memberOf": ["cn=group_name,ou=groups,dc=..."]
}
onboardingRequired is "yes" when onboardingNeeds is non-empty. Needs can be "tos", "dob", or "password".
Accept Terms of Service
Mark TOS as accepted for the authenticated user.
POST /api/user/accept-tos — Any authenticated user
Response:
{ "success": true }
Get Admin Stats
Returns user and group counts, recent signups, and inactive users.
GET /api/user/stats — app_sso_admin required
Response:
{
"totalUsers": 42,
"activeUsers": 38,
"inactiveUsers": 4,
"totalGroups": 10,
"recentSignups": [
{ "uid": "newuser", "givenName": "New", "sn": "User", "mail": "new@example.com", "createTimestamp": "20240101000000Z" }
],
"inactiveList": [
{ "uid": "lockeduser", "givenName": "Locked", "sn": "User", "mail": "locked@example.com" }
]
}
Export Users CSV
Download all users as a CSV file.
GET /api/user/export — app_sso_admin required
Response: Content-Type: text/csv, attachment download
Columns: uid, givenName, sn, mail, mobile, uidNumber, isActive, createTimestamp
Get User Verification Status
GET /api/user/:uid/verification — app_sso_admin required
Response:
{
"uid": "username",
"emailVerified": true,
"emailVerifiedAt": 1234567890000,
"phoneVerified": false,
"phoneVerifiedAt": null,
"tosAccepted": true,
"tosAcceptedAt": 1234567890000
}
Get User
GET /api/user/:uid — Any authenticated user
Response:
{ "results": { "uid": "username", "mail": "user@example.com", "givenName": "First", "sn": "Last" } }
Update User
PUT /api/user/:uid — Own account, or app_sso_admin for others
Request: Any subset of editable fields:
{
"givenName": "New First Name",
"sn": "New Last Name",
"mail": "newemail@example.com",
"dateOfBirth": "1990-01-15",
"mobile": "+15551234567",
"sshPublicKey": "ssh-rsa AAAA..."
}
Response:
{ "results": { <updated user object> }, "message": "Updated username user" }
Set User Active/Inactive
Lock or unlock a user account.
PUT /api/user/:uid/active — app_sso_admin required
Request:
{ "active": true }
Response:
{ "uid": "username", "active": true, "message": "User username activated" }
Delete User
DELETE /api/user/:uid — Own account, or app_sso_admin for others
Response:
{ "uid": "username", "results": true }
Change Own Password
PUT /api/user/password — Any authenticated user
Request:
{ "password": "new_password", "confirm": "new_password" }
Response:
{ "results": true }
Clears password_must_change on the account.
Change User Password (Admin)
PUT /api/user/:uid/password — Own account, or app_sso_admin for others
Request:
{ "password": "new_password", "confirm": "new_password" }
Response:
{ "results": true, "message": "User username password changed." }
When an admin changes another user's password, password_must_change is set on that account.
Generate Invite Token
Create an invite token, optionally emailing it to the invitee and pre-assigning LDAP groups the new account should be added to on signup.
POST /api/user/invite — app_sso_admin or app_sso_invite required
Request:
{ "mail": "invitee@example.com", "groups": ["group_name"] }
mail and groups are both optional. If mail is provided, a verification
email is sent to that address.
Response:
{
"token": "invite_token_string",
"link": "https://your-domain.com/login/invite/invite_token_string",
"mail_sent": true
}
List Invite Tokens
GET /api/user/invite — app_sso_admin or app_sso_invite required
Admins (app_sso_admin) see all invite tokens; non-admin app_sso_invite
members see only invites they created.
Response:
{
"results": [
{
"token": "invite_token_string",
"created_by": "username",
"created_on": 1234567890000,
"is_valid": true,
"mail": "invitee@example.com",
"groups": "[\"group_name\"]"
}
]
}
Update Invite Token
Update the groups an invite will assign, or change/clear the invitee's email (re-sends verification if a new email is set).
PUT /api/user/invite/:token — app_sso_admin, or the app_sso_invite
member who created the invite
URL Parameters: token — invite token
Request: Any subset of:
{ "groups": ["group_name"], "mail": "newinvitee@example.com" }
Set mail to null/empty to clear it. Fails with 400 if the token is no
longer valid.
Response:
{ "results": { "token": "invite_token_string", "is_valid": true, "...": "..." } }
Revoke Invite Token
DELETE /api/user/invite/:token — app_sso_admin, or the app_sso_invite
member who created the invite
URL Parameters: token — invite token
Response:
{ "results": true }
Add SSH Key
POST /api/user/key — Any authenticated user
Request:
{ "key": "ssh-rsa AAAAB3NzaC1yc2E... user@host" }
Response:
200—{ "message": true }400—{ "message": "error description" }
Group Management Endpoints
Base path: /api/group
All endpoints require authentication.
List Groups
GET /api/group/ — Any authenticated user
Query Parameters:
detail(optional) — return full group objectsmember(optional) — filter to groups containing this UID as a member
Response:
{ "results": [{ "cn": "group_name", "description": "Group description" }] }
Create Group
POST /api/group/ — app_sso_admin required
Request:
{ "name": "group_name", "description": "Group description" }
Response:
{ "results": { "cn": "group_name" }, "message": "group_name was added!" }
The authenticated user is automatically set as the group owner.
Get Group
GET /api/group/:name — Any authenticated user
Response:
{
"results": {
"cn": "group_name",
"description": "Group description",
"member": ["uid=user1,ou=people,dc=...", "uid=user2,ou=people,dc=..."],
"owner": ["uid=owner,ou=people,dc=..."]
}
}
Add Group Owner
PUT /api/group/owner/:group/:uid — app_sso_admin or group owner
Response:
{ "results": true, "message": "Added owner uid to group group." }
Remove Group Owner
DELETE /api/group/owner/:group/:uid — app_sso_admin or group owner
Response:
{ "results": true, "message": "Removed Owner uid from group group." }
Add User to Group
PUT /api/group/:group/:uid — app_sso_admin or group owner
Response:
{ "results": true, "message": "Added user uid to group group." }
Returns 409 if the user is already a member — common in practice, since
groupOfNames requires at least one member and so seeds whoever created the
group into it.
Remove User from Group
DELETE /api/group/:group/:uid — app_sso_admin or group owner
Response:
{ "results": true, "message": "Removed user uid from group group." }
Nest a Group Inside Another
PUT /api/group/:group/nested/:child — app_sso_admin or group owner
Makes :child a member of :group, so everyone in :child is a member of
:group at any depth.
Response:
{ "results": { "cn": "group", "member": ["..."] }, "message": "Nested child inside group." }
Errors:
| Status | When |
|---|---|
400 |
:group and :child are the same group |
409 |
already nested, or the nesting would create a loop (:child already contains :group, directly or transitively) |
Un-nest a Group
DELETE /api/group/:group/nested/:child — app_sso_admin or group owner
Response:
{ "results": { "cn": "group", "member": ["..."] }, "message": "Removed child from group." }
Errors:
| Status | When |
|---|---|
409 |
:child is the only member — groupOfNames requires at least one |
Effective Membership
GET /api/group/:group/effective — Any authenticated user
Who a group actually grants. direct is users listed on the group itself
(never groups); nestedGroups is what is nested into it; effective is every
user reachable through the whole chain.
Response:
{
"results": {
"cn": "app_gitea_access",
"direct": ["cn=alice,ou=people,dc=example,dc=com"],
"nestedGroups": [{ "cn": "developers", "dn": "cn=developers,ou=groups,dc=example,dc=com" }],
"effective": ["cn=alice,ou=people,dc=example,dc=com", "cn=bob,ou=people,dc=example,dc=com"]
}
}
Delete Group
DELETE /api/group/:group — app_sso_admin or group owner
Response:
{ "removed": true, "results": { "cn": "group_name" }, "message": "Group group_name Deleted" }
Token Management Endpoints
Base path: /api/token
All endpoints require authentication.
List Token Types
GET /api/token/ — Any authenticated user
Response:
{ "results": ["InviteToken", "PasswordResetToken"] }
List Tokens by Type
GET /api/token/:name — Any authenticated user
Query Parameters: detail (optional) — include full token objects
Response:
{
"results": [
{ "token": "token_string", "created_by": "username", "created_on": 1234567890000, "is_valid": true }
]
}
Get Specific Token
GET /api/token/:name/:token — Any authenticated user
Response:
{ "results": { "token": "token_string", "created_by": "username", "created_on": 1234567890000, "is_valid": true } }
Notification Endpoints
Base path: /api/notification
All endpoints require authentication and app_sso_admin membership.
Notifications send email blasts to filtered groups of users and record history.
Send Notification
POST /api/notification — app_sso_admin required
Request:
{
"subject": "Maintenance window tonight",
"message": "<p>We will be performing maintenance starting at midnight.</p>",
"filter_type": "group",
"filter_value": "host_hec-bot_admin, app_sso_admin",
"active_only": true
}
filter_type values:
| Value | Description |
|---|---|
all_active |
All active (unlocked) users |
all |
All users including inactive |
group |
Members of one or more LDAP groups |
users |
Specific users by UID |
filter_value:
- For
group: comma-separated group names (e.g."host_hec-bot_admin, app_sso_admin") - For
users: JSON array of UIDs (e.g.'["wmantly","jsmith"]') - Unused for
all_activeandall
active_only (boolean, default false): when true with group or all filter types, only active (unlocked) users receive the notification. Has no effect on all_active (which is always active-only).
Response:
{
"results": {
"notification_id": "uuid",
"created_by": "wmantly",
"created_on": 1234567890000,
"subject": "Maintenance window tonight",
"message": "<p>...</p>",
"filter_type": "group",
"filter_value": "host_hec-bot_admin",
"active_only": true,
"status": "sent",
"sent_count": 5,
"failed_count": 0,
"sent_at": 1234567890123
}
}
External API usage example:
# Login once, store token
TOKEN=$(curl -s -X POST https://sso.example.com/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"uid":"monitor","password":"..."}' | jq -r .token)
# Send notification to a group
curl -X POST https://sso.example.com/api/notification \
-H "auth-token: $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"subject": "hec-bot maintenance",
"message": "<p>The hec-bot VM will restart at 3am.</p>",
"filter_type": "group",
"filter_value": "host_hec-bot_admin",
"active_only": true
}'
List Notification History
GET /api/notification — app_sso_admin required
Response:
{
"results": [
{
"notification_id": "uuid",
"subject": "Maintenance window",
"filter_type": "group",
"filter_value": "host_hec-bot_admin",
"active_only": true,
"status": "sent",
"sent_count": 5,
"failed_count": 0,
"created_by": "wmantly",
"created_on": 1234567890000,
"sent_at": 1234567890123
}
]
}
Results are sorted by created_on descending (most recent first).
Get Notification Record
GET /api/notification/:id — app_sso_admin required
Response: { "results": { <notification object> } }
OAuth 2.0 / OpenID Connect
The SSO Manager acts as an OAuth 2.0 Authorization Server and OpenID Connect Provider.
Discovery
GET /.well-known/openid-configuration — No auth required
Returns the OIDC discovery document with endpoint URLs, supported scopes, and signing algorithms.
Authorization Endpoint
GET /oauth/authorize — No auth required (redirects to login if not authenticated)
Query Parameters:
response_type— Must becodeclient_id— Registered OAuth client IDredirect_uri— Must match a URI registered for the client, either exactly or against a registered wildcard pattern (*= one hostname label,**= any number of labels)scope— Space-separated:openid,profile,emailstate— Opaque value returned unchanged in the redirectcode_challenge— PKCE challenge (SHA-256 of code_verifier, base64url-encoded)code_challenge_method— Must beS256
Renders the consent screen.
Authorize (Issue Code)
Issues the authorization code after the user approves the consent form shown
by the Authorization Endpoint above. This is called by the consent page itself
(an authenticated request, via auth-token), not by the OAuth client
directly.
POST /api/oauth/authorize — Auth required (auth-token header)
Request:
{
"response_type": "code",
"client_id": "uuid",
"redirect_uri": "https://ha.example.com/auth/external/callback",
"scope": "openid profile email",
"state": "opaque-state-value",
"code_challenge": "pkce-challenge",
"code_challenge_method": "S256"
}
Only scopes the client is actually registered for are granted, even if more
are requested. If the client has allowed_groups set, the authenticated user
must be a member of at least one of those groups or the request is rejected
with 403.
Response:
{ "redirect_url": "https://ha.example.com/auth/external/callback?code=<code>&state=<state>" }
The caller (the consent page) redirects the browser to redirect_url, which
completes the flow described in the Authorization Endpoint section above.
Token Endpoint
POST /oauth/token — No auth required (client authenticates via credentials)
Content-Type: application/x-www-form-urlencoded or application/json
Client Authentication: client_id + client_secret in the request body, or HTTP Basic Auth.
Authorization Code Grant
grant_type=authorization_code
&code=<auth_code>
&redirect_uri=<redirect_uri>
&client_id=<client_id>
&client_secret=<client_secret>
&code_verifier=<pkce_verifier>
Refresh Token Grant
grant_type=refresh_token
&refresh_token=<refresh_token>
&client_id=<client_id>
&client_secret=<client_secret>
Refresh tokens are rotated on each use — the old token is invalidated and a new one is returned.
Response:
{
"access_token": "uuid",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "uuid",
"id_token": "<jwt>"
}
UserInfo Endpoint
GET /oauth/userinfo — Bearer token required (Authorization: Bearer <access_token>)
Response (claims vary by granted scopes):
{
"sub": "username",
"name": "First Last",
"given_name": "First",
"family_name": "Last",
"preferred_username": "username",
"email": "user@example.com"
}
OAuth Client Management
Base path: /api/oauth/client
All endpoints require authentication and app_sso_oauth_admin membership.
List Clients
GET /api/oauth/client/
Response:
{
"results": [
{
"client_id": "uuid",
"name": "Home Assistant",
"description": "Home automation",
"redirect_uris": ["https://ha.example.com/auth/external/callback"],
"scopes": ["openid", "profile", "email"],
"allowed_groups": [],
"token_lifetime": { "access_token": 3600, "refresh_token": 2592000 },
"created_by": "wmantly",
"created_on": 1234567890000
}
]
}
Register Client
POST /api/oauth/client/
Request:
{
"name": "Home Assistant",
"description": "Home automation dashboard",
"redirect_uris": ["https://ha.example.com/auth/external/callback"],
"scopes": ["openid", "profile", "email"],
"allowed_groups": [],
"token_lifetime": { "access_token": 3600, "refresh_token": 2592000 }
}
redirect_uris may also be a newline-separated string. scopes may also be a space-separated string.
allowed_groups restricts the client to members of the listed SSO groups (empty/omitted = any valid user).
Response:
{
"results": { "client_id": "uuid", "name": "Home Assistant" },
"client_secret": "raw-secret-shown-once",
"message": "OAuth client 'Home Assistant' created. Save the client secret — it will not be shown again."
}
The client_secret is shown only once. Store it immediately.
Get Client
GET /api/oauth/client/:client_id
Response: { "results": { <client object> } }
Update Client
PUT /api/oauth/client/:client_id
Request: Any subset of name, description, redirect_uris, scopes, allowed_groups, token_lifetime, is_valid.
Response: { "results": { <updated client> }, "message": "..." }
Delete Client
DELETE /api/oauth/client/:client_id
Response: { "client_id": "uuid", "message": "OAuth client '...' deleted." }
Rotate Client Secret
POST /api/oauth/client/:client_id/rotate
Response:
{
"client_secret": "new-raw-secret-shown-once",
"message": "Client secret rotated for '...'. Save it — it will not be shown again."
}
The old secret is invalidated immediately. The new secret is shown only once.
Token Lifetimes
Configurable per-client via token_lifetime. Global defaults (in seconds):
{ "access_token": 3600, "refresh_token": 2592000 }
Plugin Endpoints
Base path: /api/plugins
All endpoints require authentication and app_sso_admin, app_sso_directory_admin, or app_super_admin membership. Secret field values are always returned masked (********); they are stored in OpenBao at secret/plugins/<instance-id>/conf, never in the database row. See Plugins.
List Plugin Types
GET /api/plugins/types
Returns the installed plugin types and their configSchema (used to build the create-instance form).
Response:
{
"results": [
{
"type": "proxmox",
"category": "discovery",
"name": "Proxmox VE",
"description": "Discover VMs, containers, and hypervisor nodes from a Proxmox VE API endpoint.",
"configSchema": [
{ "key": "url", "label": "API URL", "type": "url", "required": true },
{ "key": "tokenId", "label": "Token ID", "type": "text", "required": true },
{ "key": "tokenSecret", "label": "Token Secret", "type": "password", "required": true, "secret": true }
]
}
]
}
List Plugin Instances
GET /api/plugins/
Response: { "results": [ { "id", "pluginType", "category", "name", "slug", "enabled", "cron", "config", "secrets": {…masked…}, "lastRunAt", "lastStatus", "lastError" } ] }
Get One Instance
GET /api/plugins/:id — same shape as a list entry.
Create Instance
POST /api/plugins/
config is a flat object of all field values (secret and non-secret); the server splits it — non-secret fields go to the DB row, secret fields to OpenBao. Creating an enabled instance schedules it and kicks one immediate run. slug is the discovery source name (lowercase letters/digits/_/-, max 64, unique).
Request:
{
"pluginType": "proxmox",
"name": "Proxmox — Home Lab",
"slug": "proxmox-homelab",
"cron": "0 * * * *",
"config": { "url": "https://pve:8006", "tokenId": "u@pam!t", "tokenSecret": "secret-value" }
}
Errors: 400 if the plugin type is unknown, the slug is malformed/duplicated, or a required field is missing; 400 with an OpenBao hint if writing the secret fails (re-run ./setup.sh with theta-suite ≥ v1.30.1).
Update Instance
PUT /api/plugins/:id — update name, cron, enabled, and non-secret config. Secret fields are changed via PUT /:id/secrets. Re-schedules if cron or enabled changed.
Update Secrets
PUT /api/plugins/:id/secrets — body is a flat object of secret field values. Blank/******** values are ignored (kept as-is).
Test Instance
POST /api/plugins/:id/test — runs the plugin's validate. Returns { "ok": true } or 400 { "ok": false, "error": "..." }.
Load / Unload / Run Now
POST /api/plugins/:id/load— enable + schedule + run now.POST /api/plugins/:id/unload— unschedule + disable.POST /api/plugins/:id/run— enqueue one immediate run (regardless of enabled).
Last Run Status
GET /api/plugins/:id/runs → { "results": { "lastRunAt", "lastStatus", "lastError" } } (lastStatus is ok | error | running).
Delete Instance
DELETE /api/plugins/:id — unschedules, removes the OpenBao secret namespace, and deletes the row.
Configuration Endpoints
Base path: /api/conf
All endpoints require authentication and app_sso_admin membership. Runtime configuration (SMTP, discovery, OAuth) is stored in OpenBao at secret/sso-manager/conf and overlaid onto the live app config; changes take effect immediately and persist across restarts. Secret fields (smtp.pass, oauth.jwtSecret) are always returned masked (********); submit a blank or ******** value to keep the current stored secret, or a new non-blank value to replace it.
Get Configuration
GET /api/conf — returns the editable config groups (smtp, discovery, oauth) with secret fields masked to ********.
Response:
{
"smtp": { "host": "smtp.example.com", "port": 587, "secure": false, "user": "noreply@example.com", "pass": "********", "from": "SSO Manager <noreply@example.com>" },
"discovery": { },
"oauth": { "issuer": "https://sso.example.com", "jwtSecret": "********", "token_lifetime": { "access_token": 3600, "refresh_token": 2592000 } }
}
Save Configuration
POST /api/conf — deep-merges the submitted groups into secret/sso-manager/conf (per-key shallow merge of nested objects) and re-applies them to the live config. A blank or ******** value for smtp.pass or oauth.jwtSecret preserves the stored secret.
Request:
{
"smtp": { "host": "smtp.example.com", "port": 587, "secure": false, "user": "noreply@example.com", "pass": "********", "from": "SSO Manager <noreply@example.com>" },
"oauth": { "issuer": "https://sso.example.com", "token_lifetime": { "access_token": 3600, "refresh_token": 2592000 } }
}
Response: { "success": true }
Error Responses
All endpoints return errors in this format:
{ "name": "ErrorName", "message": "Error message description" }
HTTP Status Codes
| Code | Meaning |
|---|---|
200 |
Success |
400 |
Bad Request — invalid input |
401 |
Unauthorized — auth required, token invalid, or insufficient permission |
404 |
Not Found |
429 |
Too Many Requests — rate limit exceeded |
500 |
Internal Server Error |
502 |
Bad Gateway — upstream service failure (e.g. SMS delivery) |