Account Linking (Social Network Binding to Account) on Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Account Linking (Social Network Binding to Account) on Website
Medium
from 1 business day to 3 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Linking Social Network Accounts

Account linking allows a user with an email/password account to add Google or GitHub as a login method and vice versa. The main challenge is safely identifying the account owner before linking.

Architecture: Data Schema

model User {
  id           String        @id @default(cuid())
  email        String        @unique
  passwordHash String?       // null if only social networks
  accounts     LinkedAccount[]
}

model LinkedAccount {
  id                String   @id @default(cuid())
  userId            String
  provider          String   // 'google' | 'github' | 'apple'
  providerAccountId String   // ID in provider system
  providerEmail     String?  // email from provider
  linkedAt          DateTime @default(now())

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
  @@index([userId])
}

Server Actions: Linking Provider

// app/settings/security/actions.ts
'use server';

import { auth } from '@/auth';
import { redirect } from 'next/navigation';
import { generateState } from '@/lib/oauth';

export async function initiateLinkProvider(provider: string) {
  const session = await auth();
  if (!session) redirect('/login');

  // Generate state with userId (CSRF protection)
  const state = await generateState({
    userId: session.user.id,
    action: 'link',
    provider,
  });

  // Redirect to OAuth flow with link=true parameter
  redirect(`/api/auth/link/${provider}?state=${state}`);
}

export async function unlinkProvider(accountId: string) {
  const session = await auth();
  if (!session) redirect('/login');

  // Check that this is current user's account
  const account = await db.linkedAccount.findFirst({
    where: { id: accountId, userId: session.user.id }
  });

  if (!account) {
    throw new Error('Account not found');
  }

  // Cannot unlink the only login method
  const accountsCount = await db.linkedAccount.count({
    where: { userId: session.user.id }
  });
  const hasPassword = await db.user.findUnique({
    where: { id: session.user.id },
    select: { passwordHash: true }
  });

  if (accountsCount <= 1 && !hasPassword?.passwordHash) {
    throw new Error('Cannot unlink the only login method');
  }

  await db.linkedAccount.delete({ where: { id: accountId } });
}

OAuth Callback: Handling Linking

// app/api/auth/callback/[provider]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { provider: string } }
) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code');
  const state = searchParams.get('state');

  // Decrypt state
  const stateData = await verifyState(state);
  if (!stateData || stateData.action !== 'link') {
    redirect('/settings/security?error=invalid_state');
  }

  // Exchange code for access token
  const tokens = await exchangeCode(params.provider, code);
  const providerUser = await fetchProviderUser(params.provider, tokens.accessToken);

  // Check: is this account already linked to another user
  const existingLink = await db.linkedAccount.findUnique({
    where: {
      provider_providerAccountId: {
        provider: params.provider,
        providerAccountId: providerUser.id,
      }
    }
  });

  if (existingLink && existingLink.userId !== stateData.userId) {
    redirect('/settings/security?error=account_already_linked');
  }

  if (!existingLink) {
    await db.linkedAccount.create({
      data: {
        userId: stateData.userId,
        provider: params.provider,
        providerAccountId: providerUser.id,
        providerEmail: providerUser.email,
      }
    });
  }

  redirect('/settings/security?success=linked');
}

UI Component for Management

// components/LinkedAccountsManager.tsx
'use client';

import { useState, useTransition } from 'react';
import { initiateLinkProvider, unlinkProvider } from './actions';

const PROVIDERS = [
  { id: 'google', name: 'Google', icon: <GoogleIcon /> },
  { id: 'github', name: 'GitHub', icon: <GitHubIcon /> },
  { id: 'apple', name: 'Apple', icon: <AppleIcon /> },
];

export function LinkedAccountsManager({
  linkedAccounts,
  hasPassword,
}: {
  linkedAccounts: Array<{ id: string; provider: string; providerEmail: string | null }>;
  hasPassword: boolean;
}) {
  const [isPending, startTransition] = useTransition();
  const linkedProviders = new Set(linkedAccounts.map(a => a.provider));

  const canUnlink = (provider: string) => {
    // Cannot unlink if it's the only login method
    const otherAccounts = linkedAccounts.filter(a => a.provider !== provider);
    return hasPassword || otherAccounts.length > 0;
  };

  return (
    <div className="space-y-4">
      {PROVIDERS.map((provider) => {
        const linked = linkedAccounts.find(a => a.provider === provider.id);

        return (
          <div key={provider.id} className="flex items-center justify-between p-4 border rounded-lg">
            <div className="flex items-center gap-3">
              {provider.icon}
              <div>
                <p className="font-medium">{provider.name}</p>
                {linked && (
                  <p className="text-sm text-gray-500">{linked.providerEmail}</p>
                )}
              </div>
            </div>

            {linked ? (
              <button
                onClick={() => startTransition(() => unlinkProvider(linked.id))}
                disabled={!canUnlink(provider.id) || isPending}
                className="text-red-600 disabled:opacity-50"
              >
                Unlink
              </button>
            ) : (
              <button
                onClick={() => startTransition(() => initiateLinkProvider(provider.id))}
                disabled={isPending}
                className="text-blue-600"
              >
                Link
              </button>
            )}
          </div>
        );
      })}
    </div>
  );
}

Implementing provider linking/unlinking with account lock prevention — 2–3 working days.