# Social sign-in for your app

A project can offer Google, Apple, Discord, Kakao and Naver next to [email codes](https://www.mocco.club/docs/en/articles/idw4yi-email-sign-in). Every method ends in the same session: a fifteen-minute ES256 access token and a rotating refresh token, scoped to your project. You bring your own provider apps. Mocco stores each client secret encrypted and never returns it.

All paths are relative to `/v1/identity` on the public API host, or `/api/ext/v1/identity` on the application host. Calls use a publishable key (`mk_pub_…`) with the `identity:auth` scope, from an origin on the project's web origins, except the provider callback, which carries no key.

## 1. Set up a provider

In the console, open the project's **App users** page and the **Sign-in methods** tab. Each card shows the callback URL to register with that provider. An agent can do the same with `mocco_identity_providers_search`, `mocco_identity_providers_set` and `mocco_identity_providers_remove`; changes need the `identity:write` scope, workspace agent opt-in and a confirmed round trip.

Register this callback URL in the provider's console, exactly as shown:

```text
<IDENTITY_ISSUER>/api/ext/v1/identity/oauth/<provider>/callback
```

`<provider>` is `google`, `apple`, `discord`, `kakao` or `naver`. On a self-hosted server, `IDENTITY_ISSUER` must be the HTTPS origin that serves Mocco's `/api/ext` routes, because that is where the provider sends the browser back. Social sign-in also needs `SECRETS_ENCRYPTION_KEYS`; see [identity vars](https://github.com/fi-workers/mocco/blob/main/docs/reference/env.md#identity-vars).

For each provider, enter the client ID and secret, optionally the scopes (the defaults are enough to read an email, name and picture), and **extra client IDs**. A token is accepted when its audience is the client ID or one of the extra client IDs. Use extra client IDs for the audiences of native apps, such as an iOS bundle ID for Apple or the iOS and Android client IDs for Google.

| Provider | What to create | Notes |
| --- | --- | --- |
| Google | An OAuth client of type Web application | Add the iOS and Android client IDs as extra client IDs for native sign-in |
| Apple | A Services ID, plus a Sign in with Apple key | Mocco asks for the team ID, key ID and the private key (`.p8`), and signs the client secret itself. Add the app's bundle ID as an extra client ID for native sign-in |
| Discord | An OAuth2 application | Redirect flow only |
| Kakao | A Kakao Login app with the REST API key | Enable the email consent item. Put the numeric Kakao app ID first in the extra client IDs for native sign-in |
| Naver | A Naver Login application | Redirect flow only |

Provider consoles change their wording and layout, so treat these hints as a guide and follow the provider's own documentation where they differ.

The OAuth clients Mocco uses with Kakao and Naver have no PKCE between Mocco and the provider; the one-time `state` still protects that leg. Your app's own PKCE (below) is unaffected.

Provider settings are admin-only and audited. Removing a provider keeps the accounts that signed in with it, but nobody can use that method until it is set up again.

## 2. Allow where sign-in may return

A redirect URI is accepted only if it is one of these:

- A URL on one of the app's **web origins**. The origin must match exactly. It must be `https`, except for loopback hosts, and may not carry credentials or a fragment.
- An exact **native redirect URI** registered on the app, such as `myapp://auth/callback`. Set them in the console (the app's **Native redirect URIs**) or with `mocco_projects_apps_native_redirect_uris_set`.

`javascript:`, `data:`, `file:`, `vbscript:` and `blob:` are never allowed. Anything else answers `400 invalid_redirect_uri`.

## 3. Sign in from a web page or an in-app browser

The redirect flow runs your app, Mocco, the provider, and back. The app holds the PKCE verifier, so a stolen redirect cannot be redeemed without it. `@mocco/js/identity` and `@mocco/react-native/identity` do this for you:

```ts
import { createIdentityClient } from '@mocco/js/identity';

const identity = createIdentityClient({
  publishableKey: 'mk_pub_…',
  storage: {
    get: async key => localStorage.getItem(key),
    set: async (key, value) => localStorage.setItem(key, value),
    remove: async key => localStorage.removeItem(key),
  },
});

// On the "Continue with Google" button:
const { authorizationUrl } = await identity.oauthStart('google', {
  redirectUri: `${location.origin}/auth/callback`,
});
location.assign(authorizationUrl);

// On /auth/callback:
const session = await identity.oauthComplete(location.href);
```

Underneath, the steps are:

1. `POST /oauth/<provider>/start` with `{ redirectUri, codeChallenge, state?, link? }`. The challenge is the base64url SHA-256 of your verifier. The answer is `{ authorizationUrl }`.
2. The provider returns the browser to Mocco's callback, which redirects it to your `redirectUri` with `?code=…&state=…`, or `?error=…&state=…` when the person declined or the provider failed.
3. `POST /oauth/exchange` with `{ code, codeVerifier }` answers the usual session: `{ accessToken, refreshToken, expiresIn, user }`.

The hand-off `code` is single-use and lasts 60 seconds. The `state` Mocco keeps for the provider leg lasts 10 minutes. A callback whose state is unknown, spent or expired shows a short page, "Sign-in link expired", because no registered address is known to send the browser to. `oauthComplete` rejects an answer whose `state` is not the one it started and keeps the pending sign-in, so a stray URL cannot cancel it.

Naver and Discord have no native flow. In a native app, open `authorizationUrl` in the system or in-app browser (for example with `expo-web-browser`'s `openAuthSessionAsync`) and pass the returned URL to `oauthComplete`.

## 4. Sign in natively with a provider token

When the platform's own SDK hands your app a token, send it to Mocco instead of opening a browser: `POST /oauth/<provider>/token`, or `identity.signInWithToken(provider, body)`.

| Provider | Send | Notes |
| --- | --- | --- |
| Apple | `{ idToken, nonce, name? }` | `nonce` is required: send the raw nonce. The token may carry the raw value or its SHA-256 hex, which iOS apps usually send. Apple gives the name only on the first sign-in, so pass it as `name` |
| Google | `{ idToken, nonce? }` | If you send a `nonce`, the token must carry it |
| Kakao | `{ accessToken }` | Configure the numeric Kakao app ID as the first extra client ID, so Mocco can check the token belongs to your app |
| Naver, Discord | Not available | Use the redirect flow in an in-app browser |

A client-supplied nonce is defence in depth, not replay protection by itself. Mocco checks the token's signature, issuer, audience, expiry and nonce; a wrong audience, an unknown key or a bad nonce answers `401 provider_token_invalid`. Accepted audiences are the client ID and the extra client IDs.

## 5. Accounts, linking and unlinking

- A provider sign-in joins an existing user by email only when the provider vouches for that address. Google, Apple and Discord must mark it verified. Kakao must report both `is_email_valid` and `is_email_verified`. Naver gives no verified flag, so its email is kept on the linked account but never joins or fills a user. Otherwise a new user is created, and an unverified address is never used to join an account.
- To add a second provider to the person who is signed in, call `oauthStart` with `link: true`. The client sends the access token with the start request. A provider account that already belongs to another user answers `409 account_in_use`.
- `DELETE /accounts/<provider|email>` with the access token removes one sign-in method and answers the updated user. The last sign-in method cannot be removed: `409 last_sign_in_method`.

`GET /me` returns `{ id, projectId, email, emailVerified, name, avatarUrl, externalId, accounts: [{ provider, email, linkedAt }] }`. `GET /config` also lists the `providers` that can sign someone in on this server, so your UI shows only working buttons.

## 6. Keep the session

`createIdentityClient` keeps the session in the `storage` you inject: `localStorage` in a page, `SecureStore` or `AsyncStorage` in React Native. `accessToken()` returns a valid token, refreshing it when under a minute is left. Refreshes are single-flight within one runtime. They are not coordinated across browser tabs, and refresh tokens rotate, so an app with several tabs should serialize refreshes itself, for example with a Web Lock. `logout()` always clears the device first and then asks Mocco to end the session, so signing out works offline.

Verify tokens on your server with [the Node or Rust verifier](https://www.mocco.club/docs/en/articles/jhr84i-verify-tokens).

## 7. Move existing users in

To bring users from another system, import them in the console's **Import** tab or with `mocco_identity_users_import`. One call takes up to 1000 users:

```json
{
  "users": [
    {
      "email": "Reader@Example.com",
      "name": "Reader",
      "externalId": "u_1042",
      "accounts": [{ "provider": "kakao", "subject": "3482910" }]
    }
  ]
}
```

- Emails are lowercased and trimmed. Each user may carry up to five provider accounts, each a provider and the provider's own subject.
- Users that already exist are skipped, never overwritten. The answer lists each skip by `index` and `reason`: `email_taken`, `external_id_taken`, `account_taken`, or `no_sign_in_method` when a user has neither an email nor a provider account.
- The answer also holds a `mapping` of each created user's `externalId` (or `null`) to the new Mocco user ID.
- An `externalId` is your own ID for the person. It appears in `/me` as `externalId` and in the access token as the `ext` claim, which is absent when none is set.

**Use your existing provider apps.** A provider's subject is scoped to the provider app, or to the team for Apple. If you configure Mocco with a new Kakao or Naver app, or another Apple team, the same person has a different subject and will not match the imported account. Configure the project with the apps your current system already uses: the same Kakao and Naver apps and the same Apple team. A suggested order:

1. Set up each provider with its existing client and register Mocco's callback URL on it, alongside the old one.
2. Import the users with their provider subjects and external IDs, and check the counts and skips.
3. Switch your app to Mocco sign-in. People continue with the same provider and land on the account you imported.
4. Remove the old callback URL once nobody uses it.

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 404 | `provider_not_configured` | The provider is unknown, disabled or has no settings for this project |
| 400 | `invalid_redirect_uri` | The redirect URI is not on the app's allowlist |
| 400 | `oauth_state_invalid` | Reserved. An unknown, spent or expired state shows the callback's 400 "Sign-in link expired" page, not a JSON problem |
| 401 | `invalid_session` | The hand-off code is unknown, spent or expired, or the PKCE verifier does not match (a code presented with a wrong verifier is burnt) |
| 502 | `oauth_exchange_failed` | The provider refused the code or could not be reached |
| 401 | `provider_token_invalid` | A native token failed its checks, or the provider has no native flow |
| 409 | `last_sign_in_method` | Unlinking would leave the user with no way to sign in |
| 409 | `account_in_use` | The provider account already belongs to another user |

Responses never contain tokens, one-time codes or the provider's own error text.
