Skip to main content

Auth Service — API Contracts

Status: populated Owner: Platform Engineering + Security Last updated: 2026-04-19

Change log

  • v1.2 (2026-04-19) — Multi-IdP surface: added OIDC start/callback and SAML ACS/SLS endpoints (brokered via Keycloak), tenant IdP management endpoints for admins, and SCIM 2.0 inbound for enterprise tenants. /v1/auth/login is retained for native / legacy provider login only; SSO tenants go through /v1/auth/sso/start.

All public endpoints fronted by Kong. Internal endpoints (JWKS, api-key lookup, SCIM) reachable only within cluster or via mTLS (no Kong route for JWKS/lookup; SCIM routed by Kong with per-tenant bearer tokens).

1. Public endpoints (Kong-routed)

Authentication — native / legacy

MethodPathAuthPurpose
POST/v1/auth/loginnoneEmail+password (native fallback only) OR legacy Firebase ID token
POST/v1/auth/refreshrefresh cookieRotate refresh; issue new access
POST/v1/auth/logoutaccessRevoke refresh + (where applicable) Keycloak SSO logout
POST/v1/auth/mfa/totp/verifysession ticketSecond factor (native)

Authentication — Keycloak / tenant external SSO (OIDC)

MethodPathAuthPurpose
GET/v1/auth/sso/start?tenant={slug}noneRedirects to Keycloak broker URL for the tenant's provider
GET/v1/auth/sso/callbackOIDC codeExchanges code at Keycloak /token, normalises claims, issues platform JWT
POST/v1/auth/sso/logoutaccessSingle-logout (Keycloak back-channel logout, then clears platform session)

Authentication — SAML 2.0 (tenant external SSO, brokered)

MethodPathAuthPurpose
GET/v1/auth/saml/{providerId}/metadatanoneSP metadata (consumed by tenant IdP)
POST/v1/auth/saml/{providerId}/acsSAML <Response>Assertion Consumer Service; verifies signature, exchanges for platform JWT
POST/v1/auth/saml/{providerId}/slsSAML <LogoutRequest>Single-Logout Service

Tenant IdP management (admin within tenant)

MethodPathAuthPurpose
GET/v1/tenants/{tenantId}/identity-providerstenant.adminList configured providers + status
POST/v1/tenants/{tenantId}/identity-providerstenant.adminRegister OIDC (discoveryUrl) or SAML (metadataXml) provider
PATCH/v1/tenants/{tenantId}/identity-providers/{providerId}tenant.adminUpdate mappers / disable
DELETE/v1/tenants/{tenantId}/identity-providers/{providerId}tenant.adminRemove (cannot remove default keycloak)
POST/v1/tenants/{tenantId}/identity-providers/{providerId}/testtenant.adminTrigger a validation probe against the upstream IdP

Users (admin within account)

MethodPathAuthPurpose
POST/v1/usersaccount.adminCreate user
GET/v1/users/{id}account.admin OR selfRead
PATCH/v1/users/{id}account.admin OR selfUpdate
DELETE/v1/users/{id}account.adminSoft-delete (GDPR: hard-delete via erasure job)
POST/v1/users/{id}/rolesaccount.adminAssign role
DELETE/v1/users/{id}/roles/{role}account.adminRemove role

API keys

MethodPathAuthPurpose
POST/v1/api-keysaccount.adminCreate; returns raw key ONCE
GET/v1/api-keysaccount.adminList (metadata only)
GET/v1/api-keys/{id}account.adminMetadata
DELETE/v1/api-keys/{id}account.adminRevoke

Self

MethodPathAuthPurpose
GET/v1/users/meaccessProfile + roles
PATCH/v1/users/me/passwordaccess (MFA if enabled)Change password
POST/v1/users/me/mfa/totpaccessEnable TOTP
DELETE/v1/users/me/mfa/totpaccess (TOTP confirm)Disable

2. Internal endpoints (NO Kong route)

MethodPathCallerPurpose
GET/.well-known/jwks.jsonKong JWT pluginJWKS
GET/v1/api-keys/lookup?hash=Kong custom pluginResolve raw-key-hash → consumer
GET/health/live, /health/ready, /metricsSREOperational

2a. SCIM 2.0 (per-tenant inbound provisioning)

MethodPathAuthPurpose
GET/POST/PATCH/DELETE/scim/v2/Users, /scim/v2/Users/{id}Per-tenant SCIM bearerUser CRUD pushed by tenant IdP (Okta/Azure AD)
GET/POST/PATCH/DELETE/scim/v2/Groups, /scim/v2/Groups/{id}Per-tenant SCIM bearerGroup CRUD
GET/scim/v2/ServiceProviderConfigPer-tenant SCIM bearerCapabilities doc

JWKS format (RFC 7517):

{
"keys": [
{ "kid": "k_2026_04", "kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "AQAB" },
{ "kid": "k_2026_05", "kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "AQAB" }
]
}

3. Token shapes

Access JWT payload:

{
"iss": "https://auth.ghasi-sms.io",
"aud": "ghasi-sms",
"sub": "usr_01H...",
"account_id": "550e8400-...",
"tenant_id": "660e8400-...",
"scopes": ["sms:send", "sms:read"],
"roles": ["account.sender"],
"idp": "keycloak",
"idp_sub": "abc-keycloak-subject",
"iat": 1713437200, "exp": 1713438100,
"jti": "jwt_01H..."
}

The idp claim records which provider authenticated the session (keycloak, tenant-oidc:<tenantId>, tenant-saml:<tenantId>, firebase-legacy, native). Downstream services MUST NOT branch on this claim for authorization; it exists for audit and debugging only.

4. Error codes

HTTPCodeWhen
400INVALID_CREDENTIALSBad email/password
401UNAUTHENTICATEDNo / expired token
401MFA_REQUIREDMFA challenge pending
403INSUFFICIENT_SCOPEToken OK, scope missing
404NOT_FOUNDUser / key not in account scope
409EMAIL_ALREADY_EXISTSDuplicate email per account
423ACCOUNT_LOCKEDToo many failures
429RATE_LIMITED(Kong)
400IDP_DISCOVERY_FAILEDTenant-registered OIDC discovery URL unreachable or malformed
400SAML_SIGNATURE_INVALIDSAML <Response> signature verification failed
400IDP_CLAIMS_INCOMPLETERequired attributes (email, sub) missing from IdP response
403RESERVED_DOMAINTenant IdP attempted to claim a reserved email domain
409IDP_ALREADY_CONFIGUREDAnother provider of same kind already configured for the tenant