Skip to content
Udibo
On this page
View Markdown

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:

MethodPathScope
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.

TypeScript
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.

Last verified 2026-10-24.