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

# Chrome extension authentication

This guide shows you how to implement Privy authentication and wallets in your Chrome extension using Privy's React SDK. Chrome extensions offer a unique application experience for your users, but come with some unique nuances specifically around social login.

## Resources

<CardGroup cols={3}>
  <Card title="Chrome Extension Starter" icon="github" href="https://github.com/privy-io/examples/tree/main/examples/privy-react-chrome-extension" arrow>
    Complete starter repository with Privy authentication and wallet management.
  </Card>
</CardGroup>

## Set up your Chrome extension project

First, create a React app and install Privy:

```bash theme={"system"}
npm create react-app my-extension
cd my-extension
npm install @privy-io/react-auth
```

Create your `manifest.json` file in the `public` directory:

```json theme={"system"}
{
  "manifest_version": 3,
  "name": "My Extension with Privy",
  "version": "1.0",
  "description": "Chrome extension with Privy authentication",
  "permissions": ["identity"],
  "host_permissions": ["https://api.privy.io/*"],
  "action": {
    "default_popup": "index.html",
    "default_title": "My Extension"
  },
  "options_page": "options.html",
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'; frame-ancestors 'none';"
  }
}
```

<Warning>
  The `identity` permission is required for OAuth flows, and `storage` is recommended for persisting
  user sessions.
</Warning>

### Security Guidelines

Below are comprehensive security guidelines for Chrome extensions. You can find more information in the [Chrome extension security documentation](https://developer.chrome.com/docs/extensions/develop/security-privacy/stay-secure).

<Accordion title="Security best practices">
  #### Content Security Policy

  Add a strict CSP to your manifest to prevent code injection and framing attacks. You can see our broader CSP guidance [here](/security/implementation-guide/content-security-policy).

  ```json theme={"system"}
  {
    "content_security_policy": {
      "extension_pages": "script-src 'self'; object-src 'self'; frame-ancestors 'none';"
    }
  }
  ```

  <Warning>
    The `frame-ancestors 'none'` directive prevents your extension from being embedded in frames,
    protecting against clickjacking attacks.
  </Warning>

  #### Minimal permissions

  Only request permissions your extension actually needs. Limiting permissions reduces attack surface if compromised:

  ```json theme={"system"}
  {
    "permissions": ["identity"],
    "host_permissions": ["https://api.privy.io/*"]
  }
  ```

  **Cross-origin fetch() restrictions:**
  Extensions can only use `fetch()` and `XMLHttpRequest()` to access domains specified in `host_permissions`. If the extension were compromised, it would still only have permission to interact with websites that meet the match pattern. The attacker would only have limited ability to access sites not in this list.

  ```json theme={"system"}
  {
    "host_permissions": ["https://api.privy.io/*", "https://api.yourservice.com/*"]
  }
  ```

  <Tip>
    Remove unused permissions like `tabs`, `activeTab`, or broad host permissions to reduce your
    extension's attack surface and improve user trust.
  </Tip>

  #### Externally connectable

  Restrict which external extensions and web pages can communicate with your extension:

  ```json theme={"system"}
  {
    "externally_connectable": {
      "ids": ["allowedextensionidheredata"],
      "matches": ["https://yourtrustedsite.com/*"],
      "accepts_tls_channel_id": false
    }
  }
  ```

  <Warning>
    Only include trusted sources in `externally_connectable`. This prevents malicious sites from
    communicating with your extension.
  </Warning>

  #### Web-accessible resources

  Minimize web-accessible resources as they make your extension detectable and create attack vectors:

  ```json theme={"system"}
  {
    "web_accessible_resources": [
      {
        "resources": ["images/icon.png"],
        "matches": ["https://yourtrustedsite.com/*"]
      }
    ]
  }
  ```

  <Tip>
    Keep web-accessible resources to a minimum. Each exposed resource increases potential attack
    surface.
  </Tip>

  #### Secure DOM manipulation

  Avoid `document.write()` and `innerHTML` which can lead to script injection:

  #### Validate all inputs

  Always validate and sanitize inputs, especially from content scripts:

  ```javascript theme={"system"}
  chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    // Validate sender is from your extension
    if (sender.id !== chrome.runtime.id) return;

    // Validate and sanitize request data
    if (request.action === 'updateUser' && typeof request.userData === 'object') {
      // Process validated data
      updateUser(request.userData);
    }
  });
  ```

  <Info>
    Content scripts can be compromised by malicious websites, so treat all messages from content
    scripts as potentially malicious.
  </Info>
</Accordion>

***

## Configure your Privy dashboard

<Info>
  You'll get your extension ID after loading the extension in Chrome's developer mode at
  `chrome://extensions/`.
</Info>

In the [Privy dashboard](https://dashboard.privy.io/apps?setting=domains\&page=settings), configure OAuth settings for your extension:

**1. Add allowed origins**

Go to **App Settings > Domains** and add:

```
chrome-extension://<your-extension-id>
```

**2. (Optional) Configure redirect URLs**

<Tip>Use `chrome.identity.getRedirectURL()` to get the exact redirect URL programmatically.</Tip>

If your extension uses social login, you'll need to configure redirect URLs.

In your allowed domains, add the following redirect URL, and additionally in your [allowed redirect URLs](https://dashboard.privy.io/apps?setting=advanced\&page=settings)

```
https://<your-extension-id>.chromiumapp.org/
```

***

## Enabling social login in your extension

Chrome extensions can't handle social OAuth flows directly in the popup due to security restrictions. **Social login requires opening either the options page or a popup window.** This provides the full browser context needed for OAuth redirects.

Both approaches follow the same flow:

1. User clicks "Sign in with social" in extension
2. Open authentication context (options page or popup window)
3. Privy handles the OAuth flow
4. User is redirected back to the extension authenticated

<Steps>
  <Step title="User initiates social login">
    ### Approach 1: Options page

    **Setup:** Add to your manifest:

    ```json theme={"system"}
    {"options_page": "options.html"}
    ```

    **Implementation:**

    ```tsx theme={"system"}
    // In your popup component
    const openOptionsForLogin = () => {
      chrome.tabs.create({
        url: chrome.runtime.getURL('options.html')
      });
    };
    ```

    ### Approach 2: Popup window

    **Implementation:**

    ```tsx theme={"system"}
    // In your popup component
    const openAuthWindow = () => {
      chrome.windows.create({
        url: chrome.runtime.getURL('auth.html'),
        type: 'popup',
        width: 400,
        height: 600
      });
    };
    ```
  </Step>

  <Step title="Open authentication context (options page or popup window)">
    Both approaches use the same authentication logic:

    <Tip>
      You can use the same `AuthComponent` for both approaches - just render it in different HTML files
      (options.html or auth.html).
    </Tip>

    ```tsx theme={"system"}
    // src/auth/AuthComponent.tsx
    import {PrivyProvider, usePrivy, useLogin} from '@privy-io/react-auth';
    import {useEffect} from 'react';

    const AuthContent = () => {
      const {authenticated, ready} = usePrivy();
      const {login} = useLogin({
        onComplete: () => {
          // Open the extension popup after authentication
          chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
            if (tabs[0]) {
              // Open the extension popup
              chrome.action.openPopup();
            }
          });
        }
      });

      // Auto-trigger login when opened for authentication
      useEffect(() => {
        if (ready && !authenticated) {
          login();
        }
      }, [authenticated, ready]);

      return null;
    };

    export const AuthComponent = () => (
      <PrivyProvider
        appId="<INSERT_YOUR_APP_ID_HERE>"
        config={{
          appearance: {
            loginMethods: ['google', 'apple', 'email', 'sms', 'twitter']
          }
        }}
      >
        <AuthContent />
      </PrivyProvider>
    );
    ```
  </Step>

  <Step title="Redirect back to the extension">
    Redirect the user back to the extension after authentication.

    ```tsx theme={"system"}
        onComplete: () => {
          // Open the extension popup after authentication
          chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
            if (tabs[0]) {
              // Open the extension popup
              chrome.action.openPopup();
            }
          });
        },
    ```
  </Step>
</Steps>

***

## That's it! 🎉

You've now implemented Privy authentication in your Chrome extension.

***

## Avoiding Chrome Web Store rejection for remote code

The Chrome Web Store prohibits extensions from loading remotely hosted code. By default, `@privy-io/react-auth` bundles dependencies that inject remote scripts at runtime, which can cause your extension to be rejected during review — even if those scripts never execute.

### Remote scripts in the React SDK

The React SDK conditionally loads these remote scripts:

| Script               | Remote URL                                      | When loaded                             |
| -------------------- | ----------------------------------------------- | --------------------------------------- |
| Cloudflare Turnstile | `challenges.cloudflare.com/turnstile/v0/api.js` | CAPTCHA enabled with Turnstile provider |
| hCaptcha             | `js.hcaptcha.com/1/api.js`                      | CAPTCHA enabled with hCaptcha provider  |
| Telegram login       | `{apiOrigin}/js/telegram-login.js`              | Telegram login enabled in app config    |

### Step 1: Disable features that load remote scripts

In the [Privy dashboard](https://dashboard.privy.io):

1. **Disable CAPTCHA** in [App settings > Advanced](https://dashboard.privy.io/apps?page=settings\&setting=advanced). This prevents the Turnstile and hCaptcha scripts from loading at runtime.
2. **Disable Telegram login** in [Authentication](https://dashboard.privy.io/apps?page=login-methods) if your extension does not need it. This prevents the Telegram login script from loading.

### Step 2: Remove bundled remote-code references

Disabling these features prevents the scripts from loading at runtime, but the Chrome Web Store review scans your extension package statically. The CAPTCHA wrapper code may still appear in your bundled output as a separate chunk, even if it is never executed.

To remove it from the bundle entirely, alias the underlying CAPTCHA dependencies to empty stubs in your bundler config:

<Tabs>
  <Tab title="Webpack">
    ```js theme={"system"}
    // webpack.config.js
    module.exports = {
      resolve: {
        alias: {
          '@marsidev/react-turnstile': false,
          '@hcaptcha/react-hcaptcha': false,
        },
      },
    };
    ```
  </Tab>

  <Tab title="Vite">
    Create a stub file at `stubs/empty-captcha.js`:

    ```js theme={"system"}
    export default () => null;
    export const Turnstile = () => null;
    ```

    Then add the aliases:

    ```js theme={"system"}
    // vite.config.js
    import {defineConfig} from 'vite';
    import path from 'path';

    export default defineConfig({
      resolve: {
        alias: {
          '@marsidev/react-turnstile': path.resolve(__dirname, 'stubs/empty-captcha.js'),
          '@hcaptcha/react-hcaptcha': path.resolve(__dirname, 'stubs/empty-captcha.js')
        }
      }
    });
    ```
  </Tab>
</Tabs>

This replaces the CAPTCHA libraries with inert stubs during bundling, so the remote Cloudflare and hCaptcha URLs do not appear anywhere in the extension package.

<Warning>
  These aliases depend on the internal dependency structure of `@privy-io/react-auth` and may need
  to be updated when upgrading SDK versions. Verify your built extension does not contain references
  to `challenges.cloudflare.com` or `js.hcaptcha.com` after each upgrade.
</Warning>

***

## Production considerations

Before publishing to the Chrome Web Store:

1. **Remove unnecessary permissions** from manifest
2. **Limit host permissions** to only required domains
3. **Minimize web-accessible resources** to reduce attack surface
4. **Implement strict CSP** with `frame-ancestors 'none'`
5. **Validate all inputs** from content scripts and external sources
6. **Update OAuth configuration** with production URLs in Privy dashboard
7. **Review externally connectable** settings for trusted domains only

<Tip>
  Chrome extensions with OAuth require Google's review. Document your authentication flow and
  privacy practices clearly in your Web Store listing. Follow the [Chrome Web Store security best
  practices](https://developer.chrome.com/docs/extensions/develop/security-privacy/stay-secure) for
  faster approval.
</Tip>
