This document explains how JWT-based authentication is implemented in the RealWorld reference implementation. It covers password hashing with bcryptjs, token generation and validation with jsonwebtoken, and the definePrivateEventHandler pattern for protecting routes.
For information about the authentication API specification and endpoint contracts, see Authentication & Authorization. For database schema details related to user storage, see Database Layer & Prisma.
The reference implementation uses a stateless JWT (JSON Web Token) authentication system where clients receive a token upon registration or login and include it in subsequent requests via the Authorization header.
Authentication Flow Diagram
Sources: api/openapi.yml22-88 apps/api/server/routes/api/articles/index.get.ts2-4
The authentication system uses the standard JWT format with three Base64-encoded sections separated by dots:
Token xxxxxx.yyyyyyy.zzzzzz
The Authorization header must include the literal string "Token " followed by the JWT. This format is specified in the OpenAPI security scheme and validated by the backend.
Token Payload Structure
The token payload contains the user's database ID (userId), which is extracted during validation to identify the authenticated user for subsequent database queries.
Sources: api/openapi.yml909-918 api/Conduit.postman_collection.json214-217
The implementation uses bcryptjs for secure password hashing. Passwords are never stored in plain text.
During user registration, the plain text password is hashed before being stored in the database:
During login, the provided password is compared against the stored hash:
Sources: apps/api/package.json9 api/openapi.yml22-54 api/Conduit.postman_collection.json12-65
JWT tokens are generated using the jsonwebtoken library with the JWT_SECRET environment variable as the signing key. Tokens are issued in two scenarios:
When a new user registers successfully:
| Step | Action | Implementation |
|---|---|---|
| 1 | Hash password | bcryptjs.hash(password, 10) |
| 2 | Create user record | prisma.user.create({data: {email, username, password: hashedPassword}}) |
| 3 | Generate JWT | jwt.sign({userId: user.id}, process.env.JWT_SECRET) |
| 4 | Return user object | {user: {...userData, token}} |
When an existing user logs in:
| Step | Action | Implementation |
|---|---|---|
| 1 | Find user by email | prisma.user.findUnique({where: {email}}) |
| 2 | Verify password | bcryptjs.compare(password, user.password) |
| 3 | Generate JWT | jwt.sign({userId: user.id}, process.env.JWT_SECRET) |
| 4 | Return user object | {user: {...userData, token}} |
Token Response Format
Both registration and login return the same response structure:
Sources: apps/api/package.json14 api/Conduit.postman_collection.json20-32 api/Conduit.postman_collection.json74-85
The reference implementation provides a custom event handler wrapper called definePrivateEventHandler that abstracts token validation logic. This pattern provides a clean interface for creating both public and protected route handlers.
definePrivateEventHandler Function Signature
Sources: apps/api/server/routes/api/articles/index.get.ts2-4 apps/api/server/routes/api/articles/index.get.ts53
Routes that require authentication call definePrivateEventHandler with the default options (or explicitly set requireAuth: true):
Routes that work with or without authentication use {requireAuth: false}:
Real Implementation Example
The GET /api/articles endpoint demonstrates optional authentication where the presence of auth.id affects query building logic:
Sources: apps/api/server/routes/api/articles/index.get.ts4-53 apps/api/server/routes/api/articles/index.get.ts55-89
| Component | Value | Example |
|---|---|---|
| Header Name | Authorization | - |
| Prefix | Token (with space) | - |
| JWT Token | <jwt> | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiaWF0IjoxNjAwMDAwMDAwfQ.signature |
| Full Header | Token <jwt> | Token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... |
The Postman collection demonstrates header usage across different authenticated endpoints:
Current User Endpoint
Update User Endpoint
Create Article Endpoint
The {{token}} variable is populated by the "Login and Remember Token" test, which extracts the token from the login response and stores it globally.
Sources: api/Conduit.postman_collection.json214-217 api/Conduit.postman_collection.json269-273 api/Conduit.postman_collection.json706-709 api/Conduit.postman_collection.json120-148
The authentication system requires the JWT_SECRET environment variable to be set:
| Variable | Purpose | Usage |
|---|---|---|
JWT_SECRET | Secret key for signing and verifying JWTs | Must be set in production; used by jwt.sign() and jwt.verify() |
JWT_SECRET should be a long, random stringjwt.sign() options (not shown in current implementation)Sources: api/openapi.yml909-918
The system returns standardized error responses for authentication failures:
401 Unauthorized Response
Returned when:
Authorization header is provided on a protected endpoint422 Unprocessable Entity Response
Returned when:
Sources: api/openapi.yml767-776 api/openapi.yml807-816
| Component | Location | Responsibility |
|---|---|---|
definePrivateEventHandler | apps/api/server/auth-event-handler.ts (inferred) | JWT validation wrapper for route handlers |
| Registration Route | apps/api/server/routes/api/users.post.ts (inferred) | Hash password, create user, generate token |
| Login Route | apps/api/server/routes/api/users/login.post.ts (inferred) | Verify credentials, generate token |
| Protected Routes | Various apps/api/server/routes/api/**/*.ts | Use definePrivateEventHandler with auth context |
| Dependencies | apps/api/package.json | jsonwebtoken, bcryptjs |
Sources: apps/api/package.json14-24 apps/api/server/routes/api/articles/index.get.ts2-4
The authentication implementation is thoroughly validated by the Postman collection, which includes:
The test collection uses Postman's scripting to store and reuse tokens:
This pattern enables stateful test execution where authentication persists across the entire test suite.
Sources: api/Conduit.postman_collection.json120-148 api/Conduit.postman_collection.json180-234 api/Conduit.postman_collection.json292-342
Refresh this wiki