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

# OAuth

> Google, GitHub, Apple, and Facebook OAuth provider integration

Sublay supports OAuth 2.0 sign-in with Google, GitHub, Apple, and Facebook. The flow is redirect-based: the user is sent to the provider's authorization page and returns to a callback URL in your app with tokens embedded in the URL fragment.

<Note>
  OAuth providers must be configured in the Sublay dashboard before use. Each
  provider requires a client ID, client secret, and a list of allowed redirect
  URIs.
</Note>

## Web Integration

The `useOAuthSignIn` hook (from `@sublay/react-js`) handles the full web OAuth flow.

```tsx theme={null}
import { useOAuthSignIn } from "@sublay/react-js";
```

### Initiating Sign-In

Call `initiateOAuth` with the provider name and the URL your app will redirect back to after authentication.

```tsx theme={null}
import { useOAuthSignIn } from "@sublay/react-js";

function SignInWithGoogle() {
  const { initiateOAuth, isLoading, error } = useOAuthSignIn();

  const handleClick = async () => {
    // The user will be redirected to Google's authorization page.
    // isLoading stays true during the redirect.
    await initiateOAuth({ provider: "google", redirectAfterAuth: "https://yourapp.com/auth/callback" });
  };

  return (
    <button onClick={handleClick} disabled={isLoading}>
      {isLoading ? "Redirecting..." : "Sign in with Google"}
    </button>
  );
}
```

Supported provider values: `"google"`, `"github"`, `"apple"`, `"facebook"`.

### Handling the Callback

On the page your app redirects back to, call `handleOAuthCallback` once on mount. It reads the tokens from the URL fragment, stores them in the SDK, and cleans the URL.

```tsx theme={null}
import { useEffect } from "react";
import { useOAuthSignIn } from "@sublay/react-js";
import { useNavigate } from "react-router-dom";

function AuthCallbackPage() {
  const { handleOAuthCallback, error } = useOAuthSignIn();
  const navigate = useNavigate();

  useEffect(() => {
    const success = handleOAuthCallback();
    if (success) {
      navigate("/dashboard");
    }
  }, []);

  if (error) return <p>Authentication failed: {error}</p>;

  return <p>Authenticating...</p>;
}
```

`handleOAuthCallback` returns `true` if tokens were found in the URL fragment and `false` otherwise. If the provider returned an error (for example, the user denied access), the error is surfaced through the `error` field.

### Linking an Additional Provider

Authenticated users can link additional OAuth providers to their account using `linkOAuthProvider`. This requires the user to already be signed in.

```tsx theme={null}
import { useOAuthSignIn } from "@sublay/react-js";

function LinkGitHubButton() {
  const { linkOAuthProvider } = useOAuthSignIn();

  return (
    <button onClick={() => linkOAuthProvider({ provider: "github", redirectAfterAuth: "https://yourapp.com/settings/callback" })}>
      Connect GitHub
    </button>
  );
}
```

The callback page logic is identical for both sign-in and link flows — `handleOAuthCallback` handles both cases.

## Expo Integration

`@sublay/expo` ships its own `useOAuthSignIn` hook with the **same API** as the web hook (`initiateOAuth`, `linkOAuthProvider`, `handleOAuthCallback`, `isLoading`, `error`). Instead of a full-page redirect, it opens the Sublay-brokered consent screen in the system browser and returns to your app through a custom-scheme deep link.

<Note>
  Deep links don't work reliably in **Expo Go**. You need a [custom dev client](https://docs.expo.dev/develop/development-builds/introduction/) (`npx expo run:ios` / `npx expo run:android`) or an [EAS Build](https://docs.expo.dev/build/introduction/).
</Note>

### Setup

<Steps>
  <Step title="Install the peer dependencies">
    The Expo hook relies on `expo-web-browser` (to open the auth session) and `expo-linking` (to parse the return URL).

    ```bash theme={null}
    npx expo install expo-web-browser expo-linking
    ```
  </Step>

  <Step title="Register a URL scheme">
    Add a custom `scheme` to your `app.json` (or `app.config.js`). This is the scheme your `redirectAfterAuth` deep link uses.

    ```json app.json theme={null}
    {
      "expo": {
        "scheme": "myapp"
      }
    }
    ```
  </Step>

  <Step title="Allow the redirect URI in the dashboard">
    In your Sublay project's OAuth provider settings, add the exact deep link you'll pass as `redirectAfterAuth` (e.g. `myapp://auth/callback`) to the provider's **Allowed Redirect URIs**.
  </Step>
</Steps>

### Initiating Sign-In

`redirectAfterAuth` is **required** on mobile — there is no `window.location` to fall back to. Pass the deep link you registered above.

```tsx theme={null}
import { useOAuthSignIn } from "@sublay/expo";

function SignInWithGoogle() {
  const { initiateOAuth, isLoading, error } = useOAuthSignIn();

  const handlePress = () =>
    initiateOAuth({
      provider: "google",
      redirectAfterAuth: "myapp://auth/callback",
    });

  return (
    <Button
      title={isLoading ? "Signing in..." : "Sign in with Google"}
      onPress={handlePress}
      disabled={isLoading}
    />
  );
}
```

On a successful return, tokens are parsed from the deep link and stored automatically. There is **no separate callback page** — the result is resolved inline. `handleOAuthCallback` exists for API parity but is a no-op that always returns `false`, so you never need to call it on mobile.

If the user cancels or dismisses the browser, the flow resolves quietly: `isLoading` resets to `false` and `error` stays `null`. `error` is only set on a server failure, a provider `error` in the redirect, or when no tokens are returned.

### Linking an Additional Provider

`linkOAuthProvider` works exactly like the web flow and requires the user to already be signed in.

```tsx theme={null}
import { useOAuthSignIn } from "@sublay/expo";

function LinkGitHubButton() {
  const { linkOAuthProvider } = useOAuthSignIn();

  return (
    <Button
      title="Connect GitHub"
      onPress={() =>
        linkOAuthProvider({
          provider: "github",
          redirectAfterAuth: "myapp://settings",
        })
      }
    />
  );
}
```

### Persistence

Tokens dispatched by the hook are picked up by the `SublayProvider`'s account manager and written to `SecureStore`, so the session survives app relaunches automatically. You don't need to add any storage code.

### Web vs. Expo at a glance

| | Web (`@sublay/react-js`) | Expo (`@sublay/expo`) |
| - | - | - |
| Browser handoff | Full-page redirect via `window.location` | System browser via `expo-web-browser` |
| `redirectAfterAuth` | Optional (defaults to current URL) | **Required** (a custom-scheme deep link) |
| Callback handling | Call `handleOAuthCallback` on the callback page | Resolved inline; `handleOAuthCallback` is a no-op |
| Token persistence | `localStorage` | `SecureStore` |
| Account limit reached | `accountLimitReached` (never throws) | `accountLimitReached` (never throws) |

### The Account Limit

A device stores at most **5 accounts** ([Multi-Account](/v8/sdk/authentication/multi-account)). Signing in a sixth through OAuth is refused — but **on both platforms the limit surfaces as state, not as a thrown error**, unlike email sign-up/sign-in and external auth, which reject the call.

Read it from `useAccounts` (or `useAddAccount`) rather than from the hook's `error` field, which stays reserved for failures the provider itself reported:

```tsx theme={null}
import { useOAuthSignIn, useAccounts } from "@sublay/react-js";

function OAuthCallbackPage() {
  const { handleOAuthCallback, error } = useOAuthSignIn();
  const { accountLimitReached } = useAccounts();

  useEffect(() => {
    handleOAuthCallback();
  }, []);

  if (accountLimitReached) return <p>You're signed in to 5 accounts already. Sign out of one to add another.</p>;
  if (error) return <p>{error}</p>;
  return <p>Signing you in...</p>;
}
```

**Why it cannot throw.** `handleOAuthCallback` is synchronous and shared by both platform packages. It returns as soon as it has read the redirect's tokens — before your app knows whose they are — and on the web the page that called `initiateOAuth` was torn down by the redirect, so there is no promise left to reject. Making it reject would mean changing the shape of a function both packages depend on.

**What happens instead.** The identity is resolved, found to be new, and refused. The session that was just created is signed back out on the server, so nothing is orphaned, and the account that was active before is re-*selected* — but its session is **not** re-established, because the redirect had already replaced its tokens before the identity was known. The app renders signed-out with that account selected and the user picks it again from the switcher. Starting the flow from `addAccount()` changes nothing here — that call leaves the selection in place — so the account the user came from is the one restored. Only a device with no account selected at all stays on the sign-in screen.

An OAuth sign-in for an account the device **already stores** is never refused — the check is keyed on the resolved account id.

## How Tokens Are Returned

After the provider callback, Sublay redirects to your `redirectAfterAuth` URL with tokens in the **URL fragment** (not query parameters):

```
https://yourapp.com/auth/callback#accessToken=...&refreshToken=...
```

Fragments are not sent to servers and do not appear in server access logs, which prevents token leakage. `handleOAuthCallback` extracts them automatically.

## Managing Linked Identities

Use `useOAuthIdentities` to list and unlink OAuth identities on the current user's account.

```tsx theme={null}
import { useOAuthIdentities } from "@sublay/react-js";

function LinkedAccounts() {
  const { identities, fetchIdentities, unlinkIdentity, isLoading } = useOAuthIdentities();

  useEffect(() => {
    fetchIdentities();
  }, []);

  return (
    <ul>
      {identities.map((identity) => (
        <li key={identity.id}>
          {identity.provider} — {identity.email}
          <button onClick={() => unlinkIdentity(identity.id)}>Unlink</button>
        </li>
      ))}
    </ul>
  );
}
```

<Warning>
  Unlinking an identity is blocked if it is the last identity on the account and
  the user has no password set. This prevents the user from being locked out.
</Warning>

## See Also

* [`useOAuthIdentities` hook reference](/v8/hooks/auth/use-oauth-identities)
* [OAuth Authorize API reference](/v8/api-reference/oauth/authorize)
* [OAuth Link API reference](/v8/api-reference/oauth/link)
* [List Identities API reference](/v8/api-reference/oauth/list-identities)
* [Unlink Identity API reference](/v8/api-reference/oauth/unlink-identity)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.