Adding OAuth Providers
This guide shows you how to add additional OAuth providers to CraftJS beyond the pre-configured Google and GitHub options.
Supported Providers
Better Auth supports many OAuth providers out of the box:
- GitHub
- Discord
- Twitter/X
- Microsoft
- Apple
- Twitch
- Spotify
Adding a New Provider
Get OAuth Credentials
Each provider requires you to create an OAuth application:
Discord
- Go to Discord Developer Portal 2. Create a new
application 3. Go to OAuth2 section 4. Add redirect:
http://localhost:3000/api/auth/callback/discord5. Copy Client ID and Client Secret
Add Environment Variables
# Discord
DISCORD_CLIENT_ID="your-discord-client-id"
DISCORD_CLIENT_SECRET="your-discord-client-secret"
# Twitter
TWITTER_CLIENT_ID="your-twitter-client-id"
TWITTER_CLIENT_SECRET="your-twitter-client-secret"
# Microsoft
MICROSOFT_CLIENT_ID="your-microsoft-client-id"
MICROSOFT_CLIENT_SECRET="your-microsoft-client-secret"
# Apple
APPLE_CLIENT_ID="your-apple-services-id"
APPLE_CLIENT_SECRET="your-apple-private-key"Update Auth Configuration
// lib/auth/server.ts
import { betterAuth } from "better-auth";
import { env } from "@/env";
export const auth = betterAuth({
// ... existing config
socialProviders: {
// Existing providers
google: {
clientId: env.GOOGLE_CLIENT_ID!,
clientSecret: env.GOOGLE_CLIENT_SECRET!,
},
github: {
clientId: env.GITHUB_CLIENT_ID!,
clientSecret: env.GITHUB_CLIENT_SECRET!,
},
// Add new providers
discord: {
clientId: env.DISCORD_CLIENT_ID!,
clientSecret: env.DISCORD_CLIENT_SECRET!,
},
twitter: {
clientId: env.TWITTER_CLIENT_ID!,
clientSecret: env.TWITTER_CLIENT_SECRET!,
},
microsoft: {
clientId: env.MICROSOFT_CLIENT_ID!,
clientSecret: env.MICROSOFT_CLIENT_SECRET!,
},
},
});Update Environment Validation
// env.ts
export const env = createEnv({
server: {
// ... existing variables
// Discord
DISCORD_CLIENT_ID: z.string().optional(),
DISCORD_CLIENT_SECRET: z.string().optional(),
// Twitter
TWITTER_CLIENT_ID: z.string().optional(),
TWITTER_CLIENT_SECRET: z.string().optional(),
// Microsoft
MICROSOFT_CLIENT_ID: z.string().optional(),
MICROSOFT_CLIENT_SECRET: z.string().optional(),
},
// ...
});Add Login Buttons
// components/auth/social-login-buttons.tsx
"use client";
import { signIn } from "@/lib/auth/client";
import { Button } from "@/components/ui/button";
import { FaGoogle, FaGithub, FaDiscord, FaTwitter, FaMicrosoft } from "react-icons/fa";
const providers = [
{ id: "google", name: "Google", icon: FaGoogle, color: "bg-white border dark:bg-neutral-800" },
{ id: "github", name: "GitHub", icon: FaGithub, color: "bg-neutral-900 dark:bg-neutral-800" },
{ id: "discord", name: "Discord", icon: FaDiscord, color: "bg-indigo-600" },
{ id: "twitter", name: "Twitter", icon: FaTwitter, color: "bg-sky-500" },
{ id: "microsoft", name: "Microsoft", icon: FaMicrosoft, color: "bg-blue-600" },
];
export function SocialLoginButtons() {
const handleSocialLogin = async (provider: string) => {
await signIn.social({
provider: provider as any,
callbackURL: "/dashboard",
});
};
return (
<div className="space-y-3">
{providers.map((provider) => (
<Button
key={provider.id}
onClick={() => handleSocialLogin(provider.id)}
className={`w-full ${provider.color} ${
provider.id === "google" ? "text-neutral-900 dark:text-neutral-100" : "text-white"
}`}
variant="outline"
>
<provider.icon className="mr-2 h-5 w-5" />
Continue with {provider.name}
</Button>
))}
</div>
);
}Provider-Specific Configuration
Discord with Bot Access
discord: {
clientId: env.DISCORD_CLIENT_ID!,
clientSecret: env.DISCORD_CLIENT_SECRET!,
scope: ["identify", "email", "guilds"], // Request guild access
},Microsoft with Specific Tenant
microsoft: {
clientId: env.MICROSOFT_CLIENT_ID!,
clientSecret: env.MICROSOFT_CLIENT_SECRET!,
tenantId: "common", // or specific tenant ID
},Apple Sign In
Apple requires additional setup:
apple: {
clientId: env.APPLE_CLIENT_ID!,
clientSecret: env.APPLE_CLIENT_SECRET!,
teamId: env.APPLE_TEAM_ID!,
keyId: env.APPLE_KEY_ID!,
},Customizing User Data
Map Provider Data
// lib/auth/server.ts
export const auth = betterAuth({
// ...
socialProviders: {
discord: {
clientId: env.DISCORD_CLIENT_ID!,
clientSecret: env.DISCORD_CLIENT_SECRET!,
mapProfileToUser: (profile) => ({
name: profile.username,
email: profile.email,
image: `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`,
// Add custom fields
discordId: profile.id,
}),
},
},
});Store Provider Tokens
// Access tokens are stored automatically
// You can use them for API calls
import { auth } from "@/lib/auth/server";
import { db } from "@/lib/db/client";
import { accounts } from "@/lib/db/schema";
import { eq, and } from "drizzle-orm";
export async function getDiscordToken(userId: string) {
const [account] = await db
.select()
.from(accounts)
.where(and(eq(accounts.userId, userId), eq(accounts.providerId, "discord")))
.limit(1);
return account?.accessToken;
}
// Use token to call Discord API
export async function getDiscordGuilds(userId: string) {
const token = await getDiscordToken(userId);
const response = await fetch("https://discord.com/api/users/@me/guilds", {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.json();
}Linking Multiple Providers
Allow users to link multiple social accounts:
// API route to link account
// app/api/auth/link/[provider]/route.ts
import { auth } from "@/lib/auth/server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export async function GET(req: Request, { params }: { params: { provider: string } }) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
redirect("/login");
}
// Generate link URL
const linkUrl = await auth.api.linkSocial({
provider: params.provider,
userId: session.user.id,
});
redirect(linkUrl);
}// components/settings/linked-accounts.tsx
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
interface LinkedAccount {
provider: string;
email: string;
linkedAt: string;
}
export function LinkedAccounts() {
const [accounts, setAccounts] = useState<LinkedAccount[]>([]);
useEffect(() => {
fetch("/api/user/accounts")
.then((res) => res.json())
.then(setAccounts);
}, []);
const linkAccount = (provider: string) => {
window.location.href = `/api/auth/link/${provider}`;
};
const unlinkAccount = async (provider: string) => {
await fetch(`/api/user/accounts/${provider}`, { method: "DELETE" });
setAccounts(accounts.filter((a) => a.provider !== provider));
};
return (
<div className="space-y-4">
<h3 className="font-semibold">Linked Accounts</h3>
{["google", "github", "discord"].map((provider) => {
const linked = accounts.find((a) => a.provider === provider);
return (
<div key={provider} className="flex items-center justify-between">
<span className="capitalize">{provider}</span>
{linked ? (
<div className="flex items-center gap-2">
<span className="text-sm text-neutral-500 dark:text-neutral-400">
{linked.email}
</span>
<Button variant="outline" size="sm" onClick={() => unlinkAccount(provider)}>
Unlink
</Button>
</div>
) : (
<Button variant="outline" size="sm" onClick={() => linkAccount(provider)}>
Link
</Button>
)}
</div>
);
})}
</div>
);
}Testing OAuth
Local Development
For local testing, use:
- Redirect URI:
http://localhost:3000/api/auth/callback/{provider}
Staging/Preview
For staging environments (like Vercel previews):
- Add preview URLs to OAuth app settings
- Use environment-specific variables
Production
For production:
- Use your production domain
- Enable additional security settings
- Consider domain verification
Always keep OAuth credentials secure. Never commit them to version control.
Troubleshooting
Common Issues
- Redirect URI mismatch - Ensure the callback URL matches exactly
- Invalid client - Check client ID/secret
- Scope errors - Some scopes require app verification
- CORS errors - OAuth should redirect, not AJAX call
Debug Mode
// Enable debug logging
export const auth = betterAuth({
// ...
debug: process.env.NODE_ENV === "development",
});Next Steps
- Authentication - Core auth concepts
- Database - Account storage
- Configuration - Environment setup
Last updated on