Skip to main content

iam-service — API Contracts

Catalog · 05 API Design · DOMAIN_MODEL · Standards · ERROR_CODES

All endpoints are versioned /api/v1/. Auth-related endpoints share the prefix /api/v1/auth/. JWKS is served at the platform-conventional well-known path outside the /api/v1/ namespace. Specs published as OpenAPI 3.1 at services/iam-service/openapi.yaml; CI diffs against committed file on every PR.

1. Conventions

AspectRule
Media typeapplication/json; charset=utf-8
IdempotencyIdempotency-Key: <uuid> required on all writes; 24-h TTL.
Tenant contextX-Tenant-Id: <ten_…> required on tenant-scoped endpoints (or carried in JWT tid). Mismatch → 403 MELMASTOON.GENERAL.TENANT_FORBIDDEN.
Tracingtraceparent (W3C) propagated; service injects if absent.
PaginationCursor-based: ?cursor=…&limit=… (max 100). Response: { items, nextCursor }.
Errorsapplication/problem+json envelope per ERROR_CODES.md.
AuthAuthorization: Bearer <accessJwt> for user routes; X-API-Key: <mlk_…> for machine routes.
Rate limitPer-IP and per-email; advertised in Retry-After + X-RateLimit-* headers.
TLSTLS 1.3 only; HSTS 1y; pinned in mobile clients.

2. Standard Error Envelope

{
"type": "https://errors.melmastoon.io/iam/invalid-credentials",
"code": "MELMASTOON.IAM.INVALID_CREDENTIALS",
"title": "Invalid credentials",
"status": 401,
"detail": "The email or password you entered is incorrect.",
"traceId": "00-3ab...-abc...-01",
"requestId": "req_01HZ...",
"tenantId": null,
"retriable": false,
"userMessageKey": "auth.invalid_credentials",
"docUrl": "https://docs.melmastoon.io/errors/IAM/INVALID_CREDENTIALS"
}

3. Auth Endpoints

3.1 POST /api/v1/auth/register

Auth: none · Idempotent: yes · Rate: 20/h per IP, 5/h per email.

POST /api/v1/auth/register HTTP/1.1
Idempotency-Key: 7b9b...-e3
X-Tenant-Id: ten_01HZ8X...
Content-Type: application/json

{
"email": "front-desk@hotel-pamir.example",
"password": "Q!7sun-river-2026",
"userType": "staff",
"tenantId": "ten_01HZ8X...",
"locale": "ps-AF"
}

202 Accepted:

{
"userId": "usr_01HZ8XW...",
"status": "pending_verification",
"verificationDispatched": true
}
CodeStatus
MELMASTOON.IAM.WEAK_PASSWORD422
MELMASTOON.IAM.BREACHED_CREDENTIAL422
MELMASTOON.IAM.INVALID_EMAIL422
MELMASTOON.IAM.JIT_DISALLOWED403
MELMASTOON.GENERAL.RATE_LIMITED429
MELMASTOON.GENERAL.IDEMPOTENCY_CONFLICT409

3.2 POST /api/v1/auth/login

Auth: none · Idempotent: yes (re-issues same session if replayed within TTL).

{
"email": "gm@hotel-pamir.example",
"password": "********",
"tenantSlug": "hotel-pamir",
"deviceFingerprint": {
"value": "8c3a9f...e1",
"attributes": { "osFamily": "windows", "osVersion": "11.0", "appVersion": "1.4.2", "cpuArch": "x64", "machineIdHash": "ab12..." }
}
}

200 OK (no MFA required):

{
"requiresMfa": false,
"accessToken": "eyJhbGci...",
"refreshToken": "rft_01HZ...XK4P",
"tokenType": "Bearer",
"expiresIn": 900,
"session": { "id": "ses_01HZ...", "tenantId": "ten_01HZ...", "deviceId": "dev_01HZ..." },
"user": { "id": "usr_01HZ...", "primaryEmail": "gm@hotel-pamir.example", "userType": "staff" }
}

200 OK (MFA required):

{
"requiresMfa": true,
"mfaTicket": "mft_eyJhbGci...",
"availableFactors": ["totp", "webauthn"],
"ticketExpiresAt": "2026-04-22T10:05:00Z"
}
CodeStatus
MELMASTOON.IAM.INVALID_CREDENTIALS401
MELMASTOON.IAM.ACCOUNT_LOCKED423
MELMASTOON.IAM.EMAIL_NOT_VERIFIED403
MELMASTOON.IAM.TENANT_DISABLED403
MELMASTOON.GENERAL.RATE_LIMITED429

3.3 POST /api/v1/auth/refresh

Auth: Authorization: Bearer <refreshToken> · Idempotent: No (rotation is destructive); replays detected via reuse path.

200 OK:

{
"accessToken": "eyJhbGci...",
"refreshToken": "rft_01HZ...XKQP",
"tokenType": "Bearer",
"expiresIn": 900,
"rotated": true
}
CodeStatus
MELMASTOON.IAM.REFRESH_INVALID401
MELMASTOON.IAM.REFRESH_EXPIRED401
MELMASTOON.IAM.REFRESH_REUSE401 (and family revoked)
MELMASTOON.IAM.SESSION_REVOKED401

3.4 POST /api/v1/auth/logout

200 OK:

{ "revoked": true, "sessionId": "ses_01HZ..." }

Revokes the refresh family for the calling session. Other sessions for the user are unaffected unless ?all=true is supplied (admin scope).

3.5 POST /api/v1/auth/mfa/enroll

Auth: Bearer <accessJwt> (with acr=fresh-auth, ≤ 5 min since password verify).

{ "type": "totp", "label": "iPhone 15" }

201 Created:

{
"factorId": "mfa_01HZ...",
"type": "totp",
"otpauthUrl": "otpauth://totp/Melmastoon:gm@hotel-pamir?secret=JBSW...&issuer=Melmastoon",
"qrPng": "data:image/png;base64,iVBOR..."
}

For webauthn:

{
"factorId": "mfa_01HZ...",
"type": "webauthn",
"creationOptions": { "challenge": "...", "rp": { "id": "id.melmastoon.io", "name": "Melmastoon" }, "pubKeyCredParams": [...] }
}

Followed by POST /api/v1/auth/mfa/verify with { factorId, proof } to mark verified.

3.6 POST /api/v1/auth/mfa/challenge

{ "mfaTicket": "mft_eyJhbGci...", "factor": "totp", "code": "418562" }

For WebAuthn: { mfaTicket, factor: "webauthn", assertion: { id, rawId, response, type } }.

200 OK: same shape as /auth/login success envelope (without mfaTicket).

CodeStatus
MELMASTOON.IAM.MFA_INVALID401
MELMASTOON.IAM.MFA_TICKET_EXPIRED401
MELMASTOON.IAM.MFA_TICKET_USED401

3.7 POST /api/v1/auth/sso/oidc/{provider}/start

{ "tenantSlug": "hotel-pamir", "returnUrl": "https://backoffice.melmastoon.io/" }

200 OK:

{
"authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth?...",
"state": "st_01HZ...",
"expiresAt": "2026-04-22T10:10:00Z"
}

3.8 POST /api/v1/auth/sso/oidc/{provider}/callback

{ "code": "4/0AY0e...", "state": "st_01HZ..." }

200 OK: same as /auth/login success.

3.9 POST /api/v1/auth/sso/saml/{tenantSlug}/callback

Content-Type: application/x-www-form-urlencoded, body SAMLResponse=<base64>&RelayState=<rs>.

3.10 POST /api/v1/auth/magic-link/request

{ "email": "guest@example.com", "intent": "login", "tenantSlug": "hotel-pamir", "returnUrl": "https://book.hotel-pamir.example/checkin" }

202 Accepted always (anti-enumeration):

{ "dispatched": true, "expiresInMinutes": 10 }

3.11 POST /api/v1/auth/magic-link/verify

{ "token": "ml_01HZ...XK4P" }

200 OK: same as /auth/login success.

CodeStatus
MELMASTOON.IAM.MAGIC_LINK_USED400
MELMASTOON.IAM.MAGIC_LINK_EXPIRED400
MELMASTOON.IAM.MAGIC_LINK_INVALID400

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

{ "email": "gm@hotel-pamir.example", "tenantSlug": "hotel-pamir" }

202 Accepted always (anti-enumeration):

{ "dispatched": true }

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

{ "token": "ml_01HZ...XK4P", "newPassword": "Q!7sun-river-2026" }

200 OK:

{ "passwordReset": true, "sessionsRevoked": 4 }

3.14 POST /api/v1/auth/devices/register

Auth: Bearer <accessJwt>.

{
"platform": "electron-desktop",
"fingerprint": { "value": "8c3a9f...e1", "attributes": { "osFamily": "windows", "osVersion": "11.0", "appVersion": "1.4.2", "cpuArch": "x64", "machineIdHash": "ab12..." } },
"publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": "11qYAYK..." },
"label": "GM Front-Desk PC #3"
}

201 Created:

{
"deviceId": "dev_01HZ...",
"trusted": false,
"trustConfirmRequired": true,
"trustConfirmDispatchedTo": "gm@hotel-pamir.example"
}

3.15 POST /api/v1/auth/devices/{id}/bind-offline

Auth: Bearer <accessJwt> with acr=fresh-auth (≤ 5 min). Requires Device.platform='electron-desktop' AND Device.trusted=true.

{
"csr": "-----BEGIN CERTIFICATE REQUEST-----\nMIIB...\n-----END CERTIFICATE REQUEST-----",
"requestedTtlDays": 7
}

201 Created:

{
"binding": {
"certificatePem": "-----BEGIN CERTIFICATE-----\nMIIB...\n-----END CERTIFICATE-----",
"issuingKid": "tenant-ca-2026-04-ten_01HZ8X",
"issuedAt": "2026-04-22T10:00:00Z",
"expiresAt": "2026-04-29T10:00:00Z",
"serial": "0xab12..."
}
}
CodeStatus
MELMASTOON.IAM.DEVICE_NOT_TRUSTED403
MELMASTOON.IAM.DEVICE_PLATFORM_INELIGIBLE422
MELMASTOON.IAM.OFFLINE_TTL_EXCEEDED422
MELMASTOON.IAM.FRESH_AUTH_REQUIRED403
MELMASTOON.IAM.TENANT_CA_UNAVAILABLE503

3.16 GET /.well-known/jwks.json

Auth: none · CDN-cached: 5 min, stale-while-revalidate=60.

200 OK:

{
"keys": [
{
"kid": "iam-2026-04",
"kty": "OKP",
"crv": "Ed25519",
"x": "11qYAYK...",
"use": "sig",
"alg": "EdDSA",
"iss": "https://id.melmastoon.io"
},
{
"kid": "iam-2026-03",
"kty": "OKP",
"crv": "Ed25519",
"x": "Pq8abK...",
"use": "sig",
"alg": "EdDSA",
"iss": "https://id.melmastoon.io"
}
]
}

Rotation overlap is ≥ 2 days; current + previous kid always present.

3.17 POST /api/v1/auth/api-keys

Auth: Bearer <accessJwt> (user) OR X-API-Key with admin:*.

{
"label": "lock-service production",
"scopes": ["lock:dispense"],
"propertyIds": ["prop_01HZ..."],
"expiresAt": "2027-04-22T00:00:00Z"
}

201 Created (key returned once):

{
"apiKeyId": "key_01HZ...",
"key": "mlk_01HZAB...XK4P_aw_v9zR0Q",
"prefix": "mlk_01HZAB",
"scopes": ["lock:dispense"],
"propertyIds": ["prop_01HZ..."],
"expiresAt": "2027-04-22T00:00:00Z",
"issuedAt": "2026-04-22T10:00:00Z"
}

3.18 DELETE /api/v1/auth/api-keys/{id}

200 OK:

{ "revoked": true, "revokedAt": "2026-04-22T10:00:00Z" }

3.19 POST /api/v1/users/{id}/lock

Auth: admin (role=tenant_admin for tenant scope, role=platform_admin for global).

{ "reason": "admin", "until": "2026-04-29T10:00:00Z", "note": "Suspicious activity, await audit." }

200 OK:

{ "userId": "usr_01HZ...", "status": "locked", "lockedUntil": "2026-04-29T10:00:00Z" }

3.20 POST /api/v1/users/{id}/unlock

{ "note": "Confirmed legitimate; cleared by GM." }

200 OK:

{ "userId": "usr_01HZ...", "status": "active" }

3.21 POST /api/v1/auth/switch-tenant

For chain operators. Mints a new session bound to a different tid from tids[] of current JWT.

{ "tenantId": "ten_01HZ8XY..." }

200 OK: standard login envelope.

4. JWT Claim Contract

Issued in every accessToken. Conformist contract: changes require platform-wide coordination.

ClaimTypeNotes
issstringhttps://id.melmastoon.io
audstring|string[]melmastoon-platform; SSO-via flows include resource server.
substringUserId
tidstringactive home TenantId; null only for platform_admin
tidsstring[]additional tenant memberships (chain operator)
didstring | nullDeviceId, when login was device-bound
amrstring[]RFC 8176 (pwd, totp, webauthn, magic_link, oidc, saml)
acrstringfresh-auth if password verified ≤ 5 min ago; mfa-strong if WebAuthn within session
scopestringspace-separated scopes (issued by tenant-service enrichment)
rolesstring[]tenant-service-injected (e.g. staff:front_desk); empty for guests
userTypestringstaff | guest | platform_admin
iatnumberissued at (Unix s)
expnumberexpiry (Unix s); access TTL = 900
nbfnumbernot before
jtistringunique token id (ULID)
kidheadersigning key ID (e.g. iam-2026-04)
algheaderalways EdDSA

Example decoded payload:

{
"iss": "https://id.melmastoon.io",
"aud": "melmastoon-platform",
"sub": "usr_01HZ8XW...",
"tid": "ten_01HZ8X...",
"tids": [],
"did": "dev_01HZ...",
"amr": ["pwd", "totp"],
"acr": "mfa-strong",
"scope": "read:reservations write:reservations read:guests",
"roles": ["staff:front_desk"],
"userType": "staff",
"iat": 1745316000,
"exp": 1745316900,
"nbf": 1745315940,
"jti": "01HZJ4...XYZ"
}

5. List Endpoints (queries)

MethodPathReturns
GET/api/v1/users/meCurrent user snapshot.
GET/api/v1/users/me/sessionsActive sessions; cursor-paginated.
GET/api/v1/users/me/devicesDevices with binding status.
GET/api/v1/users/me/api-keysAPI keys (no raw values).
GET/api/v1/users/me/external-identitiesLinked IdPs.
GET/api/v1/admin/users?cursor=&limit=&filter=Admin search; tenant-scoped.

6. Health & Readiness

MethodPathNotes
GET/health/liveProcess up.
GET/health/readyDB, Redis, KMS reachable; outbox lag < 30s.
GET/health/startupMigrations applied; signing key cached.
GET/metricsPrometheus scrape (mTLS-only).

7. Internal Endpoints (not customer-facing)

Exposed only on internal listener (mTLS, SPIFFE peer cert):

MethodPathCaller
POST/internal/sync/identity/deltasync-service (Device deltas)
POST/internal/admin/jwks/rotateplatform key-rotator job
POST/internal/replay/eventops tool (DLQ replay)

8. Error Code Catalog (iam-service)

See Standards · ERROR_CODES. iam-specific:

CodeHTTPDescription
MELMASTOON.IAM.INVALID_CREDENTIALS401Wrong email or password.
MELMASTOON.IAM.MFA_REQUIRED200Login needs MFA challenge.
MELMASTOON.IAM.MFA_INVALID401Wrong MFA proof.
MELMASTOON.IAM.MFA_TICKET_EXPIRED401mfaTicket past TTL.
MELMASTOON.IAM.MFA_TICKET_USED401Ticket consumed.
MELMASTOON.IAM.ACCOUNT_LOCKED423User in locked state.
MELMASTOON.IAM.EMAIL_NOT_VERIFIED403Verification pending.
MELMASTOON.IAM.TENANT_DISABLED403Tenant deactivated by admin.
MELMASTOON.IAM.WEAK_PASSWORD422Fails policy.
MELMASTOON.IAM.BREACHED_CREDENTIAL422HIBP hit.
MELMASTOON.IAM.INVALID_EMAIL422Email not RFC 5322.
MELMASTOON.IAM.SSO_STATE_INVALID400OIDC state/nonce mismatch.
MELMASTOON.IAM.SSO_ASSERTION_INVALID400SAML signature/window invalid.
MELMASTOON.IAM.SSO_PROVIDER_DOWN503IdP circuit open.
MELMASTOON.IAM.JIT_DISALLOWED403Tenant policy disallows JIT.
MELMASTOON.IAM.MAGIC_LINK_USED400Nonce already redeemed.
MELMASTOON.IAM.MAGIC_LINK_EXPIRED400Past TTL.
MELMASTOON.IAM.MAGIC_LINK_INVALID400Token unknown / tampered.
MELMASTOON.IAM.REFRESH_INVALID401Token unknown.
MELMASTOON.IAM.REFRESH_EXPIRED401Past family TTL.
MELMASTOON.IAM.REFRESH_REUSE401Reuse detected; family revoked.
MELMASTOON.IAM.SESSION_REVOKED401Already revoked.
MELMASTOON.IAM.DEVICE_NOT_TRUSTED403Cannot bind / refresh offline.
MELMASTOON.IAM.DEVICE_PLATFORM_INELIGIBLE422Only electron-desktop may bind.
MELMASTOON.IAM.OFFLINE_TTL_EXCEEDED422Requested > policy max.
MELMASTOON.IAM.FRESH_AUTH_REQUIRED403Re-auth required for sensitive op.
MELMASTOON.IAM.TENANT_CA_UNAVAILABLE503KMS / tenant CA unreachable.
MELMASTOON.IAM.API_KEY_INVALID401Unknown / revoked / expired.
MELMASTOON.IAM.API_KEY_SCOPE_INSUFFICIENT403Scope missing.
MELMASTOON.IAM.RECOVERY_CODE_USED401Recovery code already used.