Intermediate · Authentication and files end to end · topic 22 of 34 · 25 min
Protecting pages and API routes
Explanation
Two kinds of protection
In the Notes app, /notes must only show to logged-in users. There are two places to check: the frontend (the pages) and the backend (the API). They do different jobs.
The frontend check is for user experience. If you are not logged in, the app sends you to /login instead of showing an empty, broken page. But anyone can call your API directly with a tool like curl, so this check stops nobody.
The backend check: middleware
Middleware is a function that runs before your route handler. An auth middleware reads the session cookie, finds the session, and puts the user id on the request. If there is no valid session, it stops and answers 401 Unauthorized.
async function requireUser(req, res, next) {
const sid = req.cookies.sid;
const session = sid ? await db.findSession(sid) : null;
if (!session || session.expiresAt < Date.now()) {
return res.status(401).json({ error: 'Login required' });
}
req.userId = session.userId;
next();
}
app.get('/api/notes', requireUser, async (req, res) => {
const notes = await db.all('SELECT * FROM notes WHERE user_id = ?', [req.userId]);
res.json(notes);
});Logged in is not enough: check the owner
Asha is logged in. She changes the URL to /api/notes/42, and note 42 belongs to Ravi. The session check passes, so you also need an ownership check: always filter by the user id from the session, never by an id the browser sends in the body.
SELECT id, title, body, pinned FROM notes
WHERE id = ? AND user_id = ?;| Situation | Status | Why |
|---|---|---|
| No cookie or expired session | 401 Unauthorized | We do not know who you are; please log in |
| Logged in, note belongs to someone else | 404 Not Found (or 403) | 404 hides that the note exists at all |
| Logged in, but not allowed (e.g. not an admin) | 403 Forbidden | We know you, but you may not do this |
| Logged in, own note | 200 OK | Allowed |
Frontend guards, for a smooth experience
// React (runs in the browser)
function RequireUser({ children }) {
const { user, loading } = useCurrentUser(); // calls GET /api/me
if (loading) return <p>Loading...</p>;
if (!user) return <Navigate to={'/login?next=' + encodeURIComponent(location.pathname)} />;
return children;
}
// Next.js server component (runs on the server)
export default async function NotesPage() {
const user = await getCurrentUser(); // reads the session cookie
if (!user) redirect('/login?next=/notes');
const notes = await getNotesFor(user.id);
return <NotesList notes={notes} />;
}Sending the user back after login
When the guard sends Asha to /login?next=/notes/5, after login the app should return her to /notes/5. But if you redirect to any value of next, an attacker can send a link like /login?next=https://evil.example, and after a real login your site sends the victim to a fake site. This is called an open redirect.
Watch the lesson
Protecting pages and API routes · 1/6
Logging in is only half the job. Now we protect pages and API routes.
Prefer a video by a teacher? Find free videos on “protected routes authentication middleware react express tutorial” on YouTube ↗
In real life: A UPI app and someone else's transaction
In a payments app, your phone shows only your own transactions. If the app's server checked only "is this person logged in?", a curious user could change a transaction id in a request and see a stranger's payment. Real payment backends check, on every request, that the transaction belongs to the logged-in account, and answer "not found" otherwise. The Notes app does the same with WHERE id = ? AND user_id = ?.
Practice (0/3 done)
Write safeRedirect(next) for the login page. Return next only when it is a string that starts with exactly one "/" and whose second character is not "/" or a backslash. In every other case (missing, empty, full URLs, "//evil.example", "/\\evil.example", "javascript:...") return "/notes".
Write requireUser(handler). It returns a new function that takes req. If req.session is missing or has no userId, return { status: 401, body: { error: "Login required" } } without calling handler. Otherwise call handler with a copy of req that also has userId set to req.session.userId, and return what handler returns. Then write getNote(req) for a list called notes (already given in the tests): find the note with id req.params.id that also has userId === req.userId; return { status: 200, body: note } or { status: 404, body: { error: "Note not found" } }.
Make a protection table for every Notes API route (GET /api/notes, POST /api/notes, GET/PATCH/DELETE /api/notes/:id, POST /api/auth/signup, /login, /logout). For each, write: needs login? ownership check? status codes for failures.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.