# Native SSO Data Contract

Version: `2.9.8`

This document is the canonical payload reference for `@ollaid/native-sso`.
It describes the exact data shapes expected by the frontend package and by a
backend integration.

## Scope

Three different layers exist in the flow:

1. SaaS backend routes under `/api/sso/*`
2. IAM native routes under `/sso/native/*`
3. IAM decrypt route used by the SaaS backend: `/sso/auth/decrypt`

The frontend package calls SaaS first for config and final exchange, then IAM
for native login/registration/password steps.

## Key rule

Do not treat these values as user data:

- `encrypted_credentials`
- `native_token`
- `process_token`
- `callback_token`
- `refresh_token`

They are opaque transport tokens. The frontend must only pass them through.

## Data model summary

### Public identifiers

- `app_key`: plain application identifier exposed by SaaS config
- `reference`: canonical IAM user reference
- `alias_reference`: alias reference linked to the connected account
- `iam_token`: public access-token string whose hash is stored in `app_access_tokens.token` and returned to SaaS on session-minting flows

### User snapshot

The frontend accepts these user fields:

- `reference`
- `alias_reference`
- `iam_token`
- `name`
- `email`
- `phone`
- `ccphone`
- `address`
- `town`
- `country`
- `image_url`
- `image`
- `auth_2fa`

### Server-to-server profile sync responses

The profile sync routes do not mint a new session token. They only return the
identity snapshot and the alias when it is relevant.

Typical success payloads:

- `POST /api/sso/refresh-user-info`
  - `reference`
  - `alias_reference`
  - `user_infos`
- `POST /api/sso/link-email`
  - `reference`
  - `user_infos`
- `POST /api/sso/link-phone`
  - `reference`
  - `user_infos`
- `POST /api/sso/update-avatar`
  - `reference`
  - `alias_reference`
  - `user_infos`
- `POST /api/sso/reset-avatar`
  - `reference`
  - `alias_reference`
  - `user_infos`

Do not expect `iam_token`, `expires_at`, or `refresh_expires_at` from these
sync endpoints. Those fields belong to session-minting flows such as
`/api/sso/exchange`, `/api/sso/refresh`, and `/api/sso/check-token`.

## 1) SaaS config contract

### `GET /api/sso/config`

This route is served by the SaaS backend.
It returns the IAM credentials blob used by the frontend.

#### Response

```json
{
  "success": true,
  "app_key": "oiam_ak_...",
  "encrypted_credentials": "opaque-base64-blob",
  "iam_api_url": "https://identityam.ollaid.com/api",
  "credentials_ttl": 300,
  "debug": false,
  "bypass": false
}
```

#### Field meaning

- `success`: must be `true`
- `app_key`: plain text app identifier, not secret
- `encrypted_credentials`: opaque blob passed later to IAM `/sso/native/encrypt`
- `iam_api_url`: base URL for the IAM API, usually `https://identityam.ollaid.com/api`
- `credentials_ttl`: lifetime in seconds for the config blob, default `300`
- `debug`: optional backend debug flag
- `bypass`: optional `needs_access` auto-accept flag

### `encrypted_credentials` format

The backend reference currently builds it as:

```text
base64( IV_16_BYTES + "::" + base64(AES-256-CBC(ciphertext)) )
```

Where:

- `IV_16_BYTES` is a random 16-byte IV
- AES key = `sha256(secret_key, raw=true)`
- payload = JSON string containing at least:
  - `secret_key`
  - `ts`

Example conceptual payload before encryption:

```json
{
  "secret_key": "IAM_SECRET_KEY_VALUE",
  "ts": 1718149130
}
```

Important:

- the frontend never decrypts this blob
- the IAM backend must be able to decrypt and verify it
- `credentials_ttl` is enforced on the IAM side

## 2) Native IAM encrypt contract

### `POST /sso/native/encrypt`

The frontend sends credentials to IAM after loading SaaS config.

#### Request

```json
{
  "type": "login_email",
  "app_key": "oiam_ak_...",
  "encrypted_credentials": "opaque-base64-blob",
  "email": "user@example.com"
}
```

#### Common fields

- `type`: one of `login_email`, `login_phone`, `login_access_otp`, `register`, `recovery_password`
- `app_key`: plain SaaS app key
- `encrypted_credentials`: opaque config blob from SaaS

#### Type-specific fields

- `login_email`
  - `email`
- `login_phone`
  - `ccphone`
  - `phone`
- `login_access_otp`
  - `email` or `ccphone` + `phone`, depending on the chosen login path
- `register`
  - `name`
  - `email` or `ccphone` + `phone`
  - `town`
  - `country`
  - `registration_type`
- `recovery_password`
  - `email` or `ccphone` + `phone`

#### Response

```json
{
  "success": true,
  "native_token": "nt_...",
  "expires_in": 300
}
```

`native_token` is then sent to `POST /sso/native/init`.

## 3) Native IAM init contract

### `POST /sso/native/init`

#### Request

```json
{
  "native_token": "nt_..."
}
```

#### Response shape

The response is flow-dependent. Minimum stable fields:

```json
{
  "success": true,
  "status": "pending_password",
  "process_token": "pt_...",
  "user": {
    "name": "User Name",
    "image": "storage/users/avatar.jpg"
  },
  "has_password": true
}
```

Possible `status` values:

- `pending_password`
- `pending_otp`
- `pending_registration`
- `pending_2fa`
- `needs_access`
- `completed`
- `expired`
- `failed`

Optional fields often returned by init:

- `application`
- `prompt`
- `otp_sent_to`
- `otp_expires_in`
- `otp_code_dev`
- `otp_method`
- `disabled_type`
- `alternative_method`
- `alternative_email`
- `alternative_phone`
- `conflict`

### Practical meaning

- `pending_password`: user must enter a password
- `pending_otp`: user must enter an OTP
- `needs_access`: credentials are valid, but SaaS access is not granted yet
- `completed`: some flows can complete immediately and return a callback token later in the flow

## 4) Native IAM validate contract

### `POST /sso/native/validate`

#### Request

Password flow:

```json
{
  "process_token": "pt_...",
  "password": "plain-password"
}
```

OTP flow:

```json
{
  "process_token": "pt_...",
  "otp_code": "123456"
}
```

#### Success response variants

#### a) Final success

```json
{
  "success": true,
  "callback_token": "ct_...",
  "expires_at": "2026-06-11T23:48:54+00:00"
}
```

This is the normal handoff to the SaaS backend.

#### b) 2FA required

```json
{
  "success": true,
  "requires_2fa": true,
  "status": "pending_2fa",
  "process_token": "pt_..."
}
```

#### c) Access still missing

```json
{
  "success": true,
  "needs_access": true,
  "status": "needs_access",
  "process_token": "pt_...",
  "application": {
    "id": 12,
    "name": "Sofisticard",
    "logo": "https://..."
  },
  "user": {
    "id": 55,
    "name": "Alioune Diop",
    "email": "al***@domain.com"
  },
  "prompt": {
    "title": "Autoriser l'acces",
    "message": "Sofisticard souhaite acceder a votre compte",
    "actions": ["grant_access", "cancel"]
  }
}
```

#### Error response variants

Common IAM validation errors:

- `invalid_password`
- `invalid_otp`
- `invalid_totp`
- `otp_expired`
- `max_attempts`
- `max_attempts_exceeded`
- `session_expired`
- `invalid_token`
- `requires_2fa`
- `user_not_found`
- `alias_not_found`

Example:

```json
{
  "success": false,
  "error_type": "invalid_password",
  "message": "Mot de passe incorrect",
  "remaining_attempts": 2
}
```

Important:

- if the password is wrong, the flow must fail here
- `callback_token` must not be emitted on wrong password
- if the password is correct, the next step is SaaS `/api/sso/exchange`

## 5) Native IAM grant-access contract

### `POST /sso/native/grant-access`

Used when `needs_access` is shown and the user accepts.

#### Request

```json
{
  "process_token": "pt_..."
}
```

#### Success response

```json
{
  "success": true,
  "callback_token": "ct_...",
  "expires_at": "2026-06-11T23:48:54+00:00"
}
```

#### Errors

- `invalid_token`
- `invalid_status`
- `user_not_found`
- `alias_not_found`
- `server_error`

## 6) Native IAM resend-otp contract

### `POST /sso/native/resend-otp`

#### Request

```json
{
  "process_token": "pt_..."
}
```

#### Success response

```json
{
  "success": true,
  "otp_code": "482913",
  "receive_mode": "email",
  "sent": true,
  "cooldown_remaining": 60
}
```

#### Errors

- `cooldown_active`
- `session_expired`
- `invalid_token`
- `invalid_type`

## 7) SaaS exchange contract

### `POST /api/sso/exchange`

This is the route that often fails when the login password was correct but
the SaaS backend is unreachable or misconfigured.

The frontend calls this route only after it gets a valid `callback_token`
from IAM.

The `callback_token` must decrypt on the SaaS side into the same flat session
payload described below. If the decrypted payload misses `reference` or
`iam_token`, the SaaS must reject it as `incomplete_data`.

#### Request

```json
{
  "callback_token": "ct_..."
}
```

#### Required behavior

- route must be public
- route must be resolved on the SaaS backend, not on IAM
- route must be able to call IAM `/sso/auth/decrypt`
- route must return JSON

#### IAM decrypt call made by SaaS

The SaaS backend sends:

```json
{
  "token": "ct_..."
}
```

to:

```text
POST /sso/auth/decrypt
```

with headers:

- `Content-Type: application/json`
- `Accept: application/json`
- `X-IAM-App-Key`
- `X-IAM-Secret-Key`

#### Expected IAM decrypt response

The SaaS backend expects:

```json
{
  "reference": "iam_user_123",
  "alias_reference": "alias_123",
  "iam_token": "iam_tok_...",
  "user_infos": {
    "name": "User Name",
    "email": "user@example.com",
    "ccphone": "+221",
    "phone": "771234567",
    "image_url": "https://..."
  },
  "expires_at": "2026-07-11T23:48:54+00:00",
  "refresh_expires_at": "2026-08-10T23:48:54+00:00"
}
```

Important:

- this response is **flat**
- the SaaS must read these fields at the top level
- do **not** require a legacy wrapper like `success: true` + `data: {...}`
- if an old SaaS still reads `success.data`, it must be updated to the flat contract above

#### SaaS success response

```json
{
  "reference": "iam_user_123",
  "alias_reference": "alias_123",
  "iam_token": "sanctum-token-string",
  "user_infos": {
    "name": "User Name",
    "email": "user@example.com",
    "phone": "+221771234567",
    "image_url": "https://..."
  },
  "expires_at": "2026-07-11T23:48:54+00:00",
  "refresh_expires_at": "2026-08-10T23:48:54+00:00"
}
```

Canonical minimal SaaS persistence:

- `user_infos`
- `reference`
- `alias_reference`
- `iam_token`
- `expires_at`
- `refresh_expires_at`

#### SaaS error response variants

- `422 Unprocessable Entity`
  - the SaaS backend received the request, parsed `callback_token`, and rejected it as invalid business input
  - use for:
    - `callback_token` invalid
    - `callback_token` expired
    - user not authorized for this SaaS
    - IAM decrypt response missing required fields
  - response body must be JSON and must include:
    - `success: false`
    - `error_type`
    - `message`
- `503 Service Unavailable`
  - use when the SaaS backend cannot reach IAM or its own dependency chain is down
  - response body must be JSON and must include:
    - `success: false`
    - `error_type: "connection_error"`
    - `message`
- `500 Internal Server Error`
  - use for unexpected SaaS side failures after the request was actually received
  - response body must be JSON and must include:
    - `success: false`
    - `error_type: "server_error"`
    - `message`

Recommended canonical SaaS error payloads:

```json
{
  "success": false,
  "error_type": "invalid_token",
  "message": "Callback token invalide ou expiré"
}
```

```json
{
  "success": false,
  "error_type": "access_denied",
  "message": "Utilisateur non autorisé sur cette application"
}
```

```json
{
  "success": false,
  "error_type": "connection_error",
  "message": "Impossible de contacter le serveur d'authentification"
}
```

```json
{
  "success": false,
  "error_type": "server_error",
  "message": "Erreur interne du serveur"
}
```

#### CORS / preflight requirements

`/api/sso/exchange` must answer preflight requests cleanly.

Required behavior:

- `OPTIONS /api/sso/exchange` returns `200` or `204`
- response must include the SaaS CORS headers
- `Access-Control-Allow-Origin` must allow the frontend origin
- `Access-Control-Allow-Methods` must include `POST` and `OPTIONS`
- `Access-Control-Allow-Headers` must include at least:
  - `Content-Type`
  - `Accept`
  - `Authorization`
  - `X-Device-Id`
  - `X-Session-UUID`
  - `X-Device-Label`
  - `X-Device-Browser`
  - `X-Device-Name`
  - `X-Device-Model`
  - `X-Device-Manufacturer`
  - `X-Device-Operating-System`
  - `X-Device-Os-Version`
  - `X-Device-Platform`
  - `X-Device-Language`
  - `X-Device-WebView-Version`
  - `X-Device-Is-Native`
  - `X-IAM-Config-Prefix`

If preflight fails, the browser will often surface a `network error` even if
the SaaS handler itself would have returned a useful JSON error.

### Important troubleshooting note

If the UI shows:

```text
Impossible de contacter le serveur
```

after the password was accepted, this is not a password problem.
It means the flow reached the SaaS `exchange` step and failed there.

The most common causes are:

- SaaS `/api/sso/exchange` route is missing
- SaaS `/api/sso/exchange` route is protected by the wrong middleware
- SaaS cannot reach IAM `/sso/auth/decrypt`
- IAM decrypt route path is still set to the obsolete `/api/iam/auth/decrypt`
- bad IAM base URL in SaaS config
- timeout or CORS/proxy issue on the SaaS side
- SaaS returns non-JSON or no response body on failure

## 8) SaaS refresh contract

### `POST /api/sso/refresh`

#### Request

```json
{
  "refresh_token": "rt_..."
}
```

#### Success response

```json
{
  "reference": "iam_user_123",
  "alias_reference": "alias_123",
  "iam_token": "iam_tok_...",
  "user_infos": {
    "name": "User Name",
    "email": "user@example.com",
    "phone": "+221771234567",
    "image_url": "https://..."
  },
  "expires_at": "2026-07-11T23:48:54+00:00",
  "refresh_expires_at": "2026-08-10T23:48:54+00:00"
}
```

#### Errors

- `invalid_refresh`
- `session_expired_idle`
- `validation_error`

## 9) SaaS check-token contract

### `POST /api/sso/check-token`

#### Success response

```json
{
  "reference": "iam_user_123",
  "alias_reference": "alias_123",
  "iam_token": "iam_tok_...",
  "user_infos": {
    "name": "User Name",
    "email": "user@example.com",
    "phone": "+221771234567",
    "image_url": "https://..."
  },
  "expires_at": "2026-07-11T23:48:54+00:00",
  "refresh_expires_at": "2026-08-10T23:48:54+00:00"
}
```

#### Failure behavior

- unauthenticated requests should return `401`
- idle-expired sessions should return `401` with `session_expired_idle`

## 10) Field normalization rules

The frontend normalizes some fields:

- `image_url` may come from `image_url`, `avatar`, or `image`
- `phone` may be returned as concatenated `ccphone + phone`
- `user_infos.email` may be masked in UI but should stay consistent in payloads
- `reference` is the public contract name
- `iam_token` is the access-token string from `app_access_tokens`, not the row id

## 11) Canonical SaaS persistence contract

For the SaaS user table, the required fields are:

- `user_infos`
- `reference`
- `alias_reference`
- `iam_token`
- `expires_at`
- `refresh_expires_at`

Optional operational fields:

- `expires_at`
- `refresh_expires_at`
- `last_active_at`

## 12) What the backend must not do

- do not send plaintext `secret_key` to the frontend
- do not exchange `callback_token` on IAM from the frontend
- do not rename tokens
- do not convert opaque tokens into JWTs unless the package is updated for it
- do not move `/api/sso/exchange` under auth middleware
- do not return HTML errors for JSON routes
- do not expose the numeric `app_access_tokens.id` as the public token value

## 13) Minimal validation checklist

- `GET /api/sso/config` returns `app_key` and `encrypted_credentials`
- `POST /sso/native/encrypt` returns `native_token`
- `POST /sso/native/init` returns `process_token` for password or OTP flows
- `POST /sso/native/validate` returns `callback_token` when credentials are valid
- `POST /api/sso/exchange` returns `reference`, `alias_reference`, `iam_token`, `user_infos`, `expires_at`, `refresh_expires_at`
- wrong password returns `invalid_password`
- network or IAM reachability problems are surfaced as `connection_error`
- `needs_access` path works when access is missing
