Skip to main content

Federation v2 — Browser Login

Federation v2 lets a project (Project A) accept login through another verified ItBuild project (Project B). The user lands on A's login shell, picks an external provider, completes authentication on B, and is bounced back to A with an authenticated session, ready to call B's APIs through A's AuthProxy. No password is ever shared between projects.

The mechanism reuses Federation v2 primitives (user_app, DNS TXT verification, outbound credentials) that already power partner payments and partner chats — the difference is that login uses an HTML / cookie redirect dance plus an HMAC-signed user payload instead of a webhook.

Roles

RoleWhere it runs
Login shellapg.pwa served by AuthProxy A.
Provider pickerInside the login shell, rendered from the providers catalogue.
Federated handshakeProject B (the provider), at its own /login?get_user_info=....
Return targetA static /federated-return.html page on AuthProxy A that finalises the local session.

End-to-end flow

Endpoints

All three endpoints live under auth/v1/federated/[action]. They are gated by the AuthProxy login method (EnabledLoginMethods): when it is disabled, providers returns an empty list and start / login return AuthMethodDisabled.

GET /auth/v1/federated/providers

Returns the active list of federated login providers. A provider is any user_app that has Approved + Federation flags, a non-empty Origin, an OutboundAppId > 0, and an outbound public key. Anonymous endpoint.

Response shape (ApiResponse<List<FederatedProviderOut>>):

{
"result": [
{
"appId": "17234567890001",
"name": "Example Partner",
"url": "https://partner.example.com",
"logoUrl": "https://partner.example.com/logo.svg"
}
],
"error": null,
"id": 0
}

appId is the partner's local user_app id (int64 as string). The picker renders one button per provider; new providers go live the moment the corresponding user_app row is approved — no PWA rebuild required.

GET /auth/v1/federated/start?providerAppId=...

Anonymous endpoint that initiates the handshake. providerAppId is the int64 appId from the catalogue. AuthProxy:

  1. Writes an encrypted fed_state cookie (HttpOnly, SameSite=Lax, 5-minute lifetime) carrying the chosen providerAppId, a returnUrl derived from the Referer (not from a query parameter), and a random nonce.
  2. Returns ApiResponse<FederatedStartOut> with the URL the browser must open next:
{ "result": { "redirectUrl": "https://partner.example.com/login?get_user_info=<outboundAppId>" }, "error": null, "id": 0 }

If the requested providerAppId is unknown or not an available provider, the response is AppNotFound (1108) inside the ApiResponse envelope — not an HTTP 403.

/federated-return.html (callback page on Project A)

After B authenticates the user it redirects the browser back to A's static callback page /federated-return.html, passing the signed payload (usr, check). The page reads those values and POSTs them to login below. There is no GET /auth/v1/federated/return endpoint.

POST /auth/v1/federated/login

Finalises the session. Body (FederatedLoginIn):

{
"usr": "<Base64Url(JSON FederatedUserInfo)>",
"check": "<Base64Url(HMAC-SHA256 hash)>"
}

usr decodes to FederatedUserInfo { id, firstName, lastName, utc, providerProjectId, verifiedEmail?, verifiedPhone? }. AuthProxy:

  1. Reads and validates the fed_state cookie (missing/expired → FederatedStateExpired).
  2. Resolves the provider from the cookie's providerAppId (not available → AccessDenied).
  3. Verifies check as HMACSHA256(SHA256(provider.OutboundPublicKey), usrBytes) in constant time (mismatch → InvalidSignature).
  4. Resolves the local CRM user by providerProjectId/id. If not found, it tries to auto-link a verified email/phone match (subject to FederatedContactMatchPolicy) or creates a new account (blocked with CreatingAccountNotAllowed when AccountCreationLoginMethods lacks AuthProxy).
  5. Mints the local sid session cookie, clears fed_state, and returns:
{ "result": { "returnUrl": "/" }, "error": null, "id": 0 }

returnUrl is the normalised value stored in fed_state (a same-origin relative path, defaulting to /).

returnUrl policy

returnUrl is server-controlled and never taken verbatim from the client:

  1. It is derived from the Referer at start time, not from a returnUrl query parameter.
  2. It is normalised to a same-origin relative path; anything not starting with a single / (including protocol-relative //host) collapses to /.
  3. Login screens are excluded so the user is never bounced back to /login.

This prevents open-redirect attacks: even if a partner is compromised, it cannot bounce users to a phishing page through Project A's federation entry point.

What still needs the partner-trust handshake

Browser login is one Federation v2 capability among several. Other pieces use the same underlying trust model but flow over private channels:

CapabilityHow it travels
Cross-project payment ordersPOST /payorders/v1/put_payment_order to partner, browser redirect to partner /pay, webhook back to source project.
Cross-project chat federationOne channel per partner app, message events delivered as ChatEventOut.
Per-user cross-project chatFuture phase: requires user-level federation auth, not project-level.
  • OAuth Providers — the local provider catalogue used as a fallback when no federated provider is enabled.
  • Webhooks — the partner-side delivery model for non-browser federation events.
  • Reverse Proxy: CORS pair — the AppAuthAndCORS pair that gates cross-app routes.