How the RealWorld API Handles JWT Authentication and Token Refresh Mechanisms
The RealWorld API implements JWT authentication by issuing long-lived tokens (60 days) stored in secure HTTP-only cookies, intentionally omitting a separate refresh token mechanism to maintain architectural simplicity.
The gothinkster/realworld repository provides a full-stack "Medium clone" specification that demonstrates production-grade application patterns. In the apps/api backend package, the RealWorld API handles JWT authentication using signed tokens persisted via cookies, with verification middleware protecting sensitive endpoints.
JWT Token Generation and Configuration
The useGenerateToken Utility
In apps/api/server/utils/generate-token.ts, the useGenerateToken function creates signed JWTs using the jsonwebtoken library. The implementation signs a payload containing the user ID with a secret from process.env.JWT_SECRET and sets a 60-day expiration (expiresIn: '60d').
// apps/api/server/utils/generate-token.ts
import jwt from 'jsonwebtoken';
export const useGenerateToken = (id: number): string =>
jwt.sign({ user: { id } }, process.env.JWT_SECRET, {
expiresIn: '60d',
});
This long expiration window eliminates the need for a separate refresh token flow, as the single token remains valid for the entire user session duration.
Authentication Flow: Login, Signup, and Logout
Issuing Tokens on Authentication
After successful credential validation, both the signup and login routes invoke useGenerateToken and store the result in an HTTP-only, secure cookie named auth_token. The cookie configuration uses sameSite: 'none' to support cross-origin requests in the demo environment.
In apps/api/server/routes/api/v2/auth/login.post.ts (and similarly in signup.post.ts):
// apps/api/server/routes/api/v2/auth/login.post.ts
if (match) {
setCookie(event, 'auth_token', useGenerateToken(user.id), {
secure: true,
httpOnly: true,
sameSite: 'none',
});
// response payload …
}
Terminating Sessions
The logout route in apps/api/server/routes/api/v2/auth/logout.post.ts simply deletes the auth_token cookie, immediately invalidating the session on the client side.
// apps/api/server/routes/api/v2/auth/logout.post.ts
export default defineEventHandler(async (event) => {
deleteCookie(event, 'auth_token');
return "Logged out";
});
Token Verification and Protected Routes
The useCheckAuth Middleware
The useCheckAuth function in apps/api/server/utils/auth.ts validates incoming requests by checking either the Authorization header (supporting both Token and Bearer prefixes) or the auth_token cookie. It verifies the signature using jwt.verify() and attaches the decoded user object to event.context.user.
// apps/api/server/utils/auth.ts
export const useCheckAuth = (mode: 'optional' | 'required') => (event) => {
const header = getHeader(event, 'authorization');
let token;
if (header && (header.split(' ')[0] === 'Token' || header.split(' ')[0] === 'Bearer')) {
token = header.split(' ')[1];
}
if (!token && mode === 'required') {
throw createError({ status: 401, statusMessage: 'Unauthorized', message: 'Missing authentication token' });
}
if (token) {
const verified = jwt.verify(token, process.env.JWT_SECRET);
if (!verified) {
throw createError({ status: 403, statusMessage: 'Unauthorized', message: 'Invalid authentication token' });
}
event.context.user = verified.user; // attach decoded user to request context
}
};
definePrivateEventHandler for Route Protection
The higher-order function definePrivateEventHandler in apps/api/server/auth-event-handler.ts wraps route handlers to enforce authentication requirements. It accepts a requireAuth option (defaulting to true) and uses the same verification logic to extract the user ID before passing control to the protected handler.
// apps/api/server/auth-event-handler.ts
export function definePrivateEventHandler<T>(
handler: (event: H3Event, cxt: PrivateContext) => T,
options: { requireAuth: boolean } = { requireAuth: true }
) {
return defineEventHandler(async (event) => {
// … token extraction & verification (same as above) …
if (token) {
const verified = jwt.verify(token, process.env.JWT_SECRET);
return handler(event, { auth: { id: Number(verified.user.id) } });
} else {
return handler(event, { auth: null });
}
});
}
Token Refresh Strategy in the RealWorld API
Unlike many production APIs that implement short-lived access tokens with long-lived refresh tokens, the RealWorld API deliberately avoids a separate refresh mechanism. The 60-day JWT lifespan serves as both access and refresh credential, forcing the user to re-authenticate only after two months of inactivity or when explicitly logging out.
This design choice prioritizes simplicity for the "real-world" demonstration purpose. If stricter session control were required, developers could shorten the expiresIn value and add a dedicated refresh endpoint that validates a stored refresh token before issuing a new JWT.
Summary
- Token Creation: The
useGenerateTokenutility inapps/api/server/utils/generate-token.tssigns JWTs with a 60-day expiration usingprocess.env.JWT_SECRET. - Cookie Storage: Authentication endpoints store tokens in secure, HTTP-only
auth_tokencookies viasetCookie, while logout clears them withdeleteCookie. - Request Verification: The
useCheckAuthmiddleware anddefinePrivateEventHandlerwrapper validate tokens from either theAuthorizationheader or cookies, attaching the decodeduser.idto the request context. - No Refresh Flow: The API intentionally omits refresh tokens, relying on the long-lived JWT to maintain sessions until expiration or logout.
Frequently Asked Questions
Does the RealWorld API use refresh tokens?
No, the RealWorld API does not implement a separate refresh token mechanism. Instead, it issues JWTs with a 60-day expiration period stored in HTTP-only cookies, requiring users to log in again only after the token expires or when they manually log out.
How long do JWT tokens last in the RealWorld API?
According to the source code in apps/api/server/utils/generate-token.ts, tokens are configured with expiresIn: '60d', meaning they remain valid for 60 days from issuance unless deleted by a logout action.
Where does the RealWorld API store JWT tokens?
The backend stores JWTs in a secure, HTTP-only, SameSite-None cookie named auth_token. While client-side test helpers may store tokens in localStorage for convenience, the actual API authentication relies on the cookie-based transmission handled automatically by Nitro's request processing.
How does the RealWorld API verify JWT tokens on protected routes?
Protected routes use the useCheckAuth middleware or the definePrivateEventHandler wrapper to extract tokens from either the Authorization header (supporting Token or Bearer schemes) or the auth_token cookie. The system validates the signature using jwt.verify() against process.env.JWT_SECRET and throws 401 or 403 errors for missing or invalid tokens.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →