Authentication Flow
SmartFall implements a secure, JWT-based authentication system with role-based access control (RBAC).
Authentication Flow Diagram
Registration Flow
Login Process
Step 1: Email Verification
// Check if user exists
const user = await db.users.findByEmail(email);
if (!user) {
return Response.json({ error: "Invalid credentials" }, { status: 401 });
}
Step 2: Password Verification
import bcryptjs from "bcryptjs";
const isPasswordValid = await bcryptjs.compare(
plainPassword,
user.passwordHash,
);
if (!isPasswordValid) {
return Response.json({ error: "Invalid credentials" }, { status: 401 });
}
Step 3: JWT Generation
import * as jose from "jose";
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const token = await new jose.SignJWT({
sub: user.id,
email: user.email,
role: user.role,
})
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("24h")
.sign(secret);
Step 4: Session Creation
// Create session record
await db.sessions.create({
userId: user.id,
token: token,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
ipAddress: request.headers.get("x-forwarded-for"),
userAgent: request.headers.get("user-agent"),
});
// Set secure HTTP-only cookie
response.headers.set(
"Set-Cookie",
`session=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=86400`,
);
JWT Token Structure
Standard JWT format with three parts: header.payload.signature
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload (Claims)
{
"sub": "user_uuid",
"email": "user@example.com",
"role": "PATIENT",
"iat": 1711000000,
"exp": 1711086400
}
Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Token Verification
On each request, the token is verified:
async function verifyToken(token: string): Promise<JWTPayload | null> {
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const verified = await jose.jwtVerify(token, secret);
return verified.payload;
} catch (error) {
return null; // Invalid or expired token
}
}
Middleware Authentication
// app/api/middleware.ts
export async function authMiddleware(request: Request) {
// Extract token from Authorization header or cookie
const token = extractToken(request);
if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
// Verify token
const payload = await verifyToken(token);
if (!payload) {
return Response.json({ error: "Invalid token" }, { status: 401 });
}
// Attach to request context
request.user = payload;
return null; // Continue to next handler
}
Role-Based Access Control (RBAC)
User Roles
| Role | Permissions | Endpoints |
|---|---|---|
| PATIENT | View own data, report falls | /api/patients/me, /api/falls |
| CAREGIVER | View assigned patients, manage alerts | /api/patients, /api/caregivers/patients |
| ADMIN | Full system access, user management | /api/admin/**, all endpoints |
Authorization Decorator
function requireRole(allowedRoles: Role[]) {
return function (handler: RouteHandler) {
return async (request: Request) => {
const token = extractToken(request);
const payload = await verifyToken(token);
if (!payload) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
if (!allowedRoles.includes(payload.role)) {
return Response.json({ error: "Forbidden" }, { status: 403 });
}
return handler(request, payload);
};
};
}
Usage in Routes
export const GET = requireRole(["ADMIN"])(async (request, user) => {
// Only admins can access this
const allUsers = await db.users.list();
return Response.json(allUsers);
});
Session Management
Creating Sessions
Triggered on successful login:
await db.sessions.create({
userId: user.id,
token,
expiresAt: futureDate,
ipAddress: clientIP,
userAgent: clientUA,
lastActivity: new Date(),
});
Session Expiration
Tokens expire automatically:
- Default: 24 hours (configurable via
SESSION_DURATION) - On expiration: Client must re-authenticate
Logout
// POST /api/auth/logout
export async function POST(request: Request) {
const token = extractToken(request);
// Invalidate session
await db.sessions.delete(token);
// Clear cookie
const response = Response.json({ success: true });
response.headers.set("Set-Cookie", "session=; Max-Age=0");
return response;
}
Password Management
Registration Password Validation
function validatePassword(password: string): {
valid: boolean;
errors: string[];
} {
const errors = [];
if (password.length < 8) {
errors.push("Minimum 8 characters");
}
if (!/[A-Z]/.test(password)) {
errors.push("Must contain uppercase letter");
}
if (!/[a-z]/.test(password)) {
errors.push("Must contain lowercase letter");
}
if (!/[0-9]/.test(password)) {
errors.push("Must contain digit");
}
if (!/[!@#$%^&*]/.test(password)) {
errors.push("Must contain special character");
}
return {
valid: errors.length === 0,
errors,
};
}
Password Reset Flow
- User requests password reset
- Email verification link sent
- Link token has 30-minute expiration
- User sets new password
- All sessions invalidated (force re-login)
Token Refresh
Optional refresh token mechanism:
// POST /api/auth/refresh
export async function POST(request: Request) {
const refreshToken = extractToken(request);
// Verify refresh token (longer expiration)
const payload = await verifyRefreshToken(refreshToken);
if (!payload) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
// Generate new access token
const newAccessToken = await generateToken(payload);
return Response.json({ token: newAccessToken });
}
Security Considerations
HTTP Headers
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-XSS-Protection", "1; mode=block");
response.headers.set("Strict-Transport-Security", "max-age=31536000");
Cookie Configuration
- HttpOnly: Prevents JavaScript access
- Secure: Only over HTTPS
- SameSite=Strict: Prevents CSRF attacks
- Max-Age: Session duration
Rate Limiting
Prevent brute force attacks:
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX_REQUESTS=5 # per email per minute
CORS & Cross-Origin
// Allowed origins from environment
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [];
response.headers.set("Access-Control-Allow-Origin", allowedOrigins[0]);
response.headers.set("Access-Control-Allow-Credentials", "true");