# Social login Let your users sign in to your application with an account they already have. You configure a connector per provider in your dashboard; the provider's button appears on your tenant's hosted sign-in page; we run the whole flow and hand your application a signed-in user, exactly as if they had typed a password. Built-in connectors exist for **Google**, **GitHub**, **Discord**, and **Apple**. Any other provider that speaks OpenID Connect — an enterprise IdP, another platform, your own — is a **generic OIDC** connector: give us its issuer URL and we take the rest from the provider's own discovery document. ## How an account is matched - **A linked identity is keyed by the provider's stable subject identifier, never by email.** An email address can change hands; the provider-and-subject pair cannot. Someone who changes their email at the provider keeps their account with you. - **A verified email collision links automatically.** When a provider asserts a **verified** email that equals the **verified** email of an existing account, the identity is attached to that account and the sign-in proceeds. If either side is unverified — the provider did not vouch for the address, or the account never confirmed it — the sign-in is refused instead of guessing, because an unverified match is how account takeover by email squatting works. - **An unknown identity provisions a new account**, subject to your tenant's sign-up mode. A closed tenant refuses self-serve provisioning the same way it refuses the sign-up form. A waitlist-only tenant **captures** the provider's email onto the waitlist instead of refusing — sending the same confirmation email and landing on the same you're-on-the-list page as the join form, idempotently — but only when the provider **asserts that email as verified**. Without a verified address there is nothing safe to put on a list, so a waitlist-only tenant refuses exactly as the form does. And a waitlist invitation can be redeemed through a provider button when the provider asserts a verified email equal to the invited address. - **Signed-in users connect and disconnect providers themselves** on the hosted `/security` page. Connecting one asks for the account's current password, or a two-factor code when the account has no password, so a session cookie copied to another computer cannot add a way in. An account with neither confirms by signing in again with a provider it already uses ("Confirm by signing in with Google"), then comes back to finish. A generic OIDC connector is asked to make the person sign in afresh (`prompt=login`, `max_age=0`) and must report `auth_time`: a re-sign-in without one, or with one from before the confirmation began, is refused, so an OIDC provider that never sends `auth_time` cannot be used to confirm. A refused or cancelled re-sign-in comes back to the confirmation with an error. The built-in providers document no such request, so for them the re-sign-in proves the person controls that provider account in this browser, which the provider may confirm from its own session without asking for anything. An account with no password, no two-factor and no connected provider (one that signs in only by emailed code or link) chooses **Confirm by email** instead: the account's own address is sent a sign-in code or link, and signing in with it in the same browser brings the person back to finish. A sign-in as a different account does not count. The provider, or the emailed sign-in, must answer within 10 minutes of the confirmation, in the same signed-in session. Connecting or disconnecting a sign-in method emails the account owner with a link to `/security`, where it can be removed. Resetting a password signs out every session but keeps connected sign-in methods. The last remaining way to sign in cannot be disconnected — another usable method enabled by the tenant must remain. That can be a native or supported imported password while password sign-in is enabled, another enabled provider while social sign-in is enabled, or an email address with email-code or email-link sign-in enabled. ## Linked accounts in your application Send the person's access token to `GET /api/account/linked-accounts` on your tenant's host. The response contains `identities`, `hasPassword` and `connectable`. Each identity includes its opaque `id`, provider key, provider `displayName`, email when available, and `createdAt`. Raw provider subjects, tokens and connector secrets are omitted. Linked identities remain listed when their provider is disabled. `connectable` lists the providers this person could connect next — `key` and `displayName` only, exactly the set your hosted sign-in page already shows as buttons, and empty while your tenant's social sign-in method is off — so your application can render "Connect Google" and start the flow below. `DELETE /api/account/linked-accounts/{identityId}` disconnects that person's identity and returns 204. It shares the hosted page's last-method guard, audit and owner notification. The last usable sign-in method returns 409 with `reason: "last_method"`; an unknown, malformed or another person's ID returns the same 404. A disabled provider or a method switched off by the tenant does not count as a remaining usable method. The existing notification is attempted for an owner with a verified email; delivery failure does not undo the unlink. These caller-only operations require no management scope or dashboard seat. Machine credentials are refused, and `www.udibo.com` answers 404. A mutation accepts no body or an empty object; no subject or tenant selector is accepted. ## Connecting an account from your application Send the signed-in person to your tenant's host, as a top-level navigation: ``` https://{your-tenant-host}/auth/social/{key}?intent=link&return_to={url} ``` `{key}` is a `connectable` entry from the response above. That page never starts the provider flow by itself — a link that starts on a plain link is the cross-site forced-linking shape RFC 6819 §4.4.1.13 describes — so it lands on a confirmation on the hosted `/security` page: "Connect Google to your {tenant} account?", the person's current password (or a two-factor code, or a button to confirm by signing in again with a provider they already use, or by email for an account that signs in only by emailed code or link), and one button, which starts the flow. A wrong password keeps them on the confirmation to try again; your application hears nothing until the flow ends. A person who is not signed in on your tenant's host goes through its sign-in page first and returns to the confirmation with `return_to` intact. The link binds to the account that is signed in on the host, never to anything the request names: there is no `userId`, no email, and no way to choose the account. The matching rules above are unchanged — an identity that already belongs to someone else is refused, and nothing moves. `return_to` is honoured only when it is a path on your tenant's host or an absolute URL on the origin of a redirect URI registered on one of your live applications — the same allowlist the hosted organization pages use. It is checked when the person arrives, when they confirm, and again after the provider answers, so a registration you delete mid-flow is dropped rather than followed. When it is honoured, the person comes back to it with `link` appended: - `link=linked` — the account is connected, or already was, to this person. - `link=already-linked` — that external identity belongs to someone else; nothing changed. - `link=link-failed` — the flow could not complete; nothing changed. - `link=cancelled` — the person declined the confirmation. Without an honoured `return_to`, the person stays on the hosted `/security` page, which reports the same outcomes as it always has. The owner notice sent when a sign-in method is connected is unchanged and names no application. ## Setting up a connector In the dashboard: **Providers** → **Add provider**. Pick the provider type, then paste the client ID and client secret from the provider's own console. Apple's inputs are different: its client ID is the **Services ID** configured for Sign in with Apple on the web (not the App ID), it has no client secret — in its place you paste the contents of the **`.p8` private key** Apple lets you download once — and it also asks for your **Team ID** and **Key ID**. For built-in providers, the **Add provider** form shows the exact callback URL to register with the provider: ``` https://{your-tenant-host}/auth/social/{provider}/callback ``` For generic OIDC, the **Add provider** form shows a placeholder because the URL segment is the connector's generated row ID. Save the connector first, then copy the exact **Redirect URI** from its edit page and register it with the provider before testing sign-in. Providers match that URL exactly, so every origin your sign-in page is served on needs its own registration in the provider's console. A tenant may have several OIDC connectors; built-in types are limited to one connector each. Client secrets are sealed with authenticated encryption at rest, and reads — dashboard and API alike — report only whether a secret is stored, never its value. Each connector has its own **enabled** toggle, so you can stage one without offering it: a disabled connector keeps its configuration and its linked identities, and its button simply does not render. ## Generic OIDC A generic OIDC connector needs the provider's **issuer URL**. Configuration is read from the provider's own discovery document (`{issuer}/.well-known/openid-configuration`), so there are no endpoint URLs to transcribe. By default the flow requests the `openid`, `email`, and `profile` scopes; set the connector's scopes to override that. The issuer must be a public `https` URL. We refuse issuers that resolve to private, loopback, or otherwise internal addresses — at save time and again on every outbound call — because your issuer URL is a URL our servers fetch. ## Turning social sign-in on and off Two switches have to agree before a button renders on the sign-in page: the tenant-wide **social** sign-in method (dashboard: **Security** → **Sign-in methods**), and the connector's own **enabled** toggle. The tenant-wide method is also a management API setting, listed for reference: | Method | Path | Scope | | ------- | ---------------------------- | ------------------------ | | `PATCH` | `/api/identity/tenants/{id}` | `identity:tenants:write` | The `authMethods` object you send is **merged into the current setting** — a switch you omit keeps whatever it is set to, so a request that changes the social switch can name social alone and leave the rest of your policy untouched (send `false` to turn it off): _Reference only — dashboard today._ ```ts const tenantId = process.env.UDIBO_TENANT_ID; const response = await fetch( `https://www.udibo.com/api/identity/tenants/${tenantId}`, { method: "PATCH", headers: { authorization: `Bearer ${process.env.UDIBO_API_TOKEN}`, "content-type": "application/json", }, body: JSON.stringify({ authMethods: { social: true } }), }, ); if (!response.ok) throw new Error(`update failed: ${response.status}`); ``` `password`, `emailCode` and `emailLink` keep their current values here — the request says nothing about them. Send all four if you would rather state the whole policy; the result is the same when the values agree. A request that would leave every method off is refused, checked against the merged result. `{id}` is your tenant's id — the same one in your dashboard URL — and the token must hold administrative authority over that tenant. The management API also exposes connector configuration at `/api/identity/{tenantId}/identity-providers`: list and read with `identity:providers:read`, and create, update, or delete with `identity:providers:write`. Reads return `hasClientSecret` rather than a secret value. Updating a connector keeps its type and OIDC issuer fixed; configure a new connector to change either. These operations have the same dashboard-only credential boundary as the tenant setting above. **Dashboard only today.** These management API operations are listed for reference: they accept only the dashboard's own session or a token from Udibo's own sign-in, which you cannot get outside the dashboard. A token your tenant issues, including a client-credentials token, is refused with `401`; see [the management API](https://www.udibo.com/docs/identity/apis#the-management-api). _Last verified 2026-10-24._