Skip to main content
The IdP is not a from-scratch authorization server. It is a thin B Brands facade over a managed OAuth 2.1 / OIDC engine, plus the identity, enablement, audit and lifecycle layer that the engine does not provide.

1. Component layers

The system is composed of five logical layers living in distinct physical pieces.
What the IdP does NOT do: it does not define application permissions, compute effective permissions, or inject roles/permissions into tokens. Fine-grained authorization lives in each client; the B Brands APIs authorize with their existing internal RBAC.

2. How the code is organized

The IdP logic is a cross-cutting module, separate from the standard per-resource pattern of the rest of the API.

3. Key architectural decisions (ADR-lite)

We use a managed OAuth 2.1 engine as the authorization server: it is already our identity base, it implements the full protocol (PKCE, OIDC, JWKS, refresh rotation), and custom access-token hooks let us rewrite iss/aud and enforce enablement. Trade-off: we depend on the engine’s capabilities; the lack of native custom scopes is irrelevant because we do not inject permissions into tokens.
Every OAuth, OIDC, discovery and JWKS endpoint is served under a B Brands origin. Clients never see the engine’s URLs, not even for discovery or JWKS. This allows swapping the identity backend without touching client config, and enables custom logic (rate limiting, audit, transformation) at a single origin.
The iss claim of issued access tokens is the environment’s auth.* URL (e.g. https://development.auth.bbrands.io in development), rewritten by the access-token hook. A non-production token is never valid in production because clients validate iss against their own environment. This revises the original decision that named api.* as the issuer.
The auth.* origin serves both the interactive surfaces (login, consent) and every protocol endpoint (discovery, JWKS, authorize, token, userinfo, revoke) — the latter through transparent rewrites to the api.* backend. This is the accounts.google.com shape: one stable identity origin for clients, while the implementation host behind it stays replaceable. Boot-time consistency checks and a JWKS availability monitor guard the contract.
OAuth JWTs are signed with ES256 (ECC P-256); the public key is exposed via JWKS so clients can validate without sharing secrets. The internal B Brands JWT keeps using HS256 with a server-side secret. The two formats coexist and never mix.
Clients whose OIDC library only validates RS256 (e.g. Odoo 18 with the OCA auth_oidc module) register id_token_alg: "RS256" (the OIDC id_token_signed_response_alg). For those clients only, the IdP verifies the genuine ES256 id_token and re-signs it with its own RSA key, preserving every claim. The JWKS publishes the RSA public key alongside the EC key, discovery advertises both algorithms, and the access token is never re-signed. Any re-signing failure falls back to the ES256 original.
OAuth tokens carry only identity claims (sub, email, profile). There is no permission or role claim. The access-token hook only rewrites iss/aud and checks that the user is enabled for the client.
The client catalogue, user × platform enablement and audit live in B Brands governance tables managed from the identity admin console. The engine only keeps what it needs to mint tokens.
Enabling access fires a webhook that creates/updates the user in the target platform before their first login. The payload carries identity and status, never roles. Deprovisioning is symmetric on suspend/revoke.
Short-lived OAuth access tokens + a short-lived exchange JWT + a blacklist checked on every validation. The exchange JWT must never inherit the long TTL of the traditional login.
A client that needs to call the B Brands APIs on behalf of the user exchanges its OIDC token for an internal B Brands JWT and uses it as a Bearer. The existing internal authorizer validates it. This reuses the internal RBAC and the Resource Server with no changes. See Token exchange.

4. The central piece: the access-token hook

The hook does not inject permissions. Its job is to rewrite iss/aud, validate that the user is enabled for the client and attach the business-account dimension (https://bbrands.io/profile). If the user is not enabled, no token is issued (failsafe deny).
The identity-resolution logic is described in Data model. Failure mode: if the hook fails, no token is issued — security wins.

5. Per-client isolation

Although every client shares one engine instance, isolation is guaranteed by:
  1. The https://bbrands.io/client_handle claim identifying the requesting client in each OAuth access token (aud is the fixed bbrands-idp audience).
  2. client_handle validation by each client (a token for crm-prod is not valid in an ERP client).
  3. Enablement via the user × platform access record, checked on issuance, exchange and refresh.
  4. The audit log filtered by client_handle.

6. Deployment model