Social login and account linking

Configure providers

Gate administrators with organization/project credential:manage permission configure GitHub, Google, and Apple in Settings → Social login. Organization credentials are optional. A project can inherit, override with a complete client ID and secret, or disable each provider. Settings apply across environments. Secrets are encrypted and cannot be read back. Leaving the secret blank keeps it only when the client ID is unchanged. Changing a client ID requires a new secret.

Create an OAuth application in GitHub or Google’s developer console, or an Apple Services ID and Sign in with Apple key. Register the exact callback URL shown by Gate:

Text
https://gate.example.com/api/auth/gate-social/callback/github
https://gate.example.com/api/auth/gate-social/callback/google
https://gate.example.com/api/auth/gate-social/callback/apple

Apple’s client secret is an ES256 JWT signed with the .p8 key; replace it before expiry (at most six months). An optional app bundle identifier can be saved alongside the Services ID. Native clients use Gate’s browser OAuth flow; this API does not accept native upstream ID tokens.

Management APIs are GET /api/v1/organizations/{organizationId}/social-providers and the equivalent project path. PUT .../{provider} accepts mode, clientId, clientSecret, and optional Apple appBundleIdentifier. Organization modes are override or disabled; project modes additionally include inherit. DELETE .../{provider} removes settings; a project resumes inheritance.

Show available buttons

javascript
const response = await fetch(
  `${gate}/api/v1/oidc-clients/${encodeURIComponent(clientId)}/login-options`,
);
if (!response.ok) throw new Error("Application unavailable");
const { data } = await response.json();
// data: { providers: ["github", "apple"], emailPassword: true }

Render one button per provider. Each button starts your ordinary OIDC authorization-code/PKCE flow with provider_hint=github, google, or apple; Gate highlights that provider. The hint cannot enable a disabled provider. Always generate and verify your normal OIDC state and nonce. The hosted login page uses the same provider selection. Direct Gate login uses email/password.

Authorize account APIs

Request account:read to list accounts, or account:manage to list, link, and unlink. Add the scopes to the client registration first, then request them with user consent and resource=https://gate.example.com/api. For example, the authorization request's scope is openid profile email account:manage. Use the resulting access token, not the ID token.

These endpoints require a Gate JWT user access token for the Gate API audience. Product-audience tokens, service API keys, machine tokens, and browser-session-only requests are rejected. User IDs and project context come from the token and registered client; never send a target user ID. A web client origin must also be permitted by Gate’s existing CORS origin configuration.

javascript
const headers = { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" };
const listed = await fetch(`${gate}/api/v1/me/social-accounts`, { headers });
const { data: accounts } = await listed.json();

const state = crypto.randomUUID(); // Save and compare when the browser returns.
const response = await fetch(`${gate}/api/v1/me/social-accounts/link`, {
  method: "POST",
  headers,
  body: JSON.stringify({ provider: "google", redirectUri, state }),
});
if (!response.ok) throw new Error("Could not start linking");
const { data } = await response.json();
window.location.assign(data.authorizationUrl);

redirectUri must exactly match a registered client redirect URI. The returned URL contains a single-use ticket, expires in ten minutes, and opens a Gate confirmation page followed by the provider. It never contains the access token. The authenticated initiating user remains the target even if the browser has another Gate session. A different provider email is allowed for explicit linking, but an identity already owned by someone else cannot be transferred.

For iOS/Android, make the same bearer-authenticated request, open authorizationUrl in the system browser/authentication session, and handle the registered app/universal-link redirect. Keep the access token in secure app storage; do not put it in the browser URL or webview message history.

The redirect contains gate_link=success, failed, or cancelled, plus your state. Verify state and refresh the account list; no provider credentials or tokens are returned. Invalid/expired requests may show a Gate error page instead, so allow users to restart the flow from the client.

Account responses contain an opaque id, provider subject, available name/email/avatar, verification status, project provenance, usable, canUnlink, and an explanation of unlink impact. Legacy bindings without project provenance remain preserved but are not exposed to arbitrary clients.

Ask the user to confirm that unlinking removes the shared binding across every project using it:

javascript
const response = await fetch(
  `${gate}/api/v1/me/social-accounts/${encodeURIComponent(account.id)}`,
  {
    method: "DELETE",
    headers: { Authorization: `Bearer ${accessToken}` },
  },
);
if (response.status === 409) throw new Error("Keep another usable login method before unlinking");
if (response.status !== 204) throw new Error("Could not unlink account");

Use the account's opaque id, not its provider name or external subject. A client can unlink its project’s historical bindings even if their provider is now disabled. Gate rejects unrelated accounts and prevents removing the last usable login method, including simultaneous requests. Unlinking deletes the binding and any stored tokens, not the Gate user or existing Gate sessions. It does not revoke application authorization at GitHub/Google/Apple; users can do that with the provider.

Automatic sign-in linking requires matching verified provider and local emails. Unverified local accounts must authenticate before explicitly linking. Apple may supply a relay email and only return the name on the first consent; Gate preserves existing fields when later logins omit them.