Skip to main content

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​

MethodRouteDescriptionAuth required
POST/api/auth/loginLogin and receive tokensβœ—
POST/api/auth/refreshRefresh access tokenβœ—
POST/api/auth/logoutLogout 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:

  1. Reads the Authorization: Bearer <token> header
  2. Verifies the JWT signature using JWT_SECRET
  3. Attaches req.user ({ id, role }) for downstream handlers
  4. 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);
info

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
danger

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));
}