npm package: @prpldev/payload-expo-oauth
Description: OAuth authentication plugin for Payload CMS with Expo/React Native mobile support
Author: prpldev
Latest version: 1.0.0

# @prpldev/payload-expo-oauth

A [Payload CMS](https://payloadcms.com) plugin that adds OAuth/SSO authentication using [Arctic](https://arcticjs.dev/).

## Features

- **Multiple OAuth Providers**: Google, Microsoft Entra, GitHub, Apple, Discord, Facebook, LINE
- **Mobile App Support**: Works with Expo AuthSession and custom deep links
- **Auto User Creation**: Automatically create users on first OAuth login
- **Account Linking**: Link multiple OAuth providers to a single user
- **Customizable**: Hooks for user creation, login, and field mapping
- **PKCE Support**: Secure authentication with PKCE where supported

## Installation

```bash
npm install @prpldev/payload-expo-oauth
# or
pnpm add @prpldev/payload-expo-oauth
# or
yarn add @prpldev/payload-expo-oauth
```

## Quick Start

```typescript
import { buildConfig } from 'payload'
import { arcticOAuthPlugin, googleProvider, entraProvider } from '@prpldev/payload-expo-oauth'

export default buildConfig({
  // ... your config
  plugins: [
    arcticOAuthPlugin({
      providers: [
        googleProvider({
          clientId: process.env.GOOGLE_CLIENT_ID!,
          clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
        }),
        entraProvider({
          clientId: process.env.ENTRA_CLIENT_ID!,
          clientSecret: process.env.ENTRA_CLIENT_SECRET!,
          tenantId: process.env.ENTRA_TENANT_ID!,
        }),
      ],
    }),
  ],
})
```

OAuth buttons will automatically appear on the admin login page.

## Configuration

### Plugin Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `providers` | `OAuthProvider[]` | Required | Array of OAuth providers |
| `userCollection` | `string` | `'users'` | Collection to store users |
| `autoCreateUsers` | `boolean` | `true` | Auto-create users on first login |
| `successRedirect` | `string` | `'/admin'` | Redirect after successful login |
| `failureRedirect` | `string` | `'/admin/login?error=oauth_failed'` | Redirect on failure |
| `disableLocalStrategy` | `boolean` | `false` | Disable email/password login |
| `enableMobileAuth` | `boolean` | `false` | Enable mobile app authentication |
| `allowedMobileRedirectUris` | `string[]` | `[]` | Allowed redirect URIs for mobile |

### Supported Providers

```typescript
import {
  googleProvider,
  entraProvider,
  githubProvider,
  appleProvider,
  discordProvider,
  facebookProvider,
  lineProvider,
} from '@prpldev/payload-expo-oauth'
```

Each provider requires specific configuration. See [docs/setup.md](docs/setup.md) for detailed setup instructions.

## Mobile App Authentication (Expo)

Full authentication support for Expo apps using `expo-auth-session`. Supports both OAuth providers and email/password login.

### How It Works

```
┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│    Expo App     │     │   Payload CMS    │     │ OAuth Provider  │
└────────┬────────┘     └────────┬─────────┘     └────────┬────────┘
         │                       │                        │
         │  Open browser with    │                        │
         │  ?redirect_uri=app:// │                        │
         │──────────────────────>│                        │
         │                       │                        │
         │      Show login page (OAuth buttons +          │
         │      email/password form)                      │
         │<──────────────────────│                        │
         │                       │                        │
         │  User picks OAuth     │   OAuth flow           │
         │  or enters email/pwd  │──────────────────────> │
         │                       │<────────────────────── │
         │                       │                        │
         │  Redirect to app://   │                        │
         │  ?token=JWT&user_id=  │                        │
         │<──────────────────────│                        │
         │                       │                        │
         │  Parse token, done!   │                        │
└────────┴───────────────────────┴────────────────────────┘
```

### 1. Configure the Plugin

```typescript
arcticOAuthPlugin({
  providers: [/* ... */],
  enableMobileAuth: true,
  allowedMobileRedirectUris: [
    'myapp://auth/callback',        // Production app
    'exp://192.168.*',              // Expo Go (local network)
    'exp://localhost:*',            // Expo Go (localhost)
  ],
})
```

### 2. Expo App Code

```typescript
import * as WebBrowser from 'expo-web-browser';
import * as AuthSession from 'expo-auth-session';
import * as Linking from 'expo-linking';
import * as SecureStore from 'expo-secure-store';

WebBrowser.maybeCompleteAuthSession();

const PAYLOAD_URL = 'https://your-payload-server.com';

const redirectUri = AuthSession.makeRedirectUri({
  scheme: 'myapp',
  path: 'auth/callback',
});

const login = async () => {
  // Opens the Payload admin login page
  // User can choose OAuth provider OR enter email/password
  const loginUrl = `${PAYLOAD_URL}/admin/login?redirect_uri=${encodeURIComponent(redirectUri)}`;
  const result = await WebBrowser.openAuthSessionAsync(loginUrl, redirectUri);

  if (result.type === 'success' && result.url) {
    const parsed = Linking.parse(result.url);
    const { token, user_id, expires_in } = parsed.queryParams || {};

    if (token) {
      // Store token securely
      await SecureStore.setItemAsync('payload_token', token as string);
      console.log('Logged in! User ID:', user_id);
    }
  }
};

// Use token for authenticated requests
const fetchWithAuth = async (endpoint: string) => {
  const token = await SecureStore.getItemAsync('payload_token');
  return fetch(`${PAYLOAD_URL}${endpoint}`, {
    headers: { Authorization: `JWT ${token}` },
  });
};
```

### What Happens

1. **User opens login page** - The Payload admin login page loads with OAuth buttons and email/password form
2. **OAuth login** - Clicking an OAuth button preserves the `redirect_uri` through the OAuth flow
3. **Email/password login** - Form submission is intercepted and routed to the mobile-login endpoint
4. **Redirect with token** - After authentication, user is redirected to `redirect_uri?token=...&user_id=...&expires_in=...`
5. **App receives token** - The Expo app parses the URL and stores the JWT token

See [docs/expo-auth.md](docs/expo-auth.md) for complete integration guide.

## Customization

### Hooks

```typescript
arcticOAuthPlugin({
  providers: [/* ... */],

  // Called before creating a new user
  beforeUserCreate: async ({ userInfo, provider }) => {
    return {
      email: userInfo.email,
      name: userInfo.name,
      role: 'member', // Add custom fields
    }
  },

  // Called after successful login
  afterLogin: async ({ user, userInfo, provider }) => {
    console.log(`User ${user.email} logged in via ${provider}`)
  },

  // Map provider fields to user fields
  mapUserFields: (userInfo, provider) => ({
    email: userInfo.email,
    firstName: userInfo.firstName,
    lastName: userInfo.lastName,
    avatar: userInfo.avatarUrl,
  }),
})
```

### OAuth Accounts Field

The plugin automatically adds an `oauthAccounts` field to your user collection, storing linked OAuth providers:

```typescript
{
  oauthAccounts: [
    {
      provider: 'google',
      providerId: '123456789',
      email: 'user@gmail.com',
      connectedAt: '2024-01-15T10:30:00.000Z',
    }
  ]
}
```

## API Endpoints

The plugin adds these endpoints to your user collection:

| Endpoint | Description |
|----------|-------------|
| `GET /api/{collection}/oauth/{provider}` | Start OAuth flow |
| `GET /api/{collection}/oauth/{provider}/callback` | OAuth callback |
| `GET /api/{collection}/oauth/providers` | List available providers |
| `POST /api/{collection}/oauth/mobile-login` | Local auth for mobile apps (when `enableMobileAuth` is true)

### Mobile Local Auth

For email/password login in mobile apps, POST to `/api/{collection}/oauth/mobile-login`:

```typescript
// Form data or JSON
const response = await fetch(`${PAYLOAD_URL}/api/users/oauth/mobile-login`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'user@example.com',
    password: 'password',
    redirect_uri: 'myapp://auth/callback',
  }),
});
// Redirects to: myapp://auth/callback?token=...&user_id=...&expires_in=...
```

## Security

- **PKCE**: Enabled by default for providers that support it
- **State Parameter**: CSRF protection via state validation
- **Redirect URI Validation**: Mobile redirect URIs are validated against whitelist
- **HttpOnly Cookies**: Tokens stored securely in HttpOnly cookies (web)

## License

MIT