> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Social sign-in

> Google, on your own consent screen — and the account-linking rules that keep it from becoming a takeover.

Social sign-in is two calls, because the redirect leaves our process: **start**
hands you the URL to send the browser to, **callback** spends the code the
provider sent back.

Both are hosted-mode endpoints reached with a publishable key, from your own
domain. Nothing rides a cookie: the `state` comes back to you and presenting it
at the callback is what proves the two halves are one flow.

<Note>
  Google today. Adding a provider is a row and an implementation on our side —
  never a migration, and never a change to the two calls below.
</Note>

## The OAuth application is yours

You register the application in Google's console and store the credentials with
us. The consent screen your customer reads then says **your** product's name, not
ours — an identity provider whose own brand appears on somebody else's login
screen is selling the wrong thing.

```bash theme={null}
curl -s -X PUT $API/v1/organization/environments/$ENV_ID/oauth/google \
  -H "Authorization: Bearer uk_st_…" \
  -H "X-Organization-Id: $ORG_ID" \
  -H 'Content-Type: application/json' \
  -d '{ "client_id": "…apps.googleusercontent.com", "client_secret": "…" }'
```

The secret is encrypted at rest and **never returned**. You pasted it from a
console you administer; a secret readable back out of the panel is a secret with
one more way out. Pasting it again is how you rotate it.

Per environment, so you can point test at a throwaway Google project and ship
with the real one.

<Note>
  A **test** environment with nothing configured may fall back to a shared
  UserKit-owned application, so social sign-in works in your first ten minutes.
  Live never does: that consent screen has to be yours.
</Note>

Your redirect URI must be registered with Google as usual. It also has to pass
our own gate — its origin must be in the publishable key's allowed-origins list
whenever that list is set.

Your login screen does not have to be told any of this. `GET /v1/config/{key}`
answers with the providers this environment can actually sign someone in with,
so the button appears when you register the application and not when you next
deploy. See [Auth modes](/en/customer-auth/modes#reading-it-from-a-login-screen).

## Start

```bash theme={null}
curl -s $API/v1/contact-auth/oauth/google/start \
  -H 'Content-Type: application/json' \
  -d '{
    "publishable_key": "uk_pk_live_…",
    "redirect_uri": "https://app.example.com/auth/callback",
    "anonymous_id": "anon_2f9c1b"
  }'
```

```json 200 theme={null}
{
  "authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?…",
  "state": "uk_os_…",
  "expires_at": "2026-07-29T12:15:00Z"
}
```

Send the browser to `authorization_url` and keep `state` where the callback page
can read it — session storage is the usual answer. The flow is good for fifteen
minutes.

PKCE is applied on every flow, and the verifier never leaves our process: the
authorization code travels through a page we do not control, so a stolen code
alone buys nothing.

Passing `anonymous_id` folds the visitor this person already was into the
account, attribution included.

Rate limited to 30 per hour per IP.

## Callback

Your redirect page receives `?code=…` and posts it back with the state it kept:

```bash theme={null}
curl -s $API/v1/contact-auth/oauth/google/callback \
  -H 'Content-Type: application/json' \
  -d '{
    "publishable_key": "uk_pk_live_…",
    "state": "uk_os_…",
    "code": "4/0Ab…"
  }'
```

```json 200 theme={null}
{
  "contact": { "id": "…", "name": "Grace Hopper", "email_verified": true },
  "token": "uk_ct_…",
  "expires_at": "2026-08-28T12:00:00Z",
  "verified": true
}
```

The same shape as a password sign-in, so your SDK has one response to handle
however somebody arrived. Authenticating at the provider proves the account, so
the session is verified.

The state is single use and bound to its provider and its environment. Unknown,
expired, already spent, and "the provider refused the code" all answer the same
`401 invalid_grant`. A provider we could not reach answers `502
provider_unavailable` — that one is not your caller's fault.

Rate limited to 30 per hour per IP.

## How the account is resolved

This is the part worth reading twice. In order:

<Steps>
  <Step title="A contact already linked to this provider account">
    Resolved by the provider's stable subject id, never by the email. Somebody
    who changes their Google address is still the same person.
  </Step>

  <Step title="The provider vouched for an address">
    It links onto the contact that owns that address, or creates one. Two
    parties have now proved the same address, which is what makes linking
    permissible at all.
  </Step>

  <Step title="The provider did not vouch">
    The provider account is the only identity the sign-in carries. The email
    lands as an attribute and resolves to nothing — a new contact, even if the
    address looks familiar.
  </Step>
</Steps>

<Warning>
  Step two has a case underneath it. If the contact that owns the address had
  **never proven** it — signed up with a password and never clicked the
  verification link — then the password on it was never proven either. It is
  removed, along with every session and every outstanding one-time token, and the
  inbox is told what happened.

  That is the classic takeover closed: sign up with somebody else's address, wait
  for them to arrive by Google, and inherit their account. Here, the unproven
  credential does not survive into the account whose owner just took possession of
  it.
</Warning>

Linking is never silent. When a social account lands on a contact that already
existed, a notice goes to the address — only the person holding that inbox can
say whether it was them.

## Removing a provider

```bash theme={null}
curl -s -X DELETE $API/v1/organization/environments/$ENV_ID/oauth/google \
  -H "Authorization: Bearer uk_st_…" \
  -H "X-Organization-Id: $ORG_ID"
```

Existing links survive. They are history, and a developer who registers again
tomorrow should not find their customers locked out.
