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 registered422 validation.field_invalid— password does not meet policy429 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 required423 resource.locked— account locked,Retry-Afterheader set429 rate.limited— too many attempts
2.3 POST /api/v1/auth/sso/{provider}/start
Initiate SSO flow.
Path params: provider — google | 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 revoked401 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 invalid422 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 invalid423 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 registered422 validation.field_invalid— invalid public key format429 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 step409 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 invalid422 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
}
| Claim | Meaning | Required |
|---|---|---|
iss | Issuer URL | yes |
aud | Audience (platform identifier) | yes |
sub | UserId | yes |
tid | Active TenantId | yes (except bootstrap) |
tids | Available TenantIds | yes |
roles | Coarse RBAC roles | yes |
scope | Space-separated OAuth scopes | yes |
did | DeviceId | when device-bound |
amr | Authentication Methods Reference | yes |
iat | Issued at (unix) | yes |
exp | Expires (unix) | yes |
jti | Token ID = SessionId | yes |
v | Claim schema version | yes |
11. Error Model
All errors follow 05 API Design §8 (problem+json).
Identity-specific error codes:
| Code | HTTP | Meaning |
|---|---|---|
auth.invalid_token | 401 | Token invalid, expired, or unknown |
auth.mfa_required | 401 | MFA challenge required |
auth.email_unverified | 403 | Email verification required before proceeding |
auth.rotation_reuse_detected | 401 | Refresh token reuse; session family revoked |
auth.unauthenticated | 401 | Missing credentials |
auth.sso_state_invalid | 400 | SSO state parameter invalid or expired |
auth.sso_assertion_invalid | 400 | SAML/OIDC assertion validation failed |
auth.account_locked | 423 | Account temporarily locked |
auth.account_disabled | 403 | Account permanently disabled |
resource.not_found | 404 | User/device/key not found |
resource.conflict | 409 | Email exists, device fingerprint collision, etc. |
validation.field_invalid | 422 | Invalid input (weak password, bad email, etc.) |
rate.limited | 429 | Too many requests |
12. Security Requirements
| Requirement | Enforcement |
|---|---|
| All endpoints TLS 1.3 only | Enforced at edge |
Write endpoints require Idempotency-Key | Middleware |
| Session endpoints require valid JWT | Guard |
Tenant-scoped endpoints require X-Tenant-Id matching JWT tid | Middleware |
| Sensitive endpoints require recent auth (< 5 min for MFA/device/password changes) | Step-up guard |
| Login/refresh rate-limited per IP + per account | Redis 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.