@zewstid/nextjs
Official ZewstID SDK for Next.js - Complete authentication solution with 12 authentication methods in one simple package.
Current Version: v0.7.3 (January 2026)
⨠Features
Authentication Methods (v0.7.3)
- š OAuth/OIDC - Full OpenID Connect with ZewstID
- š§ Email/Password - Traditional username/password authentication with password reset
- ⨠Magic Links - Passwordless authentication via email
- š± OTP - One-time passwords via email or SMS
- š WebAuthn/Passkeys - Biometric and hardware key authentication
- š”ļø Multi-Factor Auth (MFA) - TOTP authenticator app support
- š Social Login - Google, GitHub, Microsoft shortcuts
- š² Device Enrollment - QR code enrollment for authenticator apps (v0.5.0+)
- š§ Device Management - List and manage authenticator devices (v0.5.0+)
Developer Experience
- ā” All-in-One - 12 authentication methods in one package
- š Cross-Portal SSO - Enable seamless SSO across Zewst apps (v0.7.0+)
- šÆ Type-Safe - Full TypeScript support with excellent IntelliSense
- š¦ Lightweight - Only 51KB (15KB gzipped) with tree-shaking
- š Zero Dependencies - No external packages required
- š 100% Compatible - Works with existing v0.1.x code
- š± Next.js 14/15 - Full App Router and Pages Router support
- šØ Easy to Use - Simple, intuitive API
š Installation
npm install @zewstid/nextjs next-auth
š Quick Start
1. Get Your Credentials
Sign up at developers.zewstid.com and create a new application to get your:
- Client ID
- Client Secret (optional for public clients)
2. Environment Variables
Create a
.env.local# Required NEXT_PUBLIC_ZEWSTID_CLIENT_ID=your_client_id ZEWSTID_CLIENT_SECRET=your_client_secret # NextAuth (for OAuth) NEXTAUTH_SECRET=your_random_secret_min_32_chars NEXTAUTH_URL=http://localhost:3000 # Optional ZEWSTID_API_URL=https://api.zewstid.com/api/v1
3. Create Auth Configuration
App Router (
app/api/auth/[...nextauth]/route.tsimport { createZewstIDAuth } from '@zewstid/nextjs'; import NextAuth from 'next-auth'; export const authOptions = createZewstIDAuth({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, clientSecret: process.env.ZEWSTID_CLIENT_SECRET!, }); const handler = NextAuth(authOptions); export { handler as GET, handler as POST };
4. Wrap Your App
App Router (
app/layout.tsximport { ZewstIDProvider } from '@zewstid/nextjs'; export default function RootLayout({ children }) { return ( <html> <body> <ZewstIDProvider> {children} </ZewstIDProvider> </body> </html> ); }
5. Use Authentication
'use client'; import { useZewstID } from '@zewstid/nextjs'; export function AuthDemo() { const { user, signIn, signOut, signInWithPassword, sendMagicLink, sendOTP, verifyOTP, webauthn, } = useZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, }); if (!user) { return ( <div> {/* OAuth */} <button onClick={() => signIn()}> Sign in with ZewstID </button> {/* Password */} <button onClick={() => signInWithPassword('user@example.com', 'pass')}> Sign in with Password </button> {/* Magic Link */} <button onClick={() => sendMagicLink('user@example.com')}> Send Magic Link </button> {/* OTP */} <button onClick={() => sendOTP('user@example.com')}> Send OTP Code </button> {/* WebAuthn */} <button onClick={() => webauthn.authenticate()}> š Use Touch ID </button> </div> ); } return ( <div> <p>Welcome {user.name}!</p> <button onClick={() => signOut()}>Sign Out</button> </div> ); }
š Cross-Portal SSO (v0.7.0+)
For internal Zewst ecosystem applications that need to share authentication sessions across portals (User Portal, Developer Portal, Admin Dashboard, etc.), enable SSO establishment:
Setup
Create
app/api/zewstid/[...zewstid]/route.tsimport { createZewstIDHandlers } from '@zewstid/nextjs/handlers'; const handlers = createZewstIDHandlers({ clientId: process.env.ZEWSTID_CLIENT_ID!, clientSecret: process.env.ZEWSTID_CLIENT_SECRET!, baseUrl: process.env.NEXTAUTH_URL, apiUrl: process.env.ZEWSTID_API_URL || 'https://api.zewstid.com', issuerUrl: process.env.ZEWSTID_ISSUER_URL || 'https://auth.zewstid.com/realms/zewstid', enableSSOEstablishment: true, // Enable cross-portal SSO }); export const GET = handlers.GET; export const POST = handlers.POST;
How It Works
When
enableSSOEstablishment: true- User authenticates via password/OTP/magic-link
- SDK validates credentials via API Gateway (ROPC flow)
- On success, SDK requests an SSO token
- User is silently redirected to ZewstID with the token
- ZewstID validates and creates a browser session
- User is redirected back with OAuth code
- NextAuth exchanges code for session
Result: User can navigate to any Zewst portal and be automatically authenticated.
When to Use
| Scenario | Use SSO | Why |
|---|---|---|
| Internal Zewst app | Yes | Cross-portal SSO needed |
| Third-party integration | No | Standard OAuth is simpler |
| Partner app | No | Don't need Zewst SSO |
| Testing/development | Optional | Depends on requirements |
Available Handlers with SSO
The
createZewstIDHandlers()| Endpoint | Method | Description |
|---|---|---|
/api/zewstid/check-email | GET/POST | Check if user exists |
/api/zewstid/methods | POST | Get available auth methods |
/api/zewstid/password | POST | Password authentication |
/api/zewstid/otp/send | POST | Send OTP code |
/api/zewstid/otp/verify | POST | Verify OTP code |
/api/zewstid/magic-link/send | POST | Send magic link |
/api/zewstid/magic-link/verify | POST | Verify magic link |
/api/zewstid/mfa/verify | POST | Verify MFA/TOTP code |
/api/zewstid/establish-sso | GET | Establish ZewstID SSO |
/api/zewstid/sso-callback | GET | Handle SSO callback |
ā” Best Practices
Backend Authentication with Tokens
Always use ID tokens for backend authentication, not access tokens:
// ā CORRECT - Use ID token for your backend const response = await fetch('/api/protected', { headers: { 'Content-Type': 'application/json', Authorization: session.idToken ? `Bearer ${session.idToken}` : '', }, }); // Decode ID token in your backend const payload = JSON.parse( Buffer.from(session.idToken.split('.')[1], 'base64url').toString() ); console.log('User ID:', payload.sub); console.log('Email:', payload.email);
// ā WRONG - Don't use access token for your own backend const response = await fetch('/api/protected', { headers: { Authorization: session.accessToken ? `Bearer ${session.accessToken}` : '', }, }); // This may fail because access tokens can be opaque (not JWTs)
Why Use ID Tokens?
- ā ID tokens are always JWTs (decodable)
- ā ID tokens contain user claims (email, name, roles)
- ā ID tokens are designed for user authentication
- ā Access tokens may be opaque (not decodable)
- ā Access tokens are designed for calling external APIs
When to Use Each Token:
| Token Type | Use For | Example |
|---|---|---|
| ID Token | Authenticating to YOUR backend | Authorization: Bearer ${session.idToken} |
| Access Token | Calling EXTERNAL APIs | Calling Google APIs, GitHub API, etc. |
For more details, see: Token Type Troubleshooting
Session Management
Session contains both tokens (v0.4.6+):
const { data: session } = useSession(); console.log(session.idToken); // ā Always available (v0.4.6+) console.log(session.accessToken); // ā Available for external APIs console.log(session.user); // ā User information
Migration from v0.4.5: If upgrading from v0.4.5 or earlier, update your backend to use
session.idTokenš Authentication Methods
1ļøā£ OAuth/OIDC (Default)
Redirect users to ZewstID for authentication:
const { signIn, signOut, user } = useZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, }); // Sign in await signIn({ callbackUrl: '/dashboard' }); // Sign out await signOut({ callbackUrl: '/' });
2ļøā£ Email/Password
Traditional username/password authentication:
const { signInWithPassword, signUpWithPassword } = useZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, }); // Sign up const { userId } = await signUpWithPassword( 'user@example.com', 'SecurePass123!', { firstName: 'John', lastName: 'Doe', sendVerificationEmail: true, } ); // Sign in await signInWithPassword('user@example.com', 'SecurePass123!');
Password Reset Flow
Complete password reset with CAPTCHA protection:
const { forgotPassword, verifyResetToken, resetPassword } = useZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, }); // Step 1: Request password reset email await forgotPassword('user@example.com', { captchaToken: captchaToken, // From CAPTCHA component redirectUrl: '/reset-password', // Optional custom reset URL }); // Step 2: (Optional) Verify token validity before showing form const { valid, email, expiresIn } = await verifyResetToken(tokenFromURL); // Step 3: Reset password with token from email await resetPassword(tokenFromURL, 'NewSecurePass123!'); // All user sessions are automatically invalidated after reset
Security Features:
- ā Tokens expire in 1 hour
- ā Single-use tokens (can't be reused)
- ā Rate limiting: 3 requests/hour per email, 10/hour per IP
- ā CAPTCHA protection against bots
- ā All existing sessions invalidated after reset
- ā Email enumeration prevention
See complete example for full UI implementation with error handling.
3ļøā£ Magic Links
Passwordless email authentication:
const { sendMagicLink, verifyMagicLink } = useZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, }); // Send magic link (auto-creates user if doesn't exist) await sendMagicLink('user@example.com', { redirectUrl: '/dashboard', }); // For security-conscious apps: Only send to existing users await sendMagicLink('user@example.com', { createIfNotExists: false, // Returns 404 if user doesn't exist }); // Verify (in callback page) await verifyMagicLink(tokenFromURL);
4ļøā£ OTP Authentication
Email or SMS one-time passwords:
const { sendOTP, verifyOTP } = useZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, }); // Send OTP (auto-creates user if doesn't exist) const { expiresIn } = await sendOTP('user@example.com', { provider: 'email', // or 'sms' }); // For security-conscious apps: Only send to existing users await sendOTP('user@example.com', { provider: 'email', createIfNotExists: false, // Returns 404 if user doesn't exist }); // Verify OTP await verifyOTP('user@example.com', '123456');
5ļøā£ WebAuthn/Passkeys
Biometric and hardware key authentication:
const { webauthn } = useZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, }); // Register credential (requires existing session) await webauthn.register({ displayName: 'Touch ID', authenticatorAttachment: 'platform', }); // Authenticate await webauthn.authenticate('user@example.com'); // Manage credentials const credentials = await webauthn.listCredentials(); await webauthn.deleteCredential(credentialId);
6ļøā£ Multi-Factor Authentication
TOTP authenticator app support:
const { mfa } = useZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, }); // Enroll in MFA const { secret, qrCode } = await mfa.enrollTOTP({ displayName: 'My Authenticator', }); // Verify enrollment await mfa.verifyTOTPEnrollment('123456'); // Disable MFA await mfa.disable();
7ļøā£ Social Login
Quick shortcuts for popular providers:
const { signInWithGoogle, signInWithGitHub, signInWithMicrosoft } = useZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, }); await signInWithGoogle(); await signInWithGitHub(); await signInWithMicrosoft();
8ļøā£ Account Linking (v0.3.0+)
Link multiple authentication methods to a single account:
const { accountLinking } = useZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, }); // When user tries to sign in with a new method using existing email // The API returns 409 Conflict with linking suggestion // Step 1: Initiate linking const { linkingToken } = await accountLinking.initiate({ email: 'user@example.com', newProvider: 'google', // or 'magic_link', 'github', etc. }); // Step 2: Verify ownership (password OR email code) await accountLinking.verify({ linkingToken: linkingToken, password: 'user-password', // OR use code from email }); // Convenience methods await accountLinking.linkWithPassword('user@example.com', 'google', 'password'); await accountLinking.linkWithCode('user@example.com', 'google', '123456'); // Manage linked identities const { identities } = await accountLinking.getLinkedIdentities(); // Returns: ['password', 'google', 'github', 'magic_link'] await accountLinking.unlink('google'); // Remove authentication method const linked = await accountLinking.isLinked('github'); // Check if linked
Security Features:
- ā Ownership verification required (password OR email code)
- ā Cannot unlink last authentication method
- ā Rate limiting on linking endpoints
- ā Audit logging for security monitoring
- ā 10-minute token expiration
Use Cases:
- Password user wants to add Google login
- OAuth user wants to set a password
- Link magic links, OTP, WebAuthn to existing account
- Single identity across all authentication methods
9ļøā£ Device Enrollment (v0.5.0+)
Allow users to enroll authenticator devices by scanning QR codes:
const { useEnrollment } = require('@zewstid/nextjs'); function EnrollDevicePage() { const { startEnrollment, checkStatus, enrollmentSession, status, isLoading, error, } = useEnrollment({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, accessToken: session?.accessToken, }); const handleStartEnrollment = async () => { const session = await startEnrollment({ accountName: 'My Work Account', iconUrl: 'https://myapp.com/icon.png', // Optional }); // Display session.qrCode to user console.log('Enrollment ID:', session.enrollmentId); }; // Poll for completion useEffect(() => { if (enrollmentSession) { const interval = setInterval(async () => { const currentStatus = await checkStatus(enrollmentSession.enrollmentId); if (currentStatus.status === 'completed') { clearInterval(interval); alert('Device enrolled successfully!'); } }, 2000); return () => clearInterval(interval); } }, [enrollmentSession]); return ( <div> <button onClick={handleStartEnrollment} disabled={isLoading}> {isLoading ? 'Starting...' : 'Enroll New Device'} </button> {enrollmentSession && ( <div> <h3>Scan this QR code with your authenticator app</h3> <img src={enrollmentSession.qrCode} alt="Enrollment QR Code" /> <p>Waiting for device to scan...</p> </div> )} {error && <p className="error">{error}</p>} </div> ); }
š Authenticator Device Management (v0.5.0+)
List and manage enrolled authenticator devices:
const { useAuthenticatorDevices } = require('@zewstid/nextjs'); function DevicesPage() { const { devices, isLoading, error, fetchDevices, removeDevice, isRemoving, } = useAuthenticatorDevices({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, accessToken: session?.accessToken, }); const handleRemove = async (deviceId: string) => { if (confirm('Remove this device?')) { await removeDevice(deviceId); } }; if (isLoading) return <p>Loading devices...</p>; return ( <div> <h2>Your Authenticator Devices</h2> {devices.length === 0 ? ( <p>No devices enrolled. Add one to enable push authentication.</p> ) : ( <ul> {devices.map((device) => ( <li key={device.id}> <span>{device.name}</span> <span>{device.platform} - {device.model}</span> <span>Last active: {device.lastActive}</span> <button onClick={() => handleRemove(device.id)} disabled={isRemoving} > Remove </button> </li> ))} </ul> )} <button onClick={fetchDevices}>Refresh</button> </div> ); }
1ļøā£1ļøā£ TOTP Setup (v0.5.0+)
Set up TOTP for authenticator apps:
const { useTOTPSetup } = require('@zewstid/nextjs'); function TOTPSetupPage() { const { setupTOTP, verifyTOTP, removeTOTP, regenerateBackupCodes, config, isConfigured, isLoading, error, } = useTOTPSetup({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, accessToken: session?.accessToken, accountId: user?.id, }); const [code, setCode] = useState(''); const handleSetup = async () => { const result = await setupTOTP({ accountName: user?.email || 'My Account', }); // Display QR code and backup codes to user console.log('Scan this QR:', result.qrCode); console.log('Backup codes:', result.backupCodes); }; const handleVerify = async () => { const result = await verifyTOTP(code); if (result.success) { alert('TOTP verified and enabled!'); } }; return ( <div> {!isConfigured ? ( <> <button onClick={handleSetup} disabled={isLoading}> Set Up Authenticator App </button> {config?.qrCode && ( <div> <img src={config.qrCode} alt="TOTP QR Code" /> <p>Manual entry: {config.secret}</p> <h4>Backup Codes (save these!)</h4> <ul> {config.backupCodes?.map((code, i) => ( <li key={i}><code>{code}</code></li> ))} </ul> <input type="text" value={code} onChange={(e) => setCode(e.target.value)} placeholder="Enter 6-digit code" maxLength={6} /> <button onClick={handleVerify}>Verify & Enable</button> </div> )} </> ) : ( <div> <p>ā TOTP is enabled</p> <button onClick={regenerateBackupCodes}> Regenerate Backup Codes </button> <button onClick={removeTOTP} className="danger"> Disable TOTP </button> </div> )} {error && <p className="error">{error}</p>} </div> ); }
1ļøā£2ļøā£ OIDC Logout (v0.4.2+)
Proper OpenID Connect logout that clears both app and SSO sessions:
Create logout endpoint (
app/api/auth/logout/route.tsimport { createLogoutHandler } from '@zewstid/nextjs/handlers'; import { authOptions } from '@/lib/auth'; export const GET = createLogoutHandler(authOptions, { postLogoutRedirectUri: 'https://myapp.com', // Optional });
Update sign-out buttons:
'use client'; export function SignOutButton() { const handleSignOut = () => { // Use logout endpoint instead of signOut() window.location.href = '/api/auth/logout'; }; return <button onClick={handleSignOut}>Sign Out</button>; }
Optional: Post-logout cleanup handler (
app/api/auth/post-logout/route.tsimport { createPostLogoutHandler } from '@zewstid/nextjs/handlers'; export const GET = createPostLogoutHandler({ redirectTo: '/', // Redirect after cleanup });
What this fixes:
- ā Before v0.4.2: Users were auto-logged back in after sign out
- ā After v0.4.2: Proper OIDC logout clears SSO cookies
- Users must re-enter credentials after logout
- Follows OpenID Connect RP-Initiated Logout spec
šÆ API Reference
Client Hooks
useZewstID(config?)
useZewstID(config?)Main authentication hook with all methods:
const { // State user, // Current user session, // Full session with tokens isAuthenticated, // Boolean auth status isLoading, // Boolean loading state error, // Any auth errors // OAuth signIn, // Sign in with OAuth signOut, // Sign out refreshToken, // Refresh access token // Password signInWithPassword, signUpWithPassword, forgotPassword, // Request password reset email verifyResetToken, // Verify reset token validity resetPassword, // Reset password with token // Magic Link sendMagicLink, verifyMagicLink, // OTP sendOTP, verifyOTP, // WebAuthn webauthn: { register, authenticate, listCredentials, deleteCredential, }, // MFA mfa: { enrollTOTP, verifyTOTPEnrollment, disable, }, // Social signInWithGoogle, signInWithGitHub, signInWithMicrosoft, // Account Linking (v0.3.0+) accountLinking: { initiate, verify, getLinkedIdentities, unlink, isLinked, linkWithPassword, linkWithCode, }, } = useZewstID({ clientId: 'your_client_id', apiUrl: 'https://api.zewstid.com/api/v1', // optional });
useEnrollment(config) (v0.5.0+)
useEnrollment(config)Hook for device enrollment via QR code:
const { startEnrollment, // Start new enrollment session checkStatus, // Check enrollment status enrollmentSession, // Current session (qrCode, enrollmentId, expiresAt) status, // 'pending' | 'completed' | 'expired' isLoading, error, } = useEnrollment({ clientId: 'your_client_id', accessToken: session?.accessToken, });
useAuthenticatorDevices(config) (v0.5.0+)
useAuthenticatorDevices(config)Hook for managing enrolled authenticator devices:
const { devices, // Array of AuthenticatorDevice isLoading, error, fetchDevices, // Refresh device list removeDevice, // Remove a device by ID isRemoving, } = useAuthenticatorDevices({ clientId: 'your_client_id', accessToken: session?.accessToken, });
useTOTPSetup(config) (v0.5.0+)
useTOTPSetup(config)Hook for TOTP authenticator setup and management:
const { setupTOTP, // Start TOTP setup (returns qrCode, secret, backupCodes) verifyTOTP, // Verify code and enable TOTP removeTOTP, // Disable TOTP regenerateBackupCodes, // Get new backup codes config, // Current TOTP config (if enabled) isConfigured, // Whether TOTP is already set up isLoading, error, } = useTOTPSetup({ clientId: 'your_client_id', accessToken: session?.accessToken, accountId: user?.id, });
useUser()
useUser()Shorthand to get current user:
const user = useUser();
useAuth()
useAuth()Get authentication status:
const { isAuthenticated, isLoading, error } = useAuth();
useAccessToken()
useAccessToken()Get access token for API calls:
const accessToken = useAccessToken();
Server-Side Client
For server-side or API route usage:
import { createZewstID } from '@zewstid/nextjs'; const client = createZewstID({ clientId: process.env.ZEWSTID_CLIENT_ID!, clientSecret: process.env.ZEWSTID_CLIENT_SECRET!, }); // Use any authentication method const session = await client.signInWithPassword(email, password); await client.sendMagicLink(email); const { expiresIn } = await client.sendOTP(email);
Logout Handlers (v0.4.2+)
For OIDC-compliant logout in API routes:
import { createLogoutHandler, createPostLogoutHandler } from '@zewstid/nextjs/handlers'; import { authOptions } from '@/lib/auth'; // Main logout handler (required) export const GET = createLogoutHandler(authOptions, { postLogoutRedirectUri: 'https://myapp.com', }); // Optional post-logout cleanup export const POST = createPostLogoutHandler({ redirectTo: '/', onLogout: async (session) => { // Custom cleanup logic console.log('User logged out:', session.user.email); }, });
š¦ TypeScript
Full TypeScript support included:
import type { ZewstIDUser, ZewstIDSession, ZewstIDConfig, UseZewstIDReturn, PasswordSignInOptions, PasswordSignUpOptions, MagicLinkSendOptions, OTPSendOptions, WebAuthnRegisterOptions, WebAuthnCredential, TOTPEnrollOptions, // Account Linking Types (v0.3.0+) LinkAccountOptions, VerifyLinkingOptions, LinkingInitiateResponse, LinkingVerifyResponse, LinkedIdentitiesResponse, UnlinkResponse, // Logout Handler Types (v0.4.2+) LogoutHandlerOptions, PostLogoutHandlerOptions, // Device Enrollment Types (v0.5.0+) EnrollmentSession, EnrollmentStatus, StartEnrollmentOptions, AuthenticatorDevice, // TOTP Setup Types (v0.5.0+) TOTPSetupOptions, TOTPSetupResponse, TOTPVerifyOptions, TOTPVerifyResponse, TOTPConfigResponse, BackupCodesResponse, } from '@zewstid/nextjs';
š Migration from v0.1.9
No breaking changes! Your existing code continues to work:
// v0.1.9 code - still works! ā const { user, signIn, signOut } = useZewstID();
To use new authentication methods, just pass the config:
// v0.2.0 - enhanced with new methods const { user, signIn, signOut, signInWithPassword, // NEW sendMagicLink, // NEW webauthn, // NEW } = useZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, });
š Bundle Size
| Package | Size (Uncompressed) | Size (Gzipped) |
|---|---|---|
| Main | 51 KB | ~15 KB |
| Handlers | 4.5 KB | ~2 KB |
| Middleware | 7.6 KB | ~3 KB |
| Server | 11 KB | ~4 KB |
Total: ~24 KB gzipped for full package with all features.
With tree-shaking, apps using only OAuth will be ~8-10 KB gzipped.
š”ļø Security
Built-in Security Features
- ā Secure token storage (httpOnly cookies via NextAuth)
- ā CSRF protection built-in
- ā PKCE for OAuth flows
- ā Proper session management
- ā WebAuthn with public key cryptography
- ā TOTP for multi-factor authentication
- ā Email verification required for all new users
- ā Rate limiting (IP-based and per-email)
User Creation Control
By default, passwordless authentication methods (magic links, OTP) auto-create users for convenience. This matches industry standards (Supabase, Auth0, Clerk).
For security-conscious applications, you can require explicit registration:
// Only send magic links to existing users await sendMagicLink('user@example.com', { createIfNotExists: false // Returns 404 if user doesn't exist }); // Only send OTP to existing users await sendOTP('user@example.com', { createIfNotExists: false });
Best Practice: Use auto-creation (
createIfNotExists: true// 1. User requests magic link (auto-creates account) await sendMagicLink('user@example.com'); // 2. User clicks link, email is verified // 3. Require email verification before allowing app access if (!user.emailVerified) { return redirect('/verify-email'); }
Rate Limiting
ZewstID includes multi-layer rate limiting to prevent abuse:
- IP-based: 10 requests per 15 minutes per IP
- Email-based: 5 magic links/OTPs per hour per email
- Strict limits: 5 requests per 15 minutes for sensitive operations
This prevents:
- ā Email bombing
- ā Account enumeration
- ā Brute force attacks
- ā Denial of service
š Examples
Complete Login Page
'use client'; import { useZewstID } from '@zewstid/nextjs'; import { useState } from 'react'; export default function LoginPage() { const { signIn, signInWithPassword, sendMagicLink, signInWithGoogle, } = useZewstID({ clientId: process.env.NEXT_PUBLIC_ZEWSTID_CLIENT_ID!, }); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); return ( <div> <h1>Sign In</h1> {/* OAuth */} <button onClick={() => signIn()}> Sign in with ZewstID </button> {/* Social */} <button onClick={signInWithGoogle}> Sign in with Google </button> {/* Password */} <form onSubmit={(e) => { e.preventDefault(); signInWithPassword(email, password); }}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" /> <button type="submit">Sign In with Password</button> </form> {/* Magic Link */} <button onClick={() => sendMagicLink(email)}> Send Magic Link </button> </div> ); }
Protected Server Component
import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { redirect } from 'next/navigation'; export default async function DashboardPage() { const session = await getServerSession(authOptions); if (!session) { redirect('/login'); } return ( <div> <h1>Welcome {session.user.name}!</h1> <p>Email: {session.user.email}</p> <p>Roles: {session.user.roles?.join(', ')}</p> </div> ); }
š Documentation
š Support
- š Documentation
- š¬ Discord Community
- š GitHub Issues
- š§ Email Support
š License
Proprietary and Confidential
Copyright Ā© 2025 Zewst, Inc. All Rights Reserved.
This software is proprietary and confidential. Unauthorized distribution or use is prohibited.
š Credits
Built with:
- NextAuth.js - OAuth/OIDC foundation
- Next.js - React framework
- TypeScript - Type safety
Made with ā¤ļø by the Zewst Team
Was this page helpful?
Let us know how we can improve our documentation