Skip to main content

Overview

Opinix Trade uses a secure phone-based authentication system powered by Twilio SMS OTP and NextAuth session management. This ensures only verified users can access the platform while maintaining a seamless user experience.
All authentication is handled server-side with industry-standard security practices including JWT tokens and secure session cookies.

Authentication flow

Here’s how the complete authentication process works:
1

User enters phone number

You provide your phone number in the format +91-XXXXXXXXXX on the sign-in page.
2

OTP generation

The server generates a secure 6-digit OTP and stores it in the database:
const OTP = Math.floor(100000 + Math.random() * 9000).toString();

await prisma.oTP.create({
  data: {
    otpID: phoneNumber,
    otp: OTP,
    expiresAt: new Date(Date.now() + 10 * 60 * 1000) // 10 minutes
  }
});
3

SMS delivery

Twilio sends the OTP to your phone:
await twilioClient.messages.create({
  body: `Your OTP for OpiniX is ${OTP}, do not share it with anyone!`,
  from: process.env.TWILIO_NUMBER,
  to: phoneNumber
});
4

OTP verification

You enter the OTP within 10 minutes. The server validates:
const otpData = await prisma.oTP.findUnique({
  where: { otpID: phoneNumber, otp }
});

if (!otpData) return { verified: false, message: "OTP not found" };
if (otpData.otp !== otp) return { verified: false, message: "Invalid OTP" };
if (otpData.expiresAt <= new Date()) {
  return { verified: false, message: "OTP expired" };
}
5

Account creation/lookup

After successful OTP verification:
  • If user exists: Retrieve account details
  • If new user: Create account automatically with initial balance of ₹0
const user = await prisma.user.create({
  data: {
    phoneNumber: credentials?.phoneNumber,
    role: "USER",
    balance: 0.0
  }
});
6

Session creation

NextAuth creates a secure session with JWT token containing:
  • User ID
  • Phone number
  • Verification status
  • Current balance

NextAuth configuration

Opinix uses NextAuth with a custom Credentials provider for phone authentication.

Provider setup

import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaAdapter } from "@next-auth/prisma-adapter";

export const authOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        phoneNumber: {
          label: "Phone",
          type: "text",
          placeholder: "+91-98765*****"
        }
      },
      async authorize(credentials) {
        const isUserExists = await prisma.user.findFirst({
          where: { phoneNumber: credentials?.phoneNumber },
          include: {
            OTP: {
              select: { isVerified: true }
            }
          }
        });
        
        const isUserVerified = isUserExists?.OTP[0].isVerified;
        
        if (isUserExists) {
          return {
            id: isUserExists?.id,
            phoneNumber: isUserExists?.phoneNumber,
            balance: isUserExists?.balance,
            role: isUserExists.role,
            isVerified: isUserVerified
          };
        }
        
        // Create new user if verified
        const user = await prisma.user.create({
          data: {
            phoneNumber: credentials?.phoneNumber,
            role: "USER",
            balance: 0.0
          }
        });
        
        return { ...user, isVerified: true };
      }
    })
  ],
  session: {
    strategy: "jwt"
  },
  secret: process.env.NEXTAUTH_SECRET
};

Session callbacks

NextAuth extends the session with custom user data:
async jwt({ token, user }) {
  if (user) {
    token.phoneNumber = user.phoneNumber;
    token.isVerified = user.isVerified;
    token.id = user.id;
    token.balance = user.balance;
  }
  return token;
}

OTP management

Sending OTP

The OTP service handles generation, storage, and delivery:
export const sendSMSOTP = async (phoneNumber: string) => {
  if (phoneNumber.length <= 10) {
    return { success: false, message: "Invalid phone number" };
  }
  
  try {
    const OTP = Math.floor(100000 + Math.random() * 9000).toString();
    
    // Check if OTP already exists
    const isOtpDataExists = await prisma.oTP.findUnique({
      where: { otpID: phoneNumber }
    });
    
    if (isOtpDataExists) {
      // Update existing OTP
      await prisma.oTP.update({
        where: { otpID: phoneNumber },
        data: { 
          otp: OTP, 
          expiresAt: new Date(Date.now() + 10 * 60 * 1000) 
        }
      });
    } else {
      // Create new OTP
      await prisma.oTP.create({
        data: {
          otpID: phoneNumber,
          otp: OTP,
          expiresAt: new Date(Date.now() + 10 * 60 * 1000)
        }
      });
    }
    
    // Send via Twilio
    await twilioClient.messages.create({
      body: `Your OTP for OpiniX is ${OTP}, do not share it with anyone!`,
      from: process.env.TWILIO_NUMBER,
      to: phoneNumber
    });
    
    return { success: true, message: "OTP sent" };
  } catch (error) {
    console.error("Error in sendSMSOTP:", error);
    return { success: false, message: "Failed to send OTP" };
  }
};

Validating OTP

export const verifySMSOTPAction = async (otp: string, phoneNumber: string) => {
  try {
    const otpData = await prisma.oTP.findUnique({
      where: { otpID: phoneNumber, otp }
    });
    
    // Check if already verified
    if (otpData?.isVerified) {
      return { verified: true, message: "User already exists" };
    }
    
    // Validate OTP
    if (!otpData) {
      return { verified: false, message: "OTP not found" };
    }
    
    if (otpData.otp !== otp) {
      return { verified: false, message: "Invalid OTP" };
    }
    
    // Check expiration
    if (otpData.expiresAt <= new Date()) {
      await prisma.oTP.delete({
        where: { otpID: phoneNumber, otp }
      });
      return { verified: false, message: "OTP expired" };
    }
    
    // Mark as verified
    await prisma.oTP.update({
      where: { otpID: phoneNumber, otp },
      data: { isVerified: true }
    });
    
    return { verified: true, message: "OTP verified successfully" };
  } catch (error) {
    console.error("Error verifying OTP:", error);
    return { verified: false, message: "An error occurred" };
  }
};

Session management

Accessing session data

In client components, use the useSession hook:
import { useSession } from 'next-auth/react';

function MyComponent() {
  const { data: session, status } = useSession();
  
  if (status === "loading") {
    return <div>Loading...</div>;
  }
  
  if (status === "unauthenticated") {
    return <div>Please sign in</div>;
  }
  
  return (
    <div>
      <p>User ID: {session.user.id}</p>
      <p>Phone: {session.user.phoneNumber}</p>
      <p>Balance: ₹{session.user.balance}</p>
    </div>
  );
}
In server components and API routes:
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";

export async function GET(req: Request) {
  const session = await getServerSession(authOptions);
  
  if (!session) {
    return new Response("Unauthorized", { status: 401 });
  }
  
  // Access user data
  const userId = session.user.id;
  const balance = session.user.balance;
  
  // ...
}

Session expiration

Sessions use JWT with the following behavior:
  • Default expiration: 30 days
  • Automatic refresh: Sessions refresh on each request
  • Logout: Clear session cookie
import { signOut } from 'next-auth/react';

// Sign out user
await signOut({ callbackUrl: '/' });

Security best practices

  • OTPs are single-use (marked verified after use)
  • 10-minute expiration window
  • Stored securely in database, never logged
  • Sent only via encrypted SMS channel
  • Never included in client-side code
  • JWT tokens signed with NEXTAUTH_SECRET
  • HttpOnly cookies prevent XSS attacks
  • Secure flag enabled in production
  • SameSite=Lax prevents CSRF
  • Sessions invalidated on password change
  • Phone numbers validated before OTP generation
  • Rate limiting on OTP requests (implementation recommended)
  • Database indexes prevent duplicate accounts
  • OTP attempts limited (implementation recommended)
Required secrets in .env:
NEXTAUTH_SECRET=your-secret-key
NEXTAUTH_URL=https://yourdomain.com
TWILIO_ACCOUNT_SID=your-twilio-sid
TWILIO_AUTH_TOKEN=your-twilio-token
TWILIO_NUMBER=+1234567890
Never commit these to version control!

Type definitions

Opinix extends NextAuth types for TypeScript support:
declare module "next-auth" {
  interface User {
    id?: string;
    phoneNumber?: string;
    isVerified?: boolean;
    balance?: number;
  }
  
  interface Session {
    user: DefaultSession["user"] & {
      id?: string;
      phoneNumber?: string;
      isVerified?: boolean;
      balance?: number;
    };
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    id?: string;
    phoneNumber?: string;
    isVerified?: boolean;
    balance?: number;
  }
}

Protected routes

Protect pages that require authentication:
import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';

export default async function ProtectedPage() {
  const session = await getServerSession(authOptions);
  
  if (!session) {
    redirect('/api/auth/signin');
  }
  
  return <div>Protected content</div>;
}
For client-side protection:
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

function ProtectedComponent() {
  const { data: session, status } = useSession();
  const router = useRouter();
  
  useEffect(() => {
    if (status === 'unauthenticated') {
      router.push('/api/auth/signin');
    }
  }, [status, router]);
  
  if (status === 'loading') {
    return <div>Loading...</div>;
  }
  
  return <div>Protected content</div>;
}

API route protection

Protect API routes from unauthorized access:
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";

export async function POST(req: NextRequest) {
  const session = await getServerSession(authOptions);
  
  if (!session?.user?.id) {
    return NextResponse.json(
      { error: "Unauthorized" },
      { status: 401 }
    );
  }
  
  // Process authenticated request
  const userId = session.user.id;
  // ...
}
Always verify sessions server-side. Client-side session checks can be bypassed and should only be used for UI purposes.

Troubleshooting

Common causes:
  • Phone number format incorrect (must include +91)
  • Twilio credentials invalid
  • Phone number not verified in Twilio (test mode)
  • SMS delivery delay (wait up to 2 minutes)
Check server logs for Twilio errors.
Verify:
  • NEXTAUTH_SECRET is set in environment
  • NEXTAUTH_URL matches your domain
  • Cookies are enabled in browser
  • Not using incognito/private mode
  • No conflicting middleware blocking cookies
Reasons:
  • OTP expired (10 minute limit)
  • Typo in entered OTP
  • OTP already used (single-use)
  • Database sync issue
Request a new OTP to resolve.

Next steps