Skip to main content

API Contracts

:::info Source Sourced from services/identity-service/API_CONTRACTS.md in the documentation repo. :::

Companion: 05 API Design · APPLICATION_LOGIC

All endpoints conform to the platform API conventions in 05 API Design: cursor pagination, problem+json errors, required Idempotency-Key on writes, mandatory X-Tenant-Id where applicable, optimistic concurrency via If-Match.

1. Base Path

/api/v1 for tenant REST. JWKS at root /.well-known/jwks.json.

Provider abstraction: Public auth routes (/auth/*, /users/me/*) are stable regardless of whether the deployment uses the in-house credential store, Keycloak, or vendor OIDC adapters (Firebase, Okta, Cognito, etc.). Optional operator-only configuration APIs for provider wiring may be added under a separate base path; they are not required for end-user clients.

2. Authentication Endpoints

2.1 POST /api/v1/auth/register

Register a new user with email + password.

Request:

POST /api/v1/auth/register
Content-Type: application/json
Idempotency-Key: 01HN2K3P4Q5R6S7T8V9W0X1Y2Z

{
"email": "user@example.com",
"password": "CorrectHorseBatteryStaple!42",
"homeTenantId": "ten_01HN...",
"registrationSource": "self"
}

Response 201:

{
"data": {
"userId": "usr_01HN...",
"primaryEmail": "user@example.com",
"status": "pending_verification",
"emailVerified": false,
"createdAt": "2026-04-15T10:00:00Z"
},
"meta": { "requestId": "req_01HN...", "apiVersion": "v1.0" }
}

Errors:

  • 409 resource.conflict — email already registered
  • 422 validation.field_invalid — password does not meet policy
  • 429 rate.limited — too many registrations from IP

2.2 POST /api/v1/auth/login

Authenticate with email + password.

Request:

POST /api/v1/auth/login
Content-Type: application/json
Idempotency-Key: 01HN...

{
"email": "user@example.com",
"password": "CorrectHorseBatteryStaple!42",
"deviceId": "dev_01HN...",
"tenantId": "ten_01HN..."
}

Response 200 (success, no MFA):

{
"data": {
"accessToken": "eyJhbGc...",
"refreshToken": "rft_...",
"expiresIn": 900,
"tokenType": "Bearer",
"user": {
"id": "usr_01HN...",
"email": "user@example.com",
"tenantId": "ten_01HN...",
"availableTenants": ["ten_01HN...", "ten_02HN..."]
}
}
}

Response 401 (MFA required):

{
"error": {
"type": "https://errors.ghasi.io/auth/mfa_required",
"code": "auth.mfa_required",
"title": "Multi-factor authentication required",
"status": 401,
"detail": "Complete MFA challenge to continue.",
"mfaChallengeToken": "mct_...",
"availableFactors": ["totp", "webauthn"],
"riskReasons": ["new_device", "atypical_location"]
}
}

Errors:

  • 401 auth.invalid_token — bad credentials (same response time as unknown email)
  • 403 auth.email_unverified — email verification required
  • 423 resource.locked — account locked, Retry-After header set
  • 429 rate.limited — too many attempts

2.3 POST /api/v1/auth/sso/{provider}/start

Initiate SSO flow.

Path params: providergoogle | microsoft | oidc | saml | {custom-provider-id}

Request:

POST /api/v1/auth/sso/google/start
Content-Type: application/json

{
"tenantId": "ten_01HN...",
"returnUrl": "https://app.ghasi.io/dashboard"
}

Response 200:

{
"data": {
"redirectUrl": "https://accounts.google.com/o/oauth2/v2/auth?...",
"state": "st_...",
"expiresIn": 300
}
}

2.4 GET /api/v1/auth/sso/{provider}/callback

OIDC callback (SAML callback is POST).

Query params: code, state

Response 302: Redirects to returnUrl with short-lived auth code.

Follow-up: Client exchanges code for tokens via a separate token endpoint.

2.5 POST /api/v1/auth/refresh

Rotate refresh token, issue new access token.

Request:

POST /api/v1/auth/refresh
Content-Type: application/json
Idempotency-Key: 01HN...

{
"refreshToken": "rft_..."
}

Response 200:

{
"data": {
"accessToken": "eyJhbGc...",
"refreshToken": "rft_...",
"expiresIn": 900,
"tokenType": "Bearer"
}
}

Errors:

  • 401 auth.invalid_token — token invalid, expired, or revoked
  • 401 auth.rotation_reuse_detected — token reuse detected; entire session family revoked

2.6 POST /api/v1/auth/logout

Revoke current session.

Request:

POST /api/v1/auth/logout
Authorization: Bearer eyJhbGc...

Response 204: No content.

3. MFA Endpoints

3.0 GET /api/v1/users/me/mfa — List enrolled factors

Auth: BFF forwards x-user-id (same pattern as /users/me/devices).

Response 200:

{
"data": {
"factors": [
{
"id": "mfa_01HN...",
"kind": "webauthn",
"verified": true,
"label": "Work laptop",
"enrolledAt": "2026-04-19T12:00:00.000Z"
}
]
}
}

3.1 POST /api/v1/users/me/mfa/enroll

Enroll a new MFA factor.

Request:

POST /api/v1/users/me/mfa/enroll
Authorization: Bearer eyJhbGc...
Content-Type: application/json
Idempotency-Key: 01HN...

{
"kind": "totp"
}

Response 200 (TOTP):

{
"data": {
"factorId": "mfa_01HN...",
"kind": "totp",
"secret": "JBSWY3DPEHPK3PXP",
"provisioningUri": "otpauth://totp/Ghasi:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Ghasi",
"qrCode": "data:image/png;base64,iVBORw0KG...",
"verificationRequired": true
}
}

Response 200 (Recovery Codes):

{
"data": {
"factorId": "mfa_01HN...",
"kind": "recovery_codes",
"codes": [
"A1B2-C3D4",
"E5F6-G7H8",
"..."
],
"warning": "Store these codes securely. They will not be shown again."
}
}

3.1a POST /api/v1/users/me/mfa/webauthn/registration-options

Begin passkey (WebAuthn) enrollment after password login returned auth.mfa_required with enrollmentRequired: true and mfaChallengeToken.

Request:

POST /api/v1/users/me/mfa/webauthn/registration-options
Content-Type: application/json
Idempotency-Key: 01HN...

{
"mfaChallengeToken": "<JWT from login error>",
"label": "Work laptop"
}

Response 200:

{
"data": {
"sessionId": "wrs_01HN...",
"publicKeyCredentialCreationOptions": { "...": "PublicKeyCredentialCreationOptionsJSON" }
}
}

Errors:

  • 401 auth.invalid_token — challenge expired or invalid
  • 422 auth.webauthn_unavailable — operator disabled WebAuthn; client should offer TOTP / recovery codes

3.1b POST /api/v1/users/me/mfa/webauthn/registration-verify

Complete passkey enrollment with the browser navigator.credentials.create() result.

Request:

POST /api/v1/users/me/mfa/webauthn/registration-verify
Content-Type: application/json
Idempotency-Key: 01HN...

{
"mfaChallengeToken": "<same as registration-options>",
"sessionId": "wrs_01HN...",
"credential": { "...": "RegistrationResponseJSON" },
"label": "Work laptop"
}

Response 200:

{
"data": {
"factorId": "mfa_01HN...",
"kind": "webauthn",
"verified": true
}
}

Emits identity.user.mfa_enrolled.v1.

3.1c POST /api/v1/users/me/mfa/webauthn/registration-cancel

Abandon passkey enrollment after registration-options (no credential persisted). Emits identity.user.webauthn_registration_canceled.v1 for analytics.

Request:

POST /api/v1/users/me/mfa/webauthn/registration-cancel
Content-Type: application/json
Idempotency-Key: 01HN...

{
"mfaChallengeToken": "<JWT from login error>",
"sessionId": "wrs_01HN...",
"reason": "user_closed_dialog"
}

Response 204: No content.

Errors:

  • 401 auth.invalid_token — challenge or session invalid / expired

3.2 POST /api/v1/users/me/mfa/challenge

Submit MFA code for verification or step-up.

Request:

POST /api/v1/users/me/mfa/challenge
Authorization: Bearer eyJhbGc...
Content-Type: application/json
Idempotency-Key: 01HN...

{
"factorId": "mfa_01HN...",
"code": "123456",
"mfaChallengeToken": "mct_..."
}

Response 200: Returns token pair (same shape as login response).

Errors:

  • 401 auth.invalid_token — code invalid
  • 423 resource.locked — too many MFA failures

3.3 DELETE /api/v1/users/me/mfa/{factorId}

Remove an MFA factor.

Response 204.

Errors:

  • 409 resource.conflict — cannot remove last factor when tenant requires MFA

4. Device Endpoints

4.1 POST /api/v1/users/me/devices

Register a new device.

Request:

POST /api/v1/users/me/devices
Authorization: Bearer eyJhbGc...
Content-Type: application/json
Idempotency-Key: 01HN...

{
"fingerprint": "sha256-...",
"publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
"userAgent": "GhasiPlayer/1.0 (Windows 11)"
}

Response 201:

{
"data": {
"deviceId": "dev_01HN...",
"fingerprint": "sha256-...",
"trusted": false,
"createdAt": "2026-04-15T10:00:00Z"
}
}

Errors:

  • 409 resource.conflict — device already registered
  • 422 validation.field_invalid — invalid public key format
  • 429 rate.limited — max devices reached (5)

4.2 POST /api/v1/users/me/devices/{id}/bind-offline

Issue offline binding certificate.

Request:

POST /api/v1/users/me/devices/dev_01HN.../bind-offline
Authorization: Bearer eyJhbGc...
Idempotency-Key: 01HN...

Response 200:

{
"data": {
"deviceId": "dev_01HN...",
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
"issuedAt": "2026-04-15T10:00:00Z",
"expiresAt": "2026-07-14T10:00:00Z",
"kid": "idsvc-ca-2026-01"
}
}

Errors:

  • 403 authz.forbidden — device not trusted; requires in-person trust step
  • 409 resource.conflict — certificate already active

4.3 DELETE /api/v1/users/me/devices/{id}

Revoke a device. Cascades: all sessions on the device are revoked.

Response 204.

4.4 GET /api/v1/users/me/devices

List devices.

Response 200:

{
"data": [
{
"id": "dev_01HN...",
"fingerprint": "sha256-...",
"userAgent": "GhasiPlayer/1.0 (Windows 11)",
"trusted": true,
"lastSeenAt": "2026-04-14T18:22:00Z",
"offlineCertificate": {
"expiresAt": "2026-07-14T10:00:00Z"
}
}
],
"meta": { "page": { "size": 20, "nextCursor": null } }
}

5. API Key Endpoints

5.1 POST /api/v1/api-keys

Issue a new API key.

Request:

POST /api/v1/api-keys
Authorization: Bearer eyJhbGc...
X-Tenant-Id: ten_01HN...
Idempotency-Key: 01HN...
Content-Type: application/json

{
"name": "CI/CD integration",
"scopes": ["catalog:read", "enrollment:write"],
"expiresAt": "2027-04-15T00:00:00Z"
}

Response 201:

{
"data": {
"id": "apk_01HN...",
"prefix": "gk_live_",
"rawKey": "gk_live_abc123def456...",
"name": "CI/CD integration",
"scopes": ["catalog:read", "enrollment:write"],
"expiresAt": "2027-04-15T00:00:00Z",
"createdAt": "2026-04-15T10:00:00Z",
"warning": "rawKey will not be shown again. Store it securely."
}
}

5.2 DELETE /api/v1/api-keys/{id}

Revoke API key.

Response 204.

5.3 GET /api/v1/api-keys

List API keys for tenant.

Response 200:

{
"data": [
{
"id": "apk_01HN...",
"prefix": "gk_live_",
"name": "CI/CD integration",
"scopes": ["catalog:read", "enrollment:write"],
"createdAt": "2026-04-15T10:00:00Z",
"lastUsedAt": "2026-04-15T12:34:00Z",
"expiresAt": "2027-04-15T00:00:00Z",
"revokedAt": null
}
],
"meta": { "page": { "size": 20, "nextCursor": null, "totalApproximate": 5 } }
}

6. Password Reset Endpoints

6.1 POST /api/v1/auth/password/reset/request

Request password reset.

Request:

POST /api/v1/auth/password/reset/request
Content-Type: application/json
Idempotency-Key: 01HN...

{
"email": "user@example.com"
}

Response 200 (always — prevents enumeration):

{
"data": {
"message": "If an account exists for this email, a reset link has been sent."
}
}

6.2 POST /api/v1/auth/password/reset/complete

Complete password reset.

Request:

POST /api/v1/auth/password/reset/complete
Content-Type: application/json
Idempotency-Key: 01HN...

{
"token": "prt_...",
"newPassword": "NewStrongPassword!2026"
}

Response 200:

{
"data": {
"message": "Password reset complete. All active sessions have been revoked."
}
}

Errors:

  • 401 auth.invalid_token — token expired or invalid
  • 422 validation.field_invalid — password fails policy

7. Email Verification

7.1 POST /api/v1/auth/email/verify

Verify email via token from verification email.

Request:

POST /api/v1/auth/email/verify
Content-Type: application/json

{
"token": "evt_..."
}

Response 200: User status transitions to active.

7.2 POST /api/v1/auth/email/verify/resend

Resend verification email.

Rate limit: 3 per 15 minutes per user.

8. JWKS Endpoint

8.1 GET /.well-known/jwks.json

Public signing keys for JWT verification.

Response 200:

{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
"kid": "idsvc-2026-01",
"use": "sig",
"alg": "EdDSA"
},
{
"kty": "OKP",
"crv": "Ed25519",
"x": "...",
"kid": "idsvc-2025-07",
"use": "sig",
"alg": "EdDSA"
}
]
}

Caching: Cache-Control: public, max-age=3600, stale-while-revalidate=86400. Multiple keys published during rotation windows (grace overlap).

9. Me (Profile) Endpoint

9.1 GET /api/v1/users/me

Return authenticated user's identity data.

Response 200:

{
"data": {
"id": "usr_01HN...",
"primaryEmail": "user@example.com",
"emailVerified": true,
"status": "active",
"homeTenantId": "ten_01HN...",
"createdAt": "2025-01-01T00:00:00Z",
"mfaEnrolled": true,
"mfaFactors": [
{ "id": "mfa_01HN...", "kind": "totp", "verified": true, "enrolledAt": "..." }
],
"externalIdentities": [
{ "provider": "google", "subject": "...", "linkedAt": "..." }
]
}
}

10. JWT Claim Contract (Published Language)

All downstream services conform to this claim schema. Changes require a new v major.

{
"iss": "https://auth.ghasi.io",
"aud": "ghasi-platform",
"sub": "usr_01HN...",
"tid": "ten_01HN...",
"tids": ["ten_01HN...", "ten_02HN..."],
"roles": ["learner"],
"scope": "openid profile catalog:read enrollment:write",
"did": "dev_01HN...",
"amr": ["pwd", "webauthn"],
"iat": 1755100000,
"exp": 1755100900,
"jti": "ses_01HN...",
"v": 1
}
ClaimMeaningRequired
issIssuer URLyes
audAudience (platform identifier)yes
subUserIdyes
tidActive TenantIdyes (except bootstrap)
tidsAvailable TenantIdsyes
rolesCoarse RBAC rolesyes
scopeSpace-separated OAuth scopesyes
didDeviceIdwhen device-bound
amrAuthentication Methods Referenceyes
iatIssued at (unix)yes
expExpires (unix)yes
jtiToken ID = SessionIdyes
vClaim schema versionyes

11. Error Model

All errors follow 05 API Design §8 (problem+json).

Identity-specific error codes:

CodeHTTPMeaning
auth.invalid_token401Token invalid, expired, or unknown
auth.mfa_required401MFA challenge required
auth.email_unverified403Email verification required before proceeding
auth.rotation_reuse_detected401Refresh token reuse; session family revoked
auth.unauthenticated401Missing credentials
auth.sso_state_invalid400SSO state parameter invalid or expired
auth.sso_assertion_invalid400SAML/OIDC assertion validation failed
auth.account_locked423Account temporarily locked
auth.account_disabled403Account permanently disabled
resource.not_found404User/device/key not found
resource.conflict409Email exists, device fingerprint collision, etc.
validation.field_invalid422Invalid input (weak password, bad email, etc.)
rate.limited429Too many requests

12. Security Requirements

RequirementEnforcement
All endpoints TLS 1.3 onlyEnforced at edge
Write endpoints require Idempotency-KeyMiddleware
Session endpoints require valid JWTGuard
Tenant-scoped endpoints require X-Tenant-Id matching JWT tidMiddleware
Sensitive endpoints require recent auth (< 5 min for MFA/device/password changes)Step-up guard
Login/refresh rate-limited per IP + per accountRedis token bucket

13. Pagination

All list endpoints use cursor pagination per 05 §6. Max page[size] = 200.

14. Versioning

Current: v1. Major bumps via new path prefix. Claim schema v field enables consumers to detect version.

15. OpenAPI

Full OpenAPI 3.1 spec published at /openapi.json. CI runs openapi-diff to detect breaking changes.