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
| Role | Where it runs |
|---|---|
| Login shell | apg.pwa served by AuthProxy A. |
| Provider picker | Inside the login shell, rendered from the providers catalogue. |
| Federated handshake | Project B (the provider), at its own /login?get_user_info=.... |
| Return target | A 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:
- Writes an encrypted
fed_statecookie (HttpOnly,SameSite=Lax, 5-minute lifetime) carrying the chosenproviderAppId, areturnUrlderived from theReferer(not from a query parameter), and a random nonce. - 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:
- Reads and validates the
fed_statecookie (missing/expired →FederatedStateExpired). - Resolves the provider from the cookie's
providerAppId(not available →AccessDenied). - Verifies
checkasHMACSHA256(SHA256(provider.OutboundPublicKey), usrBytes)in constant time (mismatch →InvalidSignature). - Resolves the local CRM user by
providerProjectId/id. If not found, it tries to auto-link a verified email/phone match (subject toFederatedContactMatchPolicy) or creates a new account (blocked withCreatingAccountNotAllowedwhenAccountCreationLoginMethodslacksAuthProxy). - Mints the local
sidsession cookie, clearsfed_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:
- It is derived from the
Refereratstarttime, not from areturnUrlquery parameter. - It is normalised to a same-origin relative path; anything not starting with a single
/(including protocol-relative//host) collapses to/. - 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:
| Capability | How it travels |
|---|---|
| Cross-project payment orders | POST /payorders/v1/put_payment_order to partner, browser redirect to partner /pay, webhook back to source project. |
| Cross-project chat federation | One channel per partner app, message events delivered as ChatEventOut. |
| Per-user cross-project chat | Future phase: requires user-level federation auth, not project-level. |
Related
- 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
AppAuthAndCORSpair that gates cross-app routes.