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
| Provider | Scopes | Profile Data |
|---|---|---|
| GitHub | read:user, user:email | ID, email, name, avatar |
openid, email, profile | ID, email, name, avatar |
How It Works
- User clicks "Sign in with GitHub" (or Google)
- SonicJS redirects to the provider's authorization page
- User grants permission on the provider's site
- Provider redirects back to SonicJS with an authorization code
- SonicJS exchanges the code for an access token
- SonicJS fetches the user profile and creates or links the account
- User is logged in with a JWT token
Setup
Step 1: Create OAuth App on Provider
GitHub:
- Go to GitHub Developer Settings
- Click New OAuth App
- Set Authorization callback URL to
https://yourdomain.com/auth/oauth/github/callback - Copy the Client ID and generate a Client Secret
Google:
- Go to Google Cloud Console
- Create a new OAuth 2.0 Client ID
- Add
https://yourdomain.com/auth/oauth/google/callbackas an Authorized redirect URI - 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
- Go to Admin > Plugins > OAuth Providers
- Enter your Client ID and Client Secret for each provider
- Toggle Enable for each provider you want to activate
- 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. BothclientIdandclientSecretmust 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:
- Existing OAuth link found — Updates tokens and logs in the linked user
- No OAuth link, but email matches existing user — Links OAuth to the existing account and logs in
- No existing user — Creates a new user account from the OAuth profile with the
viewerrole
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
/auth/oauth/:providerStarts 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
/auth/oauth/:provider/callbackThis 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:
| Parameter | Description |
|---|---|
code | Authorization code from the provider |
state | CSRF state parameter (validated against cookie) |
error | Error code if the user denied access |
error_description | Human-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
/auth/oauth/accountsAuth requiredList 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
/auth/oauth/linkAuth requiredReturns 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
/auth/oauth/unlinkAuth requiredRemoves 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/linkendpoint. - 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
);
| Column | Description |
|---|---|
user_id | Foreign key to the users table |
provider | Provider identifier (e.g., github, google) |
provider_account_id | The user's unique ID on the provider |
access_token | OAuth access token (updated on each login) |
refresh_token | OAuth refresh token (if provided by the provider) |
token_expires_at | Token expiration timestamp in milliseconds |
profile_data | JSON 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:emailscope. - Google: Ensure the
emailscope 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.