oauth-social-login
π―Skillfrom dadbodgeoff/drift
Enables seamless social login with OAuth 2.0, supporting Google, GitHub, and other providers, handling authentication flow and user account management.
Part of
dadbodgeoff/drift(69 items)
Installation
npm install -g driftdetectnpm install -g driftdetect@latestnpm install -g driftdetect-mcp{
"mcpServers": {
"drift": {
"command": "driftdetect-mcp"
}
}
...Skill Details
Implement OAuth 2.0 social login with Google, GitHub, and other providers. Handles token exchange, user creation, and account linking.
Overview
# OAuth Social Login
Add "Sign in with Google/GitHub" to your app.
When to Use This Skill
- Adding social login options
- Reducing signup friction
- Linking multiple auth providers to one account
- Enterprise SSO requirements
Architecture
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User clicks β
β "Sign in with Google" β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Redirect to Provider β
β β
β /auth/google β Google OAuth consent screen β
β Include: client_id, redirect_uri, scope, state β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Provider Callback β
β β
β /auth/google/callback?code=xxx&state=yyy β
β 1. Verify state (CSRF protection) β
β 2. Exchange code for tokens β
β 3. Fetch user profile β
β 4. Create/link user account β
β 5. Issue session/JWT β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
TypeScript Implementation
OAuth Configuration
```typescript
// oauth-config.ts
interface OAuthProvider {
name: string;
clientId: string;
clientSecret: string;
authorizationUrl: string;
tokenUrl: string;
userInfoUrl: string;
scopes: string[];
}
const providers: Record
google: {
name: 'Google',
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
userInfoUrl: 'https://www.googleapis.com/oauth2/v2/userinfo',
scopes: ['openid', 'email', 'profile'],
},
github: {
name: 'GitHub',
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
authorizationUrl: 'https://github.com/login/oauth/authorize',
tokenUrl: 'https://github.com/login/oauth/access_token',
userInfoUrl: 'https://api.github.com/user',
scopes: ['read:user', 'user:email'],
},
};
export { providers, OAuthProvider };
```
OAuth Service
```typescript
// oauth-service.ts
import crypto from 'crypto';
import { providers, OAuthProvider } from './oauth-config';
interface OAuthTokens {
accessToken: string;
refreshToken?: string;
expiresIn?: number;
tokenType: string;
}
interface OAuthUserInfo {
id: string;
email: string;
name?: string;
picture?: string;
provider: string;
}
class OAuthService {
private stateStore = new Map
generateAuthUrl(providerName: string, redirectTo?: string): string {
const provider = providers[providerName];
if (!provider) throw new Error(Unknown provider: ${providerName});
// Generate CSRF state token
const state = crypto.randomBytes(32).toString('hex');
this.stateStore.set(state, { provider: providerName, redirectTo });
// Auto-expire state after 10 minutes
setTimeout(() => this.stateStore.delete(state), 10 60 1000);
const params = new URLSearchParams({
client_id: provider.clientId,
redirect_uri: this.getCallbackUrl(providerName),
response_type: 'code',
scope: provider.scopes.join(' '),
state,
access_type: 'offline', // For refresh tokens (Google)
prompt: 'consent',
});
return ${provider.authorizationUrl}?${params};
}
async handleCallback(
providerName: string,
code: string,
state: string
): Promise<{ user: OAuthUserInfo; redirectTo?: string }> {
// Verify state
const stateData = this.stateStore.get(state);
if (!stateData || stateData.provider !== providerName) {
throw new Error('Invalid state parameter');
}
this.stateStore.delete(state);
const provider = providers[providerName];
if (!provider) throw new Error(Unknown provider: ${providerName});
// Exchange code for tokens
const tokens = await this.exchangeCode(provider, code);
// Fetch user info
const userInfo = await this.fetchUserInfo(provider, tokens.accessToken, providerName);
return { user: userInfo, redirectTo: stateData.redirectTo };
}
private async exchangeCode(provider: OAuthProvider, code: string): Promise
const response = await fetch(provider.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: new URLSearchParams({
client_id: provider.clientId,
client_secret: provider.clientSecret,
code,
grant_type: 'authorization_code',
redirect_uri: this.getCallbackUrl(provider.name.toLowerCase()),
}),
});
if (!response.ok) {
throw new Error(Token exchange failed: ${response.statusText});
}
const data = await response.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
tokenType: data.token_type,
};
}
private async fetchUserInfo(
provider: OAuthProvider,
accessToken: string,
providerName: string
): Promise
const response = await fetch(provider.userInfoUrl, {
headers: {
Authorization: Bearer ${accessToken},
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(Failed to fetch user info: ${response.statusText});
}
const data = await response.json();
// Normalize user info across providers
return this.normalizeUserInfo(data, providerName);
}
private normalizeUserInfo(data: Record
switch (provider) {
case 'google':
return {
id: data.id as string,
email: data.email as string,
name: data.name as string,
picture: data.picture as string,
provider: 'google',
};
case 'github':
return {
id: String(data.id),
email: data.email as string,
name: (data.name || data.login) as string,
picture: data.avatar_url as string,
provider: 'github',
};
default:
throw new Error(Unknown provider: ${provider});
}
}
private getCallbackUrl(provider: string): string {
return ${process.env.APP_URL}/auth/${provider}/callback;
}
}
export const oauthService = new OAuthService();
```
Express Routes
```typescript
// oauth-routes.ts
import { Router, Request, Response } from 'express';
import { oauthService } from './oauth-service';
import { userService } from './user-service';
import { sessionService } from './session-service';
const router = Router();
// Initiate OAuth flow
router.get('/auth/:provider', (req: Request, res: Response) => {
const { provider } = req.params;
const { redirect } = req.query;
try {
const authUrl = oauthService.generateAuthUrl(provider, redirect as string);
res.redirect(authUrl);
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
});
// OAuth callback
router.get('/auth/:provider/callback', async (req: Request, res: Response) => {
const { provider } = req.params;
const { code, state, error } = req.query;
if (error) {
return res.redirect(/login?error=${error});
}
if (!code || !state) {
return res.redirect('/login?error=missing_params');
}
try {
const { user: oauthUser, redirectTo } = await oauthService.handleCallback(
provider,
code as string,
state as string
);
// Find or create user
let user = await userService.findByOAuthId(provider, oauthUser.id);
if (!user) {
// Check if email already exists
const existingUser = await userService.findByEmail(oauthUser.email);
if (existingUser) {
// Link OAuth to existing account
user = await userService.linkOAuthAccount(existingUser.id, {
provider,
providerId: oauthUser.id,
email: oauthUser.email,
});
} else {
// Create new user
user = await userService.createFromOAuth({
email: oauthUser.email,
name: oauthUser.name,
picture: oauthUser.picture,
provider,
providerId: oauthUser.id,
});
}
}
// Create session
const session = await sessionService.create(user.id);
res.cookie('session', session.token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 7 24 60 60 1000, // 7 days
});
res.redirect(redirectTo || '/dashboard');
} catch (error) {
console.error('OAuth callback error:', error);
res.redirect('/login?error=oauth_failed');
}
});
export { router as oauthRoutes };
```
User Service (Account Linking)
```typescript
// user-service.ts
interface User {
id: string;
email: string;
name?: string;
picture?: string;
oauthAccounts: OAuthAccount[];
}
interface OAuthAccount {
provider: string;
providerId: string;
email: string;
}
class UserService {
async findByOAuthId(provider: string, providerId: string): Promise
// Find user by OAuth provider ID
return db.user.findFirst({
where: {
oauthAccounts: {
some: { provider, providerId },
},
},
include: { oauthAccounts: true },
});
}
async findByEmail(email: string): Promise
return db.user.findUnique({
where: { email },
include: { oauthAccounts: true },
});
}
async createFromOAuth(data: {
email: string;
name?: string;
picture?: string;
provider: string;
providerId: string;
}): Promise
return db.user.create({
data: {
email: data.email,
name: data.name,
picture: data.picture,
oauthAccounts: {
create: {
provider: data.provider,
providerId: data.providerId,
email: data.email,
},
},
},
include: { oauthAccounts: true },
});
}
async linkOAuthAccount(userId: string, account: OAuthAccount): Promise
return db.user.update({
where: { id: userId },
data: {
oauthAccounts: {
create: account,
},
},
include: { oauthAccounts: true },
});
}
async unlinkOAuthAccount(userId: string, provider: string): Promise
const user = await this.findById(userId);
// Ensure user has another way to login
if (user.oauthAccounts.length <= 1 && !user.passwordHash) {
throw new Error('Cannot unlink last authentication method');
}
await db.oauthAccount.deleteMany({
where: { userId, provider },
});
}
}
export const userService = new UserService();
```
Python Implementation
```python
# oauth_service.py
import secrets
import httpx
from dataclasses import dataclass
from typing import Optional
from urllib.parse import urlencode
@dataclass
class OAuthProvider:
name: str
client_id: str
client_secret: str
authorization_url: str
token_url: str
user_info_url: str
scopes: list[str]
PROVIDERS = {
"google": OAuthProvider(
name="Google",
client_id=os.environ["GOOGLE_CLIENT_ID"],
client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
authorization_url="https://accounts.google.com/o/oauth2/v2/auth",
token_url="https://oauth2.googleapis.com/token",
user_info_url="https://www.googleapis.com/oauth2/v2/userinfo",
scopes=["openid", "email", "profile"],
),
"github": OAuthProvider(
name="GitHub",
client_id=os.environ["GITHUB_CLIENT_ID"],
client_secret=os.environ["GITHUB_CLIENT_SECRET"],
authorization_url="https://github.com/login/oauth/authorize",
token_url="https://github.com/login/oauth/access_token",
user_info_url="https://api.github.com/user",
scopes=["read:user", "user:email"],
),
}
class OAuthService:
def __init__(self):
self._state_store: dict[str, dict] = {}
def generate_auth_url(self, provider_name: str, redirect_to: Optional[str] = None) -> str:
provider = PROVIDERS.get(provider_name)
if not provider:
raise ValueError(f"Unknown provider: {provider_name}")
state = secrets.token_hex(32)
self._state_store[state] = {"provider": provider_name, "redirect_to": redirect_to}
params = {
"client_id": provider.client_id,
"redirect_uri": self._get_callback_url(provider_name),
"response_type": "code",
"scope": " ".join(provider.scopes),
"state": state,
}
return f"{provider.authorization_url}?{urlencode(params)}"
async def handle_callback(self, provider_name: str, code: str, state: str):
state_data = self._state_store.pop(state, None)
if not state_data or state_data["provider"] != provider_name:
raise ValueError("Invalid state")
provider = PROVIDERS[provider_name]
tokens = await self._exchange_code(provider, code)
user_info = await self._fetch_user_info(provider, tokens["access_token"], provider_name)
return {"user": user_info, "redirect_to": state_data.get("redirect_to")}
async def _exchange_code(self, provider: OAuthProvider, code: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.post(
provider.token_url,
data={
"client_id": provider.client_id,
"client_secret": provider.client_secret,
"code": code,
"grant_type": "authorization_code",
"redirect_uri": self._get_callback_url(provider.name.lower()),
},
headers={"Accept": "application/json"},
)
response.raise_for_status()
return response.json()
async def _fetch_user_info(self, provider: OAuthProvider, access_token: str, provider_name: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(
provider.user_info_url,
headers={"Authorization": f"Bearer {access_token}"},
)
response.raise_for_status()
data = response.json()
return self._normalize_user_info(data, provider_name)
def _normalize_user_info(self, data: dict, provider: str) -> dict:
if provider == "google":
return {
"id": data["id"],
"email": data["email"],
"name": data.get("name"),
"picture": data.get("picture"),
"provider": "google",
}
elif provider == "github":
return {
"id": str(data["id"]),
"email": data["email"],
"name": data.get("name") or data.get("login"),
"picture": data.get("avatar_url"),
"provider": "github",
}
raise ValueError(f"Unknown provider: {provider}")
def _get_callback_url(self, provider: str) -> str:
return f"{os.environ['APP_URL']}/auth/{provider}/callback"
oauth_service = OAuthService()
```
Database Schema
```sql
-- Users table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255),
picture TEXT,
password_hash VARCHAR(255), -- NULL for OAuth-only users
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- OAuth accounts (one user can have multiple)
CREATE TABLE oauth_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(50) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
email VARCHAR(255),
access_token TEXT,
refresh_token TEXT,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(provider, provider_id)
);
CREATE INDEX idx_oauth_accounts_user ON oauth_accounts(user_id);
CREATE INDEX idx_oauth_accounts_provider ON oauth_accounts(provider, provider_id);
```
Frontend Integration
Security Considerations
- Always verify state parameter - Prevents CSRF attacks
- Use HTTPS in production - Tokens are sensitive
- Validate email ownership - Some providers don't verify emails
- Handle account linking carefully - Prevent account takeover
- Store tokens securely - Encrypt refresh tokens at rest
Common Mistakes
- Not validating state parameter
- Storing access tokens in localStorage
- Not handling token refresh
- Allowing unverified email linking
- Missing error handling for revoked tokens
More from this repository10
Enables controlled feature rollouts, A/B testing, and selective feature access through configurable flags for gradual deployment and user targeting.
Generates a comprehensive, type-safe design token system with WCAG AA color compliance and multi-framework support for consistent visual design.
Securely validates, scans, and processes file uploads with multi-stage checks, malware detection, and race condition prevention.
Guides users through articulating creative intent by extracting structured parameters and detecting conversation readiness.
Validates and centralizes environment variables with type safety, fail-fast startup checks, and multi-environment support.
Generates efficient social feed with cursor pagination, trending algorithms, and engagement tracking for infinite scroll experiences.
Enables secure, multi-tenant cloud file storage with signed URLs, direct uploads, and visibility control for user-uploaded assets.
Simplifies email sending, templating, and tracking with robust SMTP integration and support for multiple email providers and transactional workflows.
Sanitizes error messages by logging full details server-side while exposing only generic, safe messages to prevent sensitive information leakage.
Optimizes database operations by collecting and batching independent records, improving throughput by 30-40% with built-in fallback processing.