The main changes I made for authentication were adding a users table and a
sessions table. Passwords are hashed using Argon2, and on successful login the
server generates a random session token and stores it in the sessions table. The most
challenging part on the backend was making authentication work consistently across all
endpoints. To handle this, I wrote a small helper function that reads the authentication
cookie, validates the token by checking the sessions table, and attaches the authenticated
user to the request object. Another tricky part was handling ownership of books, since each
book stores a created_by_user_id. Edit and delete routes check that the logged-in
user matches this field before allowing the operation.
On the frontend, the hardest part was working with cookies and making them work correctly
with Axios. I had to enable withCredentials: true so the browser would include
the authentication cookie in API requests. I also needed to ensure the UI did not offer
actions that users were not authorized to perform, such as hiding edit and delete buttons
when the user is not logged in or does not own a book.
My biggest struggle with deployment was following the correct process to build both the backend and frontend and then serve the frontend assets in production without relying on the Vite development server. I also ran into build slowness locally while developing, especially with the frontend, due to my repository being stored in an iCloud-synced folder. Moving the project out of iCloud resolved these issues and made the build process much more reliable.
I considered XSS risks mainly around user input such as usernames, book titles, and author
bios. On the frontend, React escapes content by default, and I made sure to never use
dangerouslySetInnerHTML, so user-provided data is rendered as plain text rather
than executable HTML or JavaScript. On the backend, all incoming data is validated using
Zod schemas and returned as JSON responses. Combined with a strict Content Security Policy
set using Helmet, this significantly reduces the risk of XSS attacks.
Since my app uses cookies for authentication, CSRF was a concern. To mitigate this risk,
I implemented multiple protections. Authentication cookies are set with
SameSite=Lax, which prevents them from being sent with most cross-site requests.
Additionally, for all non-GET API requests, the backend checks for a custom
X-Requested-With header. This header is automatically added by Axios in my
frontend but would not be present in a forged request from another site. Requests missing
this header are rejected.
To reduce brute-force login attempts, I added application-level rate limiting using
express-rate-limit. This is applied to the authentication endpoints and limits
how many requests an IP address can make within a 15-minute window.
I used Helmet to automatically set security-related HTTP headers such as the Content Security Policy, X-Frame-Options, and X-Content-Type-Options. These headers provide defense-in-depth and help reduce the impact of common web-based attacks.
Authentication cookies are marked as HTTP-only so they cannot be accessed by JavaScript. I also restricted sensitive actions to non-GET HTTP methods and ensured that authorization checks are enforced on the backend even if the frontend hides certain UI elements. This ensures that security does not rely on client-side logic alone.