This document explains the authentication and authorization system used in OpenLN, including implementation details and usage examples.
OpenLN uses a multi-layered authentication system:
- JWT (JSON Web Tokens) for stateless authentication
- OAuth 2.0 for third-party authentication (Google)
- Passport.js for authentication strategies
- Role-based access control for authorization
1. User submits credentials (email/password)
2. Server validates credentials
3. Server generates JWT token
4. Client stores token (localStorage/memory)
5. Client includes token in subsequent requests
6. Server validates token on each request
1. User clicks "Login with Google"
2. Redirect to Google OAuth consent screen
3. User grants permissions
4. Google redirects back with authorization code
5. Server exchanges code for user information
6. Server creates/updates user record
7. Server generates JWT token
8. Client receives token and user data
import jwt from 'jsonwebtoken';
export const JWT_CONFIG = {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRE || '7d',
algorithm: 'HS256',
};
export const generateToken = (payload) => {
return jwt.sign(payload, JWT_CONFIG.secret, {
expiresIn: JWT_CONFIG.expiresIn,
algorithm: JWT_CONFIG.algorithm,
});
};
export const verifyToken = (token) => {
return jwt.verify(token, JWT_CONFIG.secret);
};
export const generateRefreshToken = (payload) => {
return jwt.sign(payload, JWT_CONFIG.secret, {
expiresIn: '30d',
algorithm: JWT_CONFIG.algorithm,
});
};import jwt from 'jsonwebtoken';
import { User } from '../models/User.js';
import { logger } from '../utils/logger.js';
export const authenticate = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: 'Access token required',
});
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Get user from database
const user = await User.findById(decoded.id).select('-password');
if (!user) {
return res.status(401).json({
success: false,
message: 'User not found',
});
}
if (!user.isActive) {
return res.status(401).json({
success: false,
message: 'Account deactivated',
});
}
// Attach user to request
req.user = user;
next();
} catch (jwtError) {
if (jwtError.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token expired',
});
}
if (jwtError.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: 'Invalid token',
});
}
throw jwtError;
}
} catch (error) {
logger.error('Authentication error:', error);
res.status(500).json({
success: false,
message: 'Authentication failed',
});
}
};
export const authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Authentication required',
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: 'Insufficient permissions',
});
}
next();
};
};
export const optionalAuth = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.id).select('-password');
if (user && user.isActive) {
req.user = user;
}
} catch (jwtError) {
// Ignore JWT errors for optional auth
logger.debug('Optional auth failed:', jwtError.message);
}
}
next();
} catch (error) {
logger.error('Optional authentication error:', error);
next();
}
};import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { User } from '../models/User.js';
import { logger } from '../utils/logger.js';
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/api/v1/auth/google/callback',
},
async (accessToken, refreshToken, profile, done) => {
try {
// Check if user already exists
let user = await User.findOne({ googleId: profile.id });
if (user) {
return done(null, user);
}
// Check if user exists with same email
user = await User.findOne({ email: profile.emails[0].value });
if (user) {
// Link Google account to existing user
user.googleId = profile.id;
user.profile.avatar = user.profile.avatar || profile.photos[0].value;
await user.save();
return done(null, user);
}
// Create new user
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
profile: {
avatar: profile.photos[0].value,
},
isEmailVerified: true,
});
logger.info(`New user created via Google OAuth: ${user.email}`);
done(null, user);
} catch (error) {
logger.error('Google OAuth error:', error);
done(error, null);
}
}
)
);
export default passport;import bcrypt from 'bcryptjs';
import { validationResult } from 'express-validator';
import { User } from '../models/User.js';
import { generateToken, generateRefreshToken } from '../config/jwt.js';
import { logger } from '../utils/logger.js';
export const register = async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array(),
});
}
const { email, password, name } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(409).json({
success: false,
message: 'User already exists',
});
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 12);
// Create user
const user = await User.create({
email,
password: hashedPassword,
name,
});
// Generate tokens
const token = generateToken({ id: user._id, email: user.email });
const refreshToken = generateRefreshToken({ id: user._id });
logger.info(`User registered successfully: ${email}`);
res.status(201).json({
success: true,
data: {
token,
refreshToken,
user: user.toProfileJSON(),
},
message: 'User registered successfully',
});
} catch (error) {
logger.error('Registration error:', error);
res.status(500).json({
success: false,
message: 'Registration failed',
});
}
};
export const login = async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array(),
});
}
const { email, password } = req.body;
// Find user and include password for comparison
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid credentials',
});
}
if (!user.isActive) {
return res.status(401).json({
success: false,
message: 'Account deactivated',
});
}
// Check password
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: 'Invalid credentials',
});
}
// Generate tokens
const token = generateToken({ id: user._id, email: user.email });
const refreshToken = generateRefreshToken({ id: user._id });
// Update last login
user.lastLogin = new Date();
await user.save();
logger.info(`User logged in successfully: ${email}`);
res.status(200).json({
success: true,
data: {
token,
refreshToken,
user: user.toProfileJSON(),
},
message: 'Login successful',
});
} catch (error) {
logger.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Login failed',
});
}
};
export const refreshToken = async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({
success: false,
message: 'Refresh token required',
});
}
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET);
const user = await User.findById(decoded.id);
if (!user || !user.isActive) {
return res.status(401).json({
success: false,
message: 'Invalid refresh token',
});
}
// Generate new tokens
const newToken = generateToken({ id: user._id, email: user.email });
const newRefreshToken = generateRefreshToken({ id: user._id });
res.status(200).json({
success: true,
data: {
token: newToken,
refreshToken: newRefreshToken,
},
});
} catch (jwtError) {
return res.status(401).json({
success: false,
message: 'Invalid refresh token',
});
}
} catch (error) {
logger.error('Token refresh error:', error);
res.status(500).json({
success: false,
message: 'Token refresh failed',
});
}
};
export const logout = (req, res) => {
// In a stateless JWT system, logout is handled client-side
// Here you could add token to a blacklist if needed
logger.info(`User logged out: ${req.user.email}`);
res.status(200).json({
success: true,
message: 'Logout successful',
});
};
export const googleCallback = async (req, res) => {
try {
if (!req.user) {
return res.redirect(`${process.env.CLIENT_URL}/login?error=auth_failed`);
}
// Generate tokens
const token = generateToken({ id: req.user._id, email: req.user.email });
const refreshToken = generateRefreshToken({ id: req.user._id });
// Redirect to frontend with tokens
const redirectUrl = `${process.env.CLIENT_URL}/auth/callback?token=${token}&refreshToken=${refreshToken}`;
res.redirect(redirectUrl);
} catch (error) {
logger.error('Google callback error:', error);
res.redirect(`${process.env.CLIENT_URL}/login?error=auth_failed`);
}
};import React, { createContext, useContext, useReducer, useEffect } from 'react';
import { authService } from '../services/authService';
import type { User } from '../types/user';
interface AuthState {
user: User | null;
token: string | null;
loading: boolean;
error: string | null;
}
interface AuthContextType extends AuthState {
login: (email: string, password: string) => Promise<void>;
register: (email: string, password: string, name: string) => Promise<void>;
logout: () => void;
clearError: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
type AuthAction =
| { type: 'AUTH_START' }
| { type: 'AUTH_SUCCESS'; payload: { user: User; token: string } }
| { type: 'AUTH_FAILURE'; payload: string }
| { type: 'AUTH_LOGOUT' }
| { type: 'CLEAR_ERROR' };
const authReducer = (state: AuthState, action: AuthAction): AuthState => {
switch (action.type) {
case 'AUTH_START':
return { ...state, loading: true, error: null };
case 'AUTH_SUCCESS':
return {
...state,
loading: false,
error: null,
user: action.payload.user,
token: action.payload.token,
};
case 'AUTH_FAILURE':
return {
...state,
loading: false,
error: action.payload,
user: null,
token: null,
};
case 'AUTH_LOGOUT':
return {
...state,
user: null,
token: null,
error: null,
};
case 'CLEAR_ERROR':
return { ...state, error: null };
default:
return state;
}
};
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, {
user: null,
token: localStorage.getItem('token'),
loading: false,
error: null,
});
useEffect(() => {
const initializeAuth = async () => {
const token = localStorage.getItem('token');
if (token) {
try {
const user = await authService.getCurrentUser();
dispatch({ type: 'AUTH_SUCCESS', payload: { user, token } });
} catch (error) {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
dispatch({ type: 'AUTH_LOGOUT' });
}
}
};
initializeAuth();
}, []);
const login = async (email: string, password: string): Promise<void> => {
dispatch({ type: 'AUTH_START' });
try {
const response = await authService.login(email, password);
localStorage.setItem('token', response.token);
localStorage.setItem('refreshToken', response.refreshToken);
dispatch({
type: 'AUTH_SUCCESS',
payload: { user: response.user, token: response.token }
});
} catch (error) {
dispatch({
type: 'AUTH_FAILURE',
payload: error instanceof Error ? error.message : 'Login failed'
});
throw error;
}
};
const register = async (email: string, password: string, name: string): Promise<void> => {
dispatch({ type: 'AUTH_START' });
try {
const response = await authService.register(email, password, name);
localStorage.setItem('token', response.token);
localStorage.setItem('refreshToken', response.refreshToken);
dispatch({
type: 'AUTH_SUCCESS',
payload: { user: response.user, token: response.token }
});
} catch (error) {
dispatch({
type: 'AUTH_FAILURE',
payload: error instanceof Error ? error.message : 'Registration failed'
});
throw error;
}
};
const logout = (): void => {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
dispatch({ type: 'AUTH_LOGOUT' });
};
const clearError = (): void => {
dispatch({ type: 'CLEAR_ERROR' });
};
return (
<AuthContext.Provider
value={{
...state,
login,
register,
logout,
clearError,
}}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};import { api } from './api';
import type { User } from '../types/user';
interface AuthResponse {
token: string;
refreshToken: string;
user: User;
}
class AuthService {
async login(email: string, password: string): Promise<AuthResponse> {
const response = await api.post('/auth/login', { email, password });
return response.data;
}
async register(email: string, password: string, name: string): Promise<AuthResponse> {
const response = await api.post('/auth/register', { email, password, name });
return response.data;
}
async getCurrentUser(): Promise<User> {
const response = await api.get('/users/me');
return response.data;
}
async refreshToken(): Promise<{ token: string; refreshToken: string }> {
const refreshToken = localStorage.getItem('refreshToken');
const response = await api.post('/auth/refresh', { refreshToken });
return response.data;
}
async logout(): Promise<void> {
await api.post('/auth/logout');
}
getGoogleAuthUrl(): string {
return `${import.meta.env.VITE_API_URL}/auth/google`;
}
}
export const authService = new AuthService();import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { LoadingSpinner } from '../components/common/LoadingSpinner';
interface ProtectedRouteProps {
children: React.ReactNode;
requiredRole?: string;
}
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children,
requiredRole
}) => {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) {
return <LoadingSpinner />;
}
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (requiredRole && user.role !== requiredRole) {
return <Navigate to="/unauthorized" replace />;
}
return <>{children}</>;
};- Use strong, random secrets (min 256 bits)
- Set appropriate expiration times
- Use HTTPS in production
- Store tokens securely (not in localStorage for sensitive apps)
- Implement token refresh mechanism
- Consider token blacklisting for logout
- Use bcrypt with appropriate salt rounds (12+)
- Enforce strong password policies
- Implement password reset functionality
- Rate limit authentication attempts
- Validate all inputs
- Use CORS properly
- Implement rate limiting
- Log authentication events
- Monitor for suspicious activity
This authentication system provides a secure, scalable foundation for user management in OpenLN. For additional security requirements, consider implementing features like two-factor authentication, session management, and advanced monitoring.