Overview
MicroCBM uses a secure, token-based authentication system with email-based OTP (One-Time Password) verification. All authentication flows are protected by JWT tokens stored in HTTP-only cookies.
Login requires OTP verification via email. To complete end-to-end login, users must have access to their email inbox to receive the 6-digit verification code.
Authentication Flow
Login Process
The login flow consists of two steps:
Credential submission - User enters email and password
OTP verification - User enters 6-digit code sent to email
Step 1: Login Service
From src/app/actions/auth.ts:
export async function loginService ( payload : {
email : string ;
password : string ;
}) : Promise < ApiResponse > {
return handleApiRequest ( ` ${ commonEndpoint } /login` , payload );
}
Used in the login form:
src/app/auth/components/Login.tsx
const onSubmit = async ( data : FormData ) => {
const { email , password } = data ;
setErrorMessage ( "" );
const response = await loginService ({
email ,
password ,
});
if ( response . success ) {
toast . success ( "OTP sent" , { description: ` ${ response . data ?. message } ` });
setUserEmail ( email );
setStep ( "otp" );
} else {
setErrorMessage ( response . message || "Login failed. Please try again." );
}
};
Step 2: OTP Verification
After receiving the OTP code via email:
export async function verifyOTPService ( formData : {
email : string ;
otp : string ;
}) : Promise < ApiResponse > {
const response = await handleApiRequest ( ` ${ commonEndpoint } /verify-otp` , {
email: formData . email ,
otp: formData . otp ,
});
if ( response ?. success ) {
const token =
response . data ?. data ?. token ||
response . data ?. data ?. access_token ||
response . data ?. token ||
response . data ?. access_token ;
if ( token && typeof token === "string" ) {
await createUserSession ( token );
} else {
console . error ( "No token found in response:" , response . data );
}
}
return response ;
}
Used in the OTP verification component:
src/app/auth/components/OTPVerification.tsx
const onSubmit = async ( data : FormData ) => {
setErrorMessage ( "" );
setIsLoading ( true );
try {
const response = await verifyOTPService ({
email ,
otp: data . otp ,
});
if ( response . success ) {
toast . success ( "OTP verified" , {
description: response . data ?. message ,
});
router . push ( ROUTES . HOME );
} else {
toast . error (
response . message || "OTP verification failed. Please try again."
);
}
} finally {
setIsLoading ( false );
}
};
The OTP input auto-submits when the user enters all 6 digits, providing a seamless experience.
JWT Token Management
Token Structure
JWT tokens contain the following payload:
export interface JWTPayload {
user_id : string ;
email : string ;
role : string ;
role_id : string ;
permissions : string []; // Array of permission strings
org_id : string ; // Organization ID
exp : number ; // Expiration timestamp
iat : number ; // Issued at timestamp
}
Token Utilities
The JWT library provides helper functions:
import { decodeJwt } from "jose" ;
export function decodeJWT ( token : string ) : JWTPayload | null {
try {
const decoded = decodeJwt ( token );
return decoded as unknown as JWTPayload ;
} catch ( error ) {
console . error ( "Error decoding JWT:" , error );
return null ;
}
}
export function isTokenExpired ( token : string ) : boolean {
const payload = decodeJWT ( token );
if ( ! payload ) return true ;
const currentTime = Math . floor ( Date . now () / 1000 );
return payload . exp < currentTime ;
}
export function getTokenExpirationTime ( token : string ) : Date | null {
const payload = decodeJWT ( token );
if ( ! payload ) return null ;
return new Date ( payload . exp * 1000 );
}
Session Management
Creating Sessions
When OTP verification succeeds, a session is created:
"use server" ;
export async function createUserSession ( token : string ) {
const cookieStore = await cookies ();
// Decode token to get expiration time
const tokenExpiration = getTokenExpirationTime ( token );
if ( ! tokenExpiration ) {
throw new Error ( "Invalid token format" );
}
// Set cookie expiration to match token expiration (24 hours)
const expiresAt = tokenExpiration ;
cookieStore . set ( "token" , token , {
httpOnly: true ,
secure: process . env . NODE_ENV === "production" ,
expires: expiresAt ,
sameSite: "lax" ,
});
// Also store user data in a separate cookie for easy access
const userData = decodeJWT ( token );
if ( userData ) {
cookieStore . set ( "userData" , JSON . stringify ( userData ), {
httpOnly: true ,
secure: process . env . NODE_ENV === "production" ,
expires: expiresAt ,
sameSite: "lax" ,
});
}
}
Two cookies are created:
token - The raw JWT token for API authentication
userData - Decoded user data for quick access (role, permissions, etc.)
Both are HTTP-only to prevent XSS attacks.
Retrieving Sessions
export async function getUserSession () {
const cookieStore = await cookies ();
const token = cookieStore . get ( "token" )?. value ;
if ( ! token ) return null ;
// Check if token is expired
if ( isTokenExpired ( token )) {
await logoutUserSession ();
return null ;
}
// Get user data from cookie
const userDataCookie = cookieStore . get ( "userData" )?. value ;
let userData : SessionUser | null = null ;
if ( userDataCookie ) {
try {
userData = JSON . parse ( userDataCookie );
} catch ( error ) {
console . error ( "Error parsing user data:" , error );
}
}
// If user data is not available, decode from token
if ( ! userData ) {
userData = decodeJWT ( token );
}
return {
token ,
user: userData ,
};
}
export async function getCurrentUser () : Promise < SessionUser | null > {
const session = await getUserSession ();
return session ?. user || null ;
}
Logging Out
export async function logoutUserSession () {
const cookieStore = await cookies ();
cookieStore . delete ( "token" );
cookieStore . delete ( "userData" );
redirect ( "/auth/login" );
}
Middleware Protection
All routes are protected by Next.js middleware that runs before every request:
const publicPaths = new Set ([
ROUTES . AUTH . LOGIN ,
ROUTES . AUTH . REGISTER ,
ROUTES . AUTH . RESET_PASSWORD ,
]);
function isPublicPath ( pathname : string ) : boolean {
return publicPaths . has ( pathname ) || pathname . startsWith ( "/auth/" );
}
export default async function middleware ( req : NextRequest ) {
const { pathname } = req . nextUrl ;
const token = req . cookies . get ( "token" )?. value ;
const isPublic = isPublicPath ( pathname );
// If token is expired, clear cookies and redirect to login
if ( token && isTokenExpired ( token )) {
const response = isPublic
? NextResponse . next ()
: NextResponse . redirect ( new URL ( ROUTES . AUTH . LOGIN , req . nextUrl ));
response . cookies . delete ( "token" );
response . cookies . delete ( "userData" );
return response ;
}
// Redirect to login if accessing protected route without token
if ( ! isPublic && ! token ) {
return NextResponse . redirect ( new URL ( ROUTES . AUTH . LOGIN , req . nextUrl ));
}
// Redirect to home if accessing auth pages with valid token
if ( isPublic && token ) {
return NextResponse . redirect ( new URL ( ROUTES . HOME , req . nextUrl ));
}
return NextResponse . next ();
}
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|assets|favicon.ico|robots.txt|sitemap.xml|manifest.webmanifest).*)" ,
],
};
Middleware Features
Automatic token validation on every request
Cookie cleanup when tokens expire
Smart redirects based on authentication state
Static asset exclusion for performance
The middleware runs on EVERY request. Keep it lightweight to avoid performance issues.
Password Reset Flow
Similar to login, password reset uses OTP verification:
Step 1: Request Reset
export async function requestPasswordResetService (
email : string
) : Promise < ApiResponse > {
return handleApiRequest ( ` ${ commonEndpoint } /request-password-reset` , {
email: email ,
});
}
Step 2: Verify Reset OTP
export async function verifyPasswordResetOTPService (
email : string ,
otp : string
) : Promise < ApiResponse > {
return handleApiRequest ( ` ${ commonEndpoint } /verify-reset-otp` , {
email: email ,
otp: otp ,
});
}
Step 3: Reset Password
export async function resetPasswordService ( payload : {
email : string ;
password : string ;
}) : Promise < ApiResponse > {
return handleApiRequest ( ` ${ commonEndpoint } /reset-password` , payload );
}
User Registration
New users register with organization details:
export async function signupService ( formData : {
user : {
first_name : string ;
last_name : string ;
email : string ;
};
organization : {
name : string ;
industry : string ;
team_strength : string ;
logo_url : string ;
};
password : string ;
}) : Promise < ApiResponse > {
return handleApiRequest ( ` ${ commonEndpoint } /signup` , {
user: formData . user ,
organization: formData . organization ,
password: formData . password ,
});
}
After registration, users must verify their email with an OTP code before they can log in.
Permission-Based Access
Permissions are embedded in the JWT token and checked throughout the application:
Server-Side Checks
const currentUser = await getCurrentUser ();
if ( ! currentUser ?. permissions . includes ( "assets:write" )) {
return { success: false , message: "Unauthorized" };
}
Client-Side Guards
< ComponentGuard
permissions = "dashboard:read"
unauthorizedFallback = {<div>Access Denied</div>}
>
< DashboardContent />
</ ComponentGuard >
Common Permissions
dashboard:read - View dashboard
assets:read / assets:write - Asset management
samples:read / samples:write - Sample management
alarms:read / alarms:write - Alarm monitoring
users:read / users:write - User management
Security Best Practices
HTTP-Only Cookies Tokens are stored in HTTP-only cookies to prevent XSS attacks
Secure Flag Production cookies use the secure flag for HTTPS-only transmission
SameSite Protection sameSite: 'lax' prevents CSRF attacks
Token Expiration Tokens expire after 24 hours and are automatically validated
Backend API Integration
All authentication requests go through the helper function:
src/app/actions/helpers.ts
export async function requestWithAuth (
input : RequestInfo ,
init ?: RequestInit ,
) : Promise < Response > {
const token = ( await cookies ()). get ( "token" )?. value ;
const headers = new Headers ( init ?. headers || {});
headers . set ( "Content-Type" , "application/json" );
if ( token ) {
headers . set ( "Authorization" , `Bearer ${ token } ` );
}
const requestInit : RequestInit = { ... init , headers };
const url = ` ${ process . env . NEXT_PUBLIC_API_URL }${ input } ` ;
return fetch ( url , requestInit );
}
The backend API URL is configured via the NEXT_PUBLIC_API_URL environment variable. The backend may require 30-60 seconds for cold start on free tier hosting.
Troubleshooting
Token Not Found
If you see “No token found in response” errors, check:
Backend API is running and accessible
OTP verification endpoint returns a valid token
Token format matches expected structure
Automatic Logout
Users are automatically logged out when:
Token expires (after 24 hours)
Token is invalid or malformed
Middleware detects an expired token
OTP Not Received
If OTP emails aren’t arriving:
Check spam/junk folder
Verify backend email service is configured
Wait 10 seconds before requesting a new code
System Architecture Learn about the overall application architecture
Data Flow Understand how data flows through the application
Environment Setup Configure authentication environment variables
Security Security best practices for deployment