Social sign-in for your app

A project can offer Google, Apple, Discord, Kakao and Naver next to email codes. 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:

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

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.

ProviderWhat to createNotes
GoogleAn OAuth client of type Web applicationAdd the iOS and Android client IDs as extra client IDs for native sign-in
AppleA Services ID, plus a Sign in with Apple keyMocco 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
DiscordAn OAuth2 applicationRedirect flow only
KakaoA Kakao Login app with the REST API keyEnable the email consent item. Put the numeric Kakao app ID first in the extra client IDs for native sign-in
NaverA Naver Login applicationRedirect 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:

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

ProviderSendNotes
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, DiscordNot availableUse 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.

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:

{
  "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

StatusCodeMeaning
404provider_not_configuredThe provider is unknown, disabled or has no settings for this project
400invalid_redirect_uriThe redirect URI is not on the app's allowlist
400oauth_state_invalidReserved. An unknown, spent or expired state shows the callback's 400 "Sign-in link expired" page, not a JSON problem
401invalid_sessionThe hand-off code is unknown, spent or expired, or the PKCE verifier does not match (a code presented with a wrong verifier is burnt)
502oauth_exchange_failedThe provider refused the code or could not be reached
401provider_token_invalidA native token failed its checks, or the provider has no native flow
409last_sign_in_methodUnlinking would leave the user with no way to sign in
409account_in_useThe provider account already belongs to another user

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