Account Linking
Enable users to link multiple authentication methods (password, OAuth, magic links, etc.) to a single ZewstID account
Overview
Account linking allows users to authenticate using multiple methods while maintaining a single identity. For example, a user who initially signed up with email/password can later add Google OAuth or magic link authentication to their account.
Key Benefits
- Users can choose their preferred sign-in method
- No duplicate accounts for the same email
- Seamless migration from local auth to SSO
- Consistent user identity across all auth methods
How It Works
1. Automatic Conflict Detection
When a user tries to authenticate with a new method (e.g., magic link) using an email that already exists with a different method (e.g., password), ZewstID automatically detects the conflict:
// Response when email exists with different auth method { "error": "account_exists", "error_description": "An account with this email already exists", "existingIdentities": ["password", "google"], "linkingRequired": true, "linkingEndpoint": "/api/v1/auth/link/initiate", "suggestion": "Use the account linking flow to add..." }
2. Verification Flow
To ensure security, users must verify ownership before linking:
- Initiate linking: User provides email and new authentication method
- Verify ownership: User verifies with password OR email verification code
- Link accounts: ZewstID links the new method to the existing account
- Consistent identity: All auth methods return the same in JWT
sub
3. Using the SDK
The ZewstID SDK provides a simple API for account linking:
import { ZewstID } from '@zewstid/nextjs'; const zewstid = new ZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, apiUrl: 'https://api.zewstid.com', }); // Initiate account linking const { linkingToken, verificationRequired } = await zewstid.accountLinking.initiate({ email: 'user@example.com', newProvider: 'google' }); // Verify with password if (verificationRequired === 'password') { await zewstid.accountLinking.verify({ linkingToken, password: userPassword }); } // Or verify with email code if (verificationRequired === 'email_code') { await zewstid.accountLinking.verify({ linkingToken, code: emailCode }); }
Client Application Integration
Database Schema
Your application should use ZewstID's
sub// Prisma schema example model User { id String @id @default(cuid()) // ZewstID Integration - Stable identifier zewstIdSub String? @unique // Cached identity data (from ZewstID JWT) email String @unique name String? emailVerified Boolean @default(false) // Application-specific data organizationId String role Role @default(USER) @@index([zewstIdSub]) @@index([email]) }
Authentication Handler
Implement a lookup pattern that prioritizes
zewstIdSubasync function findOrCreateUser(token: string) { const { sub, email, email_verified } = decodeJWT(token); if (!email_verified) { throw new Error("Email not verified"); } // PRIMARY LOOKUP: By ZewstID sub (stable) let user = await db.user.findUnique({ where: { zewstIdSub: sub } }); if (user) return user; // MIGRATION: Check for existing email-based user const existingUser = await db.user.findUnique({ where: { email } }); if (existingUser) { // Link to ZewstID return await db.user.update({ where: { id: existingUser.id }, data: { zewstIdSub: sub, emailVerified: true } }); } // NEW USER: Create with ZewstID sub return await db.user.create({ data: { zewstIdSub: sub, email, emailVerified: true, // ... application defaults } }); }
API Reference
POST /auth/link/initiate
Initiate the account linking process.
Request Body:
{ "email": "user@example.com", "newProvider": "google", "clientId": "your-client-id" }
Response:
{ "linkingToken": "eyJhbGc...", "verificationRequired": "password", "existingIdentities": ["password"], "message": "Please verify with your password" }
POST /auth/link/verify
Verify ownership and complete linking.
Request Body:
{ "linkingToken": "eyJhbGc...", "verification": { "password": "user-password" // OR "code": "123456" } }
Response:
{ "success": true, "userId": "user-id", "linkedProvider": "google", "message": "Successfully linked..." }
GET /auth/link/identities
Get all linked authentication methods (requires authentication).
Headers:
Authorization: Bearer <access-token>
Response:
{ "identities": ["password", "google", "magic_link"], "email": "user@example.com" }
DELETE /auth/link/:provider
Unlink an authentication method (requires authentication).
Headers:
Authorization: Bearer <access-token>
Response:
{ "success": true, "unlinkedProvider": "google", "remainingIdentities": ["password", "magic_link"] }
Best Practices
✅ DO: Use ZewstID sub as primary identifier - Always look up users by
zewstIdSub✅ DO: Handle 409 conflict responses gracefully - When magic link or OTP returns a 409 error, guide users through the account linking flow.
✅ DO: Notify users when accounts are linked - Send confirmation emails when new authentication methods are added for security transparency.
❌ DON'T: Auto-link without verification - Always require password or email code verification to prevent unauthorized account access.
❌ DON'T: Store ZewstID sub without uniqueness constraint - Ensure
zewstIdSub❌ DON'T: Allow unlinking the last authentication method - Users must always have at least one way to authenticate. The API prevents this, but handle it in your UI too.
Next Steps
- SDK Documentation - Explore the full SDK API reference and examples
- RBAC Guide - Learn how to implement role-based access control
Was this page helpful?
Let us know how we can improve our documentation