OAuth providers
How OAuth 2.0 sign-in works in KavachOS and how to add any provider.
The oauth() plugin adds social sign-in to KavachOS using OAuth 2.0 authorization code flow with PKCE. It handles token exchange, account linking, and session creation automatically.
Built-in providers
Generic setup
import { createKavach } from '@kavachos/core';
import { oauth } from '@kavachos/core/plugins/oauth';
const kavach = await createKavach({
database: { provider: 'postgres', url: process.env.DATABASE_URL! },
secret: process.env.KAVACH_SECRET!,
baseUrl: 'https://auth.example.com',
plugins: [
oauth({
providers: [
{
id: 'google',
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
],
}),
],
});The callback URL registered with each provider follows the pattern:
https://auth.example.com/auth/oauth/{providerId}/callbackThe sign-in flow
- Your frontend redirects to
/auth/oauth/{providerId}/authorize. KavachOS generates a state parameter and PKCE challenge, then redirects to the provider. - The provider redirects back with an authorization code.
- KavachOS exchanges the code for tokens, fetches the user profile, and creates or updates the user record.
- A session cookie is set and the user lands on your redirect destination.
The user ID is stable: the same provider account always resolves to the same row in kavach_users.
Account linking
When a sign-in email matches an existing user, KavachOS links the OAuth identity to that account automatically. A single user can connect multiple providers.
// Check connected providers
const connections = await kavach.auth.getOAuthConnections(userId);
// Disconnect a provider (requires at least one remaining sign-in method)
await kavach.auth.disconnectOAuth(userId, 'github');Auto-linking trusts the email from the provider. If your threat model requires stronger guarantees, set autoLink: false and handle linking explicitly.
Custom providers
Any OAuth 2.0 provider works by supplying the authorization and token endpoints:
oauth({
providers: [
{
id: 'linear',
clientId: process.env.LINEAR_CLIENT_ID!,
clientSecret: process.env.LINEAR_CLIENT_SECRET!,
authorizationUrl: 'https://linear.app/oauth/authorize',
tokenUrl: 'https://api.linear.app/oauth/token',
userInfoUrl: 'https://api.linear.app/graphql',
scopes: ['read'],
getUserProfile: (data) => ({
id: data.data.viewer.id,
email: data.data.viewer.email,
name: data.data.viewer.name,
}),
},
],
})getUserProfile maps the raw provider response to { id, email?, name?, image? }. The id is the provider-side user ID, stored for deduplication.