A full-stack authentication demo showcasing JWT implementation with access and refresh tokens in a modern monorepo setup.
This project demonstrates a complete authentication system using Express.js backend with React frontend, featuring secure JWT token management, password hashing, and protected routes. Built as a learning resource for understanding modern authentication patterns.
- 🔐 JWT Authentication - Access & refresh token implementation
- 🔒 Secure Password Hashing - bcrypt with salt rounds
- 🍪 HTTP-Only Cookies - Secure token storage
- 🛡️ Protected Routes - Middleware-based route protection
- 🔄 Token Refresh - Automatic token renewal
- 📱 React Frontend - Modern UI with shadcn/ui components
- 🗄️ PostgreSQL Database - Drizzle ORM integration
- 🚦 Rate Limiting - Protection on authentication routes
- 🏗️ Monorepo Structure - Organized workspace with pnpm
- Express.js 5 - Web framework
- TypeScript - Type safety
- JWT - Authentication tokens
- bcrypt - Password hashing
- PostgreSQL - Database
- Drizzle ORM - Database toolkit
- express-rate-limit - Rate limiting
- React 19 - UI framework
- TypeScript - Type safety
- Tailwind CSS - Styling
- shadcn/ui - UI component library
- React Router 7 - Client routing
- Vite - Build tool
- pnpm - Package manager
- Docker - PostgreSQL container
- SSL/TLS - HTTPS support
- Node.js 24+
- pnpm 8+
- Docker (for PostgreSQL)
- mkcert (for SSL certificates)
- OpenSSL (for JWT key generation)
- Clone and install dependencies:
git clone https://github.com/toruiwasa/express-jsonwebtoken-demo.git
cd express-jsonwebtoken-demo
pnpm install- Generate SSL certificates:
pnpm certs:create # Creates HTTPS certificates
pnpm certs:jwt:create # Creates JWT signing keys- Start PostgreSQL:
pnpm db:up- Create environment file:
# Create .env in root directory
echo "DATABASE_URL=postgresql://postgres:mypassword@localhost:5432/postgres
ACCESS_TOKEN_SECRET=your-secret-key-here
SERVER_PORT=4000" > .env- Start development servers:
# Terminal 1 - API server
cd apps/api
pnpm dev
# Terminal 2 - Web app
cd apps/web
pnpm dev- Access the application:
- Frontend: https://localhost:3000
- Backend: https://localhost:4000
To quickly test authentication, you can seed the database with a test user. Run the following command:
pnpm db:seedThis will create a test user with the following credentials:
- Email:
test@example.com - Password:
SecureDemoP@ssw0rd!2026
These credentials meet the password complexity requirements below.
Passwords must contain:
- At least 1 uppercase letter
- At least 1 lowercase letter
- At least 1 number
- At least 1 special character
- Minimum 8 characters
- Navigate to https://localhost:3000
- Click "Sign Up"
- Enter email and strong password (meeting requirements above)
- Submit form to create account
- Click "Login"
- Enter credentials
- Access protected content after authentication
/protected- Requires valid access token- Automatic token refresh when expired
- Logout clears all tokens
Note: Authentication routes are rate-limited to prevent abuse
Register a new user
curl -X POST https://localhost:4000/signup \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "SecurePass123!"}'Authenticate user and receive tokens
curl -X POST https://localhost:4000/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "SecurePass123!"}' \
-c cookies.txtGet current user info (requires authentication)
curl https://localhost:4000/me \
-b cookies.txtRefresh access token using refresh token
curl -X POST https://localhost:4000/refresh_token \
-b cookies.txtLogout and clear tokens
curl -X POST https://localhost:4000/logout \
-b cookies.txtAccess protected resource
curl -X POST https://localhost:4000/protected \
-b cookies.txtexpress-jsonwebtoken-demo/
├── apps/
│ ├── api/ # Express.js backend
│ │ └── src/
│ │ ├── auth/ # Authentication logic
│ │ │ ├── authController.ts
│ │ │ ├── authMiddleware.ts
│ │ │ ├── authRoutes.ts
│ │ │ └── authService.ts
│ │ └── index.ts # Server entry point
│ └── web/ # React frontend
│ └── src/
│ ├── components/ # React components
│ │ └── ui/ # shadcn/ui components
│ ├── contexts/ # React context
│ └── lib/ # Utility functions
├── packages/
│ ├── database/ # Database schema & config
│ └── shared/ # Shared types & utilities
├── certs/ # SSL certificates & JWT keys
│ ├── cert.pem # HTTPS certificate
│ ├── key.pem # HTTPS private key
│ ├── jwt-private.pem # JWT signing key
│ └── jwt-public.pem # JWT verification key
└── docker-compose.yml # PostgreSQL setup
- Dual Token Strategy - Short-lived access tokens + long-lived refresh tokens
- Secure Cookie Storage - HTTP-only, secure, SameSite cookies
- Password Security - bcrypt hashing with complex password requirements
- Route Protection - Middleware-based authorization
- Rate Limiting - Protection against brute force attacks
- Monorepo Organization - Shared packages and clean separation
- Type Safety - Full TypeScript implementation
- Modern UI Components - shadcn/ui component system
- Security Best Practices - HTTPS, secure headers, input validation
- API Design - RESTful endpoints with proper error handling
- State Management - React Context for user state
- Client-Server Communication - Fetch API with credentials
- Database Integration - Modern ORM with type safety
# Install dependencies
pnpm install
# Generate certificates
pnpm certs:create # HTTPS certificates (requires mkcert)
pnpm certs:jwt:create # JWT keys (requires OpenSSL)
# Database
pnpm db:up # Start PostgreSQL
pnpm db:down # Stop PostgreSQL
# Development (run in separate terminals)
cd apps/api && pnpm dev # Start API server
cd apps/web && pnpm dev # Start web app
# Build
cd apps/api && pnpm build # Build API
cd apps/web && pnpm build # Build web app
# Linting
cd apps/api && pnpm lint # Lint API code
cd apps/web && pnpm lint # Lint web codeCreate .env file in project root:
# Database
DATABASE_URL=postgresql://postgres:mypassword@localhost:5432/postgres
# JWT Secret (use a strong secret in production)
ACCESS_TOKEN_SECRET=your-super-secret-key-change-this-in-production
# Server Port
SERVER_PORT=4000- Password Complexity - Enforced strong password requirements
- Rate Limiting - Applied to authentication endpoints
- JWT Key Separation - RSA keys for refresh tokens, HMAC for access tokens
- Secure Cookies - HTTP-only, secure, SameSite attributes
- HTTPS Only - All communication encrypted
- Input Validation - Schema validation on all inputs
- Development Only - This demo uses self-signed certificates
- JWT Secrets - Use strong, unique secrets in production
- Database Credentials - Change default PostgreSQL password
- CORS Configuration - Currently allows localhost only
- Rate Limiting - Implemented only on auth routes in this demo
ISC License - feel free to use this code for learning and experimentation.
