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/loginis 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
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /v1/auth/login | none | Email+password (native fallback only) OR legacy Firebase ID token |
| POST | /v1/auth/refresh | refresh cookie | Rotate refresh; issue new access |
| POST | /v1/auth/logout | access | Revoke refresh + (where applicable) Keycloak SSO logout |
| POST | /v1/auth/mfa/totp/verify | session ticket | Second factor (native) |
Authentication — Keycloak / tenant external SSO (OIDC)
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /v1/auth/sso/start?tenant={slug} | none | Redirects to Keycloak broker URL for the tenant's provider |
| GET | /v1/auth/sso/callback | OIDC code | Exchanges code at Keycloak /token, normalises claims, issues platform JWT |
| POST | /v1/auth/sso/logout | access | Single-logout (Keycloak back-channel logout, then clears platform session) |
Authentication — SAML 2.0 (tenant external SSO, brokered)
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /v1/auth/saml/{providerId}/metadata | none | SP metadata (consumed by tenant IdP) |
| POST | /v1/auth/saml/{providerId}/acs | SAML <Response> | Assertion Consumer Service; verifies signature, exchanges for platform JWT |
| POST | /v1/auth/saml/{providerId}/sls | SAML <LogoutRequest> | Single-Logout Service |
Tenant IdP management (admin within tenant)
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /v1/tenants/{tenantId}/identity-providers | tenant.admin | List configured providers + status |
| POST | /v1/tenants/{tenantId}/identity-providers | tenant.admin | Register OIDC (discoveryUrl) or SAML (metadataXml) provider |
| PATCH | /v1/tenants/{tenantId}/identity-providers/{providerId} | tenant.admin | Update mappers / disable |
| DELETE | /v1/tenants/{tenantId}/identity-providers/{providerId} | tenant.admin | Remove (cannot remove default keycloak) |
| POST | /v1/tenants/{tenantId}/identity-providers/{providerId}/test | tenant.admin | Trigger a validation probe against the upstream IdP |
Users (admin within account)
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /v1/users | account.admin | Create user |
| GET | /v1/users/{id} | account.admin OR self | Read |
| PATCH | /v1/users/{id} | account.admin OR self | Update |
| DELETE | /v1/users/{id} | account.admin | Soft-delete (GDPR: hard-delete via erasure job) |
| POST | /v1/users/{id}/roles | account.admin | Assign role |
| DELETE | /v1/users/{id}/roles/{role} | account.admin | Remove role |
API keys
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /v1/api-keys | account.admin | Create; returns raw key ONCE |
| GET | /v1/api-keys | account.admin | List (metadata only) |
| GET | /v1/api-keys/{id} | account.admin | Metadata |
| DELETE | /v1/api-keys/{id} | account.admin | Revoke |
Self
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /v1/users/me | access | Profile + roles |
| PATCH | /v1/users/me/password | access (MFA if enabled) | Change password |
| POST | /v1/users/me/mfa/totp | access | Enable TOTP |
| DELETE | /v1/users/me/mfa/totp | access (TOTP confirm) | Disable |
2. Internal endpoints (NO Kong route)
| Method | Path | Caller | Purpose |
|---|---|---|---|
| GET | /.well-known/jwks.json | Kong JWT plugin | JWKS |
| GET | /v1/api-keys/lookup?hash= | Kong custom plugin | Resolve raw-key-hash → consumer |
| GET | /health/live, /health/ready, /metrics | SRE | Operational |
2a. SCIM 2.0 (per-tenant inbound provisioning)
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET/POST/PATCH/DELETE | /scim/v2/Users, /scim/v2/Users/{id} | Per-tenant SCIM bearer | User CRUD pushed by tenant IdP (Okta/Azure AD) |
| GET/POST/PATCH/DELETE | /scim/v2/Groups, /scim/v2/Groups/{id} | Per-tenant SCIM bearer | Group CRUD |
| GET | /scim/v2/ServiceProviderConfig | Per-tenant SCIM bearer | Capabilities 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
| HTTP | Code | When |
|---|---|---|
| 400 | INVALID_CREDENTIALS | Bad email/password |
| 401 | UNAUTHENTICATED | No / expired token |
| 401 | MFA_REQUIRED | MFA challenge pending |
| 403 | INSUFFICIENT_SCOPE | Token OK, scope missing |
| 404 | NOT_FOUND | User / key not in account scope |
| 409 | EMAIL_ALREADY_EXISTS | Duplicate email per account |
| 423 | ACCOUNT_LOCKED | Too many failures |
| 429 | RATE_LIMITED | (Kong) |
| 400 | IDP_DISCOVERY_FAILED | Tenant-registered OIDC discovery URL unreachable or malformed |
| 400 | SAML_SIGNATURE_INVALID | SAML <Response> signature verification failed |
| 400 | IDP_CLAIMS_INCOMPLETE | Required attributes (email, sub) missing from IdP response |
| 403 | RESERVED_DOMAIN | Tenant IdP attempted to claim a reserved email domain |
| 409 | IDP_ALREADY_CONFIGURED | Another provider of same kind already configured for the tenant |