OAuth Providers Plugin (Social Login)

Add social login to your SonicJS application with GitHub, Google, and more using the OAuth2 authorization code flow.


Overview

The OAuth Providers plugin enables users to sign in with their existing social accounts instead of creating a new username and password. It implements the standard OAuth2 authorization code flow with CSRF protection, automatic account creation, and provider linking.

🔑

Social Login

Sign in with GitHub, Google, and other OAuth2 providers

🔗

Account Linking

Link multiple OAuth providers to a single user account

🛡️

CSRF Protection

State parameter validation with secure HTTP-only cookies

👤

Auto Registration

Automatically create accounts from OAuth profiles

Supported Providers

ProviderScopesProfile Data
GitHubread:user, user:emailID, email, name, avatar
Googleopenid, email, profileID, email, name, avatar

How It Works

  1. User clicks "Sign in with GitHub" (or Google)
  2. SonicJS redirects to the provider's authorization page
  3. User grants permission on the provider's site
  4. Provider redirects back to SonicJS with an authorization code
  5. SonicJS exchanges the code for an access token
  6. SonicJS fetches the user profile and creates or links the account
  7. User is logged in with a JWT token

Setup

Step 1: Create OAuth App on Provider

GitHub:

  1. Go to GitHub Developer Settings
  2. Click New OAuth App
  3. Set Authorization callback URL to https://yourdomain.com/auth/oauth/github/callback
  4. Copy the Client ID and generate a Client Secret

Google:

  1. Go to Google Cloud Console
  2. Create a new OAuth 2.0 Client ID
  3. Add https://yourdomain.com/auth/oauth/google/callback as an Authorized redirect URI
  4. Copy the Client ID and Client Secret

For local development, use http://localhost:8787/auth/oauth/github/callback (or /google/callback) as your callback URL. GitHub allows http for localhost; Google requires you to add it as an authorized redirect URI.

Step 2: Configure in Admin

  1. Go to Admin > Plugins > OAuth Providers
  2. Enter your Client ID and Client Secret for each provider
  3. Toggle Enable for each provider you want to activate
  4. Save settings

Step 3: Add Login Buttons

Add social login buttons to your login page that link to the OAuth authorization endpoints:

Social Login Buttons

<!-- GitHub Login -->
<a href="/auth/oauth/github" class="btn btn-github">
  Sign in with GitHub
</a>

<!-- Google Login -->
<a href="/auth/oauth/google" class="btn btn-google">
  Sign in with Google
</a>

Configuration

The plugin stores provider credentials in the database via the admin plugin settings UI.

Plugin Settings Schema

interface OAuthPluginSettings {
  providers: {
    github?: {
      clientId: string       // GitHub OAuth App Client ID
      clientSecret: string   // GitHub OAuth App Client Secret
      enabled: boolean       // Enable/disable GitHub login
    }
    google?: {
      clientId: string       // Google OAuth Client ID
      clientSecret: string   // Google OAuth Client Secret
      enabled: boolean       // Enable/disable Google login
    }
  }
}

Provider Configuration Details

  • Name
    clientId
    Type
    string
    Description

    The OAuth Client ID obtained from the provider's developer console.

  • Name
    clientSecret
    Type
    string
    Description

    The OAuth Client Secret obtained from the provider's developer console. Stored securely and never exposed to the client.

  • Name
    enabled
    Type
    boolean
    Description

    Whether this provider is active. Defaults to false. Both clientId and clientSecret must be set for the provider to work.

Callback URLs

The callback URL is automatically constructed from the incoming request headers:

{protocol}://{host}/auth/oauth/{provider}/callback

For example:

  • Production: https://yourdomain.com/auth/oauth/github/callback
  • Local dev: http://localhost:8787/auth/oauth/google/callback

The callback URL uses x-forwarded-proto and host headers from the request, so it works correctly behind reverse proxies and load balancers.


Authentication Flow

OAuth2 Authorization Code Flow

The plugin implements the standard OAuth2 authorization code flow with CSRF protection:

Flow Details

1. GET /auth/oauth/github
   → Generates random state parameter
   → Stores state in HTTP-only cookie (10-minute expiry)
   → Redirects to GitHub authorization URL

2. User authorizes on GitHub
   → GitHub redirects to /auth/oauth/github/callback?code=xxx&state=yyy

3. GET /auth/oauth/github/callback
   → Validates state parameter against cookie (CSRF protection)
   → Clears state cookie
   → Exchanges authorization code for access token
   → Fetches user profile from GitHub API
   → Creates or links user account
   → Issues JWT and sets auth cookie
   → Redirects to /admin

Account Resolution

When an OAuth callback is received, the plugin follows this logic:

  1. Existing OAuth link found — Updates tokens and logs in the linked user
  2. No OAuth link, but email matches existing user — Links OAuth to the existing account and logs in
  3. No existing user — Creates a new user account from the OAuth profile with the viewer role

New users created via OAuth have:

  • Email from the provider profile
  • Name parsed from the provider profile
  • Username derived from the email prefix (with collision handling)
  • No password set (they authenticate exclusively via OAuth unless they set one later)
  • Default role of viewer

API Reference

Initiate OAuth Login

GET/auth/oauth/:provider
Redirect to OAuth provider authorization page

Starts the OAuth flow by redirecting the user to the provider's authorization page. The :provider parameter must be github or google.

Initiate Login

# Browser redirect — typically used as an <a> link, not via cURL
curl -v http://localhost:8787/auth/oauth/github
# → 302 redirect to https://github.com/login/oauth/authorize?client_id=...&state=...

OAuth Callback

GET/auth/oauth/:provider/callback
Handle OAuth provider callback with authorization code

This endpoint is called by the OAuth provider after the user authorizes. It exchanges the authorization code for tokens, fetches the user profile, and creates or links the account.

Query Parameters:

ParameterDescription
codeAuthorization code from the provider
stateCSRF state parameter (validated against cookie)
errorError code if the user denied access
error_descriptionHuman-readable error description

On success: Sets JWT auth cookie and redirects to /admin.

On failure: Redirects to /auth/login?error=... with a descriptive error message.

List Linked Accounts

GET/auth/oauth/accountsAuth required
List OAuth accounts linked to the current user

List Linked Accounts

curl http://localhost:8787/auth/oauth/accounts \
  -H "Authorization: Bearer eyJhbGc..."

Response (200 OK):

{
  "accounts": [
    {
      "provider": "github",
      "providerAccountId": "12345678",
      "linkedAt": 1712000000000
    },
    {
      "provider": "google",
      "providerAccountId": "109876543210",
      "linkedAt": 1712100000000
    }
  ]
}

Link OAuth Account

POST/auth/oauth/linkAuth required
Link an OAuth provider to the current user account

Returns a redirect URL to start the OAuth flow for account linking. The callback will automatically link the provider to the authenticated user's existing account.

Link Account

curl -X POST http://localhost:8787/auth/oauth/link \
  -H "Authorization: Bearer eyJhbGc..." \
  -H "Content-Type: application/json" \
  -d '{ "provider": "google" }'

Response (200 OK):

{
  "redirectUrl": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&state=..."
}

Unlink OAuth Account

POST/auth/oauth/unlinkAuth required
Unlink an OAuth provider from the current user account

Removes an OAuth provider link from the user's account. This operation is blocked if it would leave the user with no authentication method (no password and no other OAuth links).

Unlink Account

curl -X POST http://localhost:8787/auth/oauth/unlink \
  -H "Authorization: Bearer eyJhbGc..." \
  -H "Content-Type: application/json" \
  -d '{ "provider": "github" }'

Response (200 OK):

{
  "success": true,
  "message": "github account unlinked"
}

Error (400 Bad Request):

{
  "error": "Cannot unlink the only authentication method. Set a password first."
}

Account Linking

How Linking Works

Users can connect multiple OAuth providers to a single SonicJS account:

  • Automatic linking on login: If a user signs in with OAuth and their email matches an existing account, the OAuth provider is automatically linked.
  • Manual linking: Authenticated users can link additional providers via the POST /auth/oauth/link endpoint.
  • Safe unlinking: Users can remove a provider link, but only if they have another authentication method (a password or another OAuth link).

Database Schema

The plugin uses an oauth_accounts table to track provider links:

CREATE TABLE oauth_accounts (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL,
  provider TEXT NOT NULL,
  provider_account_id TEXT NOT NULL,
  access_token TEXT,
  refresh_token TEXT,
  token_expires_at INTEGER,
  profile_data TEXT,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL
);
ColumnDescription
user_idForeign key to the users table
providerProvider identifier (e.g., github, google)
provider_account_idThe user's unique ID on the provider
access_tokenOAuth access token (updated on each login)
refresh_tokenOAuth refresh token (if provided by the provider)
token_expires_atToken expiration timestamp in milliseconds
profile_dataJSON snapshot of the provider profile

Advanced Usage

Building a Social Login Page

Complete Login Page Example

import { Hono } from 'hono'

const app = new Hono()

app.get('/login', (c) => {
  const error = c.req.query('error')

  return c.html(`
    <!DOCTYPE html>
    <html>
    <head><title>Login</title></head>
    <body>
      <h1>Sign In</h1>

      ${error ? `<div class="error">${error}</div>` : ''}

      <form method="POST" action="/auth/login">
        <input name="email" type="email" placeholder="Email" required />
        <input name="password" type="password" placeholder="Password" required />
        <button type="submit">Sign In</button>
      </form>

      <hr />
      <p>Or sign in with:</p>

      <a href="/auth/oauth/github">Sign in with GitHub</a>
      <a href="/auth/oauth/google">Sign in with Google</a>
    </body>
    </html>
  `)
})

Checking Provider Availability

Before showing social login buttons, you can check which providers are enabled:

Check Enabled Providers

// Server-side: check if a provider is configured
async function getEnabledProviders(db: D1Database) {
  const row = await db.prepare(
    `SELECT settings FROM plugins WHERE id = 'oauth-providers'`
  ).first()

  if (!row?.settings) return []

  const settings = JSON.parse(row.settings)
  return Object.entries(settings.providers || {})
    .filter(([_, config]) => config.enabled && config.clientId && config.clientSecret)
    .map(([id]) => id)
}

// Usage in a route
app.get('/login', async (c) => {
  const providers = await getEnabledProviders(c.env.DB)

  return c.html(`
    ${providers.includes('github') ? '<a href="/auth/oauth/github">GitHub</a>' : ''}
    ${providers.includes('google') ? '<a href="/auth/oauth/google">Google</a>' : ''}
  `)
})

Account Settings Page

Allow users to manage their linked OAuth accounts:

Account Settings

// Fetch linked accounts
const response = await fetch('/auth/oauth/accounts', {
  headers: { 'Authorization': `Bearer ${token}` }
});
const { accounts } = await response.json();

// Display linked accounts with unlink buttons
accounts.forEach(account => {
  console.log(`${account.provider} - linked ${new Date(account.linkedAt).toLocaleDateString()}`);
});

// Link a new provider
async function linkProvider(provider) {
  const response = await fetch('/auth/oauth/link', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ provider })
  });

  const { redirectUrl } = await response.json();
  window.location.href = redirectUrl;
}

// Unlink a provider
async function unlinkProvider(provider) {
  const response = await fetch('/auth/oauth/unlink', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ provider })
  });

  const result = await response.json();
  if (result.error) {
    alert(result.error);
  }
}

Troubleshooting

"OAuth provider is not configured or not enabled"

  • Verify that you have entered the Client ID and Client Secret in Admin > Plugins > OAuth Providers
  • Ensure the Enable toggle is turned on for the provider
  • Check that the plugin itself is activated

"Invalid OAuth state. Please try again."

This indicates a CSRF validation failure. Possible causes:

  • The state cookie expired (10-minute window)
  • Cookies are being blocked by the browser
  • The user opened multiple OAuth flows in different tabs

Fix: Have the user try again. Ensure cookies are enabled and SameSite=Lax is compatible with your setup.

"Could not retrieve email from OAuth provider"

  • GitHub: The user's email may be set to private. The plugin attempts to fetch emails via the GitHub API, but the user must have at least one verified email. Ensure the OAuth app requests the user:email scope.
  • Google: Ensure the email scope is included.

Callback URL Mismatch

If the provider rejects the callback, verify that:

  • The callback URL registered with the provider matches exactly: https://yourdomain.com/auth/oauth/{provider}/callback
  • For local development: http://localhost:8787/auth/oauth/{provider}/callback
  • There are no trailing slashes or protocol mismatches

"Account is deactivated"

The user's account exists but has been deactivated by an admin. An admin must re-enable the account in Admin > Users.


Next Steps

Was this page helpful?