Authentication
Every project generated by xpress-generator includes a complete authentication system out of the box.
Auth flow overviewβ
Client Server
β β
βββ POST /api/auth/login βββββββΊβ Verify credentials
β β Generate accessToken (15m)
ββββ { accessToken } + cookie ββ€ Generate refreshToken (7d)
β β Store refreshToken in DB
β β Set httpOnly cookie
β β
βββ GET /api/protected βββββββββΊβ Verify Bearer token
β Authorization: Bearer ... β (middleware: verifyToken)
ββββ { data } ββββββββββββββββββ€
β β
βββ POST /api/auth/refresh βββββΊβ Read refreshToken from cookie
β β Verify against DB
ββββ { accessToken } βββββββββββ€ Issue new accessToken
β β
βββ POST /api/auth/logout ββββββΊβ Delete refreshToken from DB
ββββ 204 No Content ββββββββββββ€ Clear cookie
Auth endpointsβ
| Method | Route | Description | Auth required |
|---|---|---|---|
| POST | /api/auth/login | Login and receive tokens | β |
| POST | /api/auth/refresh | Refresh access token | β |
| POST | /api/auth/logout | Logout and revoke token | β |
Loginβ
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"secret123"}'
Response:
{
"status": "success",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
The refreshToken is set as an httpOnly cookie β it is never exposed in the response body.
Refreshβ
curl -X POST http://localhost:3000/api/auth/refresh \
-H "Cookie: refreshToken=<token>"
Logoutβ
curl -X POST http://localhost:3000/api/auth/logout \
-H "Authorization: Bearer <accessToken>"
Protecting routesβ
verifyToken β require authenticationβ
const { verifyToken } = require('../middleware/auth');
router.get('/profile', verifyToken, getProfile);
This middleware:
- Reads the
Authorization: Bearer <token>header - Verifies the JWT signature using
JWT_SECRET - Attaches
req.user({ id, role }) for downstream handlers - Passes
AppError(401)to the error handler if invalid
requireRole β require a specific roleβ
const { verifyToken, requireRole } = require('../middleware/auth');
// Only admins can delete
router.delete('/:id', verifyToken, requireRole('admin'), deleteUser);
// Multiple roles allowed
router.put('/:id', verifyToken, requireRole('admin', 'editor'), updatePost);
requireRole must be used after verifyToken β it depends on req.user being set.
authRateLimiter β limit auth attemptsβ
Already applied to all /api/auth/* routes: maximum 10 requests per 15 minutes per IP. No additional configuration needed.
Environment variablesβ
JWT_SECRET=your-strong-secret-here
JWT_EXPIRES_IN=15m
REFRESH_TOKEN_SECRET=another-strong-secret
Never commit real secrets to Git. Use strong random values in production:
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
Refresh token storageβ
The RefreshToken model is unified across all databases:
// Same interface for MongoDB, Sequelize (MySQL/PostgreSQL), and mssql
const RefreshToken = {
findOne: (token) => ...,
create: (userId, token, expiresAt) => ...,
deleteOne: (token) => ...,
};
This means authController.js works identically regardless of your chosen database.
For MongoDB, the schema uses a TTL index to auto-delete expired tokens:
expiresAt: { type: Date, expires: '7d' }
For Sequelize/mssql, you should set up a scheduled job or CRON to clean up expired tokens periodically.
Customizing authenticationβ
The generated authController.js is a standard file β open it and modify it:
Add registrationβ
exports.register = catchAsync(async (req, res) => {
const { name, email, password } = req.body;
const hashed = await bcrypt.hash(password, 12);
const user = await User.create({ name, email, password: hashed });
httpResponse.created(res, { id: user._id });
});
Change token expiryβ
In .env:
JWT_EXPIRES_IN=1h # 1 hour
JWT_EXPIRES_IN=7d # 7 days
Store user in DBβ
The generated login handler checks against a hardcoded user for demonstration. Replace it with your own User model query:
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.password))) {
return next(new AppError(MESSAGES.UNAUTHORIZED, 401));
}