Агрегация социальных логинов (Multiple OAuth Providers)
Поддержка нескольких OAuth-провайдеров одновременно: пользователь выбирает Google, GitHub, Apple или Microsoft — и попадает в один аккаунт. Ключевой вопрос — связывание аккаунтов при одинаковом email.
NextAuth.js / Auth.js: несколько провайдеров
// auth.ts (Auth.js v5)
import NextAuth from 'next-auth';
import Google from 'next-auth/providers/google';
import GitHub from 'next-auth/providers/github';
import Apple from 'next-auth/providers/apple';
import MicrosoftEntraID from 'next-auth/providers/microsoft-entra-id';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
Apple({
clientId: process.env.APPLE_ID!,
clientSecret: process.env.APPLE_SECRET!, // JWT из .p8 ключа
}),
MicrosoftEntraID({
clientId: process.env.AZURE_AD_CLIENT_ID!,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET!,
tenantId: process.env.AZURE_AD_TENANT_ID!, // или 'common' для всех
}),
],
callbacks: {
async signIn({ user, account, profile }) {
// Автоматическое связывание по email
if (user.email) {
const existingUser = await db.user.findUnique({
where: { email: user.email }
});
if (existingUser) {
// Проверяем, не привязан ли уже этот провайдер
const existingAccount = await db.account.findFirst({
where: {
userId: existingUser.id,
provider: account!.provider,
}
});
if (!existingAccount) {
// Привязываем новый провайдер к существующему аккаунту
await db.account.create({
data: {
userId: existingUser.id,
provider: account!.provider,
providerAccountId: account!.providerAccountId,
type: account!.type,
access_token: account!.access_token,
refresh_token: account!.refresh_token,
expires_at: account!.expires_at,
}
});
}
return true;
}
}
return true;
},
async session({ session, token }) {
if (token.sub) {
session.user.id = token.sub;
}
return session;
},
},
adapter: PrismaAdapter(db),
});
Prisma Schema: связанные аккаунты
model User {
id String @id @default(cuid())
email String @unique
name String?
image String?
createdAt DateTime @default(now())
accounts Account[]
sessions Session[]
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
UI: кнопки провайдеров
// components/SocialLoginButtons.tsx
'use client';
import { signIn } from 'next-auth/react';
const PROVIDERS = [
{
id: 'google',
name: 'Google',
icon: <GoogleIcon />,
className: 'bg-white border border-gray-300 hover:bg-gray-50',
},
{
id: 'github',
name: 'GitHub',
icon: <GitHubIcon />,
className: 'bg-gray-900 text-white hover:bg-gray-800',
},
{
id: 'apple',
name: 'Apple',
icon: <AppleIcon />,
className: 'bg-black text-white hover:bg-gray-900',
},
{
id: 'microsoft-entra-id',
name: 'Microsoft',
icon: <MicrosoftIcon />,
className: 'bg-[#00a4ef] text-white hover:bg-[#0090d4]',
},
] as const;
export function SocialLoginButtons({
callbackUrl = '/',
mode = 'login',
}: {
callbackUrl?: string;
mode?: 'login' | 'register';
}) {
return (
<div className="flex flex-col gap-3">
{PROVIDERS.map((provider) => (
<button
key={provider.id}
type="button"
onClick={() => signIn(provider.id, { callbackUrl })}
className={`flex items-center gap-3 px-4 py-2.5 rounded-lg font-medium ${provider.className}`}
>
{provider.icon}
<span>{mode === 'login' ? 'Войти' : 'Зарегистрироваться'} через {provider.name}</span>
</button>
))}
</div>
);
}
Clerk: готовое решение
// Clerk настраивается через dashboard.clerk.com
// Включаем провайдеры: Google, GitHub, Apple, Microsoft
// Связывание аккаунтов — автоматически
// Использование
import { SignIn } from '@clerk/nextjs';
export default function LoginPage() {
return (
<SignIn
appearance={{
elements: {
socialButtonsBlockButton: 'border border-gray-200',
}
}}
/>
);
}
Clerk автоматически обрабатывает conflict resolution при одинаковых email от разных провайдеров, показывая пользователю диалог подтверждения.
Управление привязанными аккаунтами
// Страница настроек: список привязанных провайдеров
import { auth } from '@/auth';
import { db } from '@/lib/db';
export default async function SecuritySettingsPage() {
const session = await auth();
const accounts = await db.account.findMany({
where: { userId: session!.user.id }
});
return (
<div>
<h2>Связанные аккаунты</h2>
{accounts.map(account => (
<div key={account.id} className="flex justify-between">
<span>{account.provider}</span>
{accounts.length > 1 && (
<button onClick={() => unlinkAccount(account.id)}>
Отвязать
</button>
)}
</div>
))}
</div>
);
}
Настройка Auth.js с 4 провайдерами, связыванием аккаунтов и Prisma адаптером — 2–3 рабочих дня.







