Skip to Content
⭐ CraftJS is open source. Star on GitHub →
DocsAdding OAuth Providers

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:

  • Google
  • GitHub
  • Discord
  • Twitter/X
  • Microsoft
  • Apple
  • LinkedIn
  • Facebook
  • Twitch
  • Spotify

Adding a New Provider

Get OAuth Credentials

Each provider requires you to create an OAuth application:

  1. Go to Discord Developer Portal  2. Create a new application 3. Go to OAuth2 section 4. Add redirect: http://localhost:3000/api/auth/callback/discord 5. 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

  1. Redirect URI mismatch - Ensure the callback URL matches exactly
  2. Invalid client - Check client ID/secret
  3. Scope errors - Some scopes require app verification
  4. 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

Last updated on