> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bbrands.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Client Integration

> Step-by-step guide to integrate a platform as an OAuth client, using a generic ERP as the reference implementation.

This guide walks an integrator through connecting a platform to the B Brands IdP.
A generic ERP is used as the reference (first-party) client, but the steps
generalize to any OIDC platform.

<Info>
  The IdP authenticates your users. **Your platform manages its own
  authorization** (roles/groups). The IdP never sends roles or permissions.
</Info>

## Integration summary

| Aspect                    | Decision                                           |
| ------------------------- | -------------------------------------------------- |
| Flow                      | Authorization Code + PKCE with OIDC                |
| Identity mapping          | ID token `sub` → your platform's external user id  |
| Provisioning              | Pre-sync via webhook (identity + status)           |
| Authorization in your app | **Your responsibility**                            |
| Consuming B Brands APIs   | With user → token exchange; without user → API key |

## 1. Register the OAuth client (B Brands side)

Create the client via the identity admin console or the management API. Register a
**distinct client per environment** (`<platform>-dev`, `<platform>-cert`,
`<platform>-prod`).

```http theme={null}
POST /api/v3/idp/clients
Content-Type: application/json

{
  "client_handle": "erp-prod",
  "display_name": "ERP (Production)",
  "platform_type": "erp",
  "trust_level": "first_party",
  "redirect_uris": ["https://your-erp.example.com/auth/callback"],
  "owner_team": "erp-team",
  "provisioning_webhook_url": "https://your-erp.example.com/internal/idp/provision",
  "access_token_ttl_seconds": 1800
}
```

The response returns the `client_secret` and the provisioning webhook secret
**once** — store them in your secret manager. Then enable pilot users:

```http theme={null}
POST /api/v3/idp/access
{ "user_id": "<uuid>", "client_handle": "erp-prod" }
```

## 2. Configure the client (your platform side)

Using your platform's OIDC support, point auto-configuration at discovery and
supply Client ID + Secret:

* Discovery URL: `https://auth.bbrands.io/.well-known/openid-configuration`
* Client ID: `<UUID returned at onboarding>`
* Client Secret: `<the one returned by the IdP>`

Per-environment base URL (issuer):

| Environment   | Base URL / issuer                       |
| ------------- | --------------------------------------- |
| Development   | `https://development.auth.bbrands.io`   |
| Certification | `https://certification.auth.bbrands.io` |
| Production    | `https://auth.bbrands.io`               |

For manual configuration (production values shown):

| Field                   | Value (production)                                                             |
| ----------------------- | ------------------------------------------------------------------------------ |
| Authorization URL       | `https://auth.bbrands.io/api/v3/oauth/authorize`                               |
| Token URL               | `https://auth.bbrands.io/api/v3/oauth/token`                                   |
| User Info URL           | `https://auth.bbrands.io/api/v3/oauth/userinfo`                                |
| JWKS URL                | `https://auth.bbrands.io/.well-known/jwks.json`                                |
| Scope                   | `openid email profile`                                                         |
| PKCE                    | Mandatory, `S256` only                                                         |
| Token signing algorithm | `ES256` (default) or `RS256` if your client registered `id_token_alg: "RS256"` |
| Issuer                  | `https://auth.bbrands.io`                                                      |

<Warning>
  Use a distinct client per environment. A non-production token must **never** be
  valid in production because the `iss` differs.
</Warning>

<Note>
  Validate the **access token** (issuer `auth.*`, `aud` = `bbrands-idp`,
  `https://bbrands.io/client_handle` = your handle) as the identity assertion.
  The `id_token` is signed with the same ES256 key but keeps the engine's
  internal issuer, so do not pin your library's issuer check to it — see
  [Endpoints](/oauth/endpoints#token-endpoint).
</Note>

<Note>
  **If your OIDC library only validates RS256 id\_tokens** (e.g. Odoo 18 with the
  OCA `auth_oidc` module): ask for your client to be registered with
  `id_token_alg: "RS256"`. Your `id_token` is then delivered signed as RS256
  (`kid` resolvable via the JWKS URL above, same claims), with no changes needed
  on your side beyond standard configuration. The access token remains ES256.
</Note>

### Validate the configuration

1. Visit your login page — the "Sign in with B Brands" button should appear.
2. Click it → you should be redirected to `…auth.bbrands.io/api/v3/oauth/authorize?…`.
3. After `authorize`, the browser is redirected to `…auth.bbrands.io/oauth/consent?…` (same origin) and, without a session, to the B Brands login first.
4. With an active B Brands session and an enabled user → auto-approve → back to your platform, logged in.

## 3. Identity mapping

| Concept             | B Brands                             | Your platform                  |
| ------------------- | ------------------------------------ | ------------------------------ |
| User id             | User UUID                            | Your internal user id          |
| Email               | `email`                              | Your login/email field         |
| External link       | —                                    | External id = ID token `sub`   |
| Business account    | `https://bbrands.io/profile.account` | Your customer/tenant record    |
| External account id | `https://bbrands.io/profile.odoo`    | Your own record id (e.g. Odoo) |

On each login, look the user up by (`provider`, external id = `sub`). Recommended
behavior when not found: **reject** with "request access from your admin" (users
must be pre-provisioned). The JIT alternative (auto-create) is only for flows
where pre-sync is impossible.

<Note>
  Both the access token and the UserInfo response include the
  `https://bbrands.io/profile` object: `account` is the B Brands account bound
  to the user through the `primary_user` role, `odoo` is that account's
  external system id (`null` until it is mapped by the migration API) and
  `user` mirrors `sub`. Use it to link the login to your own customer record
  without extra API calls.
</Note>

## 4. Provisioning webhook

Expose an endpoint that the IdP calls when access changes. Verify the HMAC
signature, then create/update or deactivate the user. **Assign your own groups —
the IdP does not send roles.**

```python theme={null}
# Framework-agnostic handler (pseudocode)
def provision(request):
    signature = request.headers.get("X-BBrands-Signature")
    body = request.raw_body
    secret = get_config("idp.webhook_secret")
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(signature, expected):
        return 401, {"error": "invalid_signature"}

    payload = json.loads(body)
    if payload["event_type"] == "user_provisioned":
        upsert_user(payload["user"])  # assign your default group
    elif payload["event_type"] in ("user_deprovisioned", "user_suspended"):
        deactivate_user(payload["user"]["sub"])
    return 200, {"ok": True}
```

### Webhook payload

Headers: `X-BBrands-Signature` (hex HMAC-SHA256 of the body),
`X-BBrands-Event-Id`, `X-BBrands-Delivery-Attempt`.

```json theme={null}
{
  "event_id": "uuid",
  "event_type": "user_provisioned",
  "client_handle": "erp-prod",
  "user": {
    "sub": "uuid-of-bbrands-user",
    "email": "user@example.com",
    "name": "Jane Doe",
    "given_name": "Jane",
    "family_name": "Doe"
  },
  "occurred_at": "2026-06-15T10:00:00Z"
}
```

Expected responses: `2xx` → `delivered`; `4xx` → `failed` (no retry, e.g. invalid
signature); `5xx`/timeout → exponential backoff retry, then `dead_letter` + admin
alert. Use `event_id` for idempotency against retries.

## 5. Consuming the B Brands APIs (with user)

When acting in the context of a logged-in user, exchange the OIDC token for an
internal B Brands JWT and use it as a Bearer. Details and error handling in
[Token exchange](/oauth/token-exchange).

```python theme={null}
r = requests.post(
    'https://api.bbrands.io/api/v3/oauth/exchange',
    json={'subject_token': id_token, 'subject_token_type': 'id_token'},
    timeout=10,
)
token = r.json()['data']['token']  # internal B Brands JWT, short TTL

requests.get(
    'https://api.bbrands.io/api/v3/account/account',
    headers={'Authorization': f'Bearer {token}'},
    timeout=30,
)
```

## 6. Machine-to-machine

A nightly sync cron has **no interactive user**, so it does not use OAuth or the
exchange. It uses an existing B Brands **API key**, provisioned with least
privilege.

```python theme={null}
requests.get(
    'https://api.bbrands.io/api/v3/account/account',
    headers={'X-API-Key': api_key},
    params={'page': 1, 'itemsPerPage': 200},
    timeout=30,
)
```

## 7. Edge cases

| Case                                        | Handling                                                                 |
| ------------------------------------------- | ------------------------------------------------------------------------ |
| User logged in client, disabled in B Brands | Next OAuth refresh fails → client logs them out (keep TTL low)           |
| Webhook arrives before first login          | User is created without password (forced to OIDC)                        |
| Email change                                | `sub` is stable, email is not; upsert updates the email                  |
| Reactivated user                            | `user_reactivated` event restores active state (groups kept by your app) |
| Duplicate webhook (retry)                   | Idempotent via `event_id`                                                |
| Exchange JWT expires mid-operation          | Re-exchange and retry once                                               |

## 8. Go-live checklist

<Steps>
  <Step title="Module installed">Integration module installed in production.</Step>
  <Step title="OIDC provider tested">Provider configured and verified in staging.</Step>
  <Step title="Secrets stored">Webhook secret and client secret stored encrypted.</Step>
  <Step title="API key provisioned">For the cron, with minimum permissions.</Step>
  <Step title="Exchange tested">OIDC → internal JWT tested for user-context flows.</Step>
  <Step title="Group policy defined">Your platform's group assignment policy is defined.</Step>
  <Step title="Pre-provision list validated">User list validated with People/HR.</Step>
  <Step title="Runbooks & alerts">"Cannot authenticate" runbook published; webhook/login alerts configured.</Step>
</Steps>
