Notes
11 min read
By Le Quoc Khanh
Building Login/Logout Functions Yourself: Security Aspects Often Overlooked
Think Login/Logout is simple? An A-Z security checklist (CSRF, 2FA, rate limiting and more) to work through before you build your own authentication.
Hi everyone,
Many of you have probably had this experience as a student. When working on a large group project, if we felt our coding skills weren't quite on par with the rest of the team, we'd often "volunteer" for the tasks that seemed "easy," like coding the Login/Logout feature. The thinking was simple: it's just a form, check the username/password, let them in if it's correct, and show an error if it's not.
However, reality isn't quite that simple.
Even though we were warned about various security attacks in our classes, it probably didn't fully sink in. It wasn't until I started working and was exposed to a dedicated team whose sole responsibility was managing authentication (Login/Logout) for the company's entire product ecosystem that I truly understood its complexity. Those seemingly basic functions hide a myriad of detailed use cases that need to be handled.
Of course, today we have more convenient options. Third-party providers like Auth0, Firebase Authentication, or AWS Cognito have already handled these problems. We just need to integrate them, which is both fast and secure.
But...
If you're curious, or if your company or project requires you to "do it yourself" and build the entire system from scratch, then I invite you to join me. Let's explore what it really takes to build a "login" function properly.
Hold on! Please read these important notes first!
Before we dive into the detailed list below, I need to clarify a few important points:
- NOT every project needs to implement the "full" list of features I'm listing. This is meant to be a comprehensive reference for you to consider.
- CONSIDER CAREFULLY. The decision to apply these should be based on the scale of your project. Most importantly, please consult with experienced Senior developers or a Solutions Architect (SA) before implementation.
- THIS IS BASED ON PERSONAL EXPERIENCE compiled from what I know and have learned. It will certainly have omissions, so please feel free to provide feedback and additions.
1. The Login Function
This is the most basic function, but it's also the most important gateway to your system.
- Input validation: Validate input data (email/username format, password) on both the client-side (browser) and server-side.
- Password security:
- Favor length over complexity rules: allow long passphrases, set a sensible minimum (NIST SP 800-63B asks for at least 15 characters when the password is the only factor, 8 when it is part of MFA) and reject passwords that appear in known breach lists.
- NEVER store passwords in plain text. You must hash them (using bcrypt, Argon2, scrypt, etc.) before saving them to the database.
- Session creation (if using sessions): After successful authentication, create a server-side session and send the session ID back to the client via a cookie.
- JWT issuance (if using tokens): If using JWT, generate an access token (and possibly a refresh token) after authentication and send it to the client for storage.
- Set secure cookies: If storing tokens/session IDs in cookies, you must set the
HttpOnly,Secure, andSameSiteflags to enhance security. - CSRF protection: If you use cookies, you must have a mechanism to prevent CSRF (Cross-Site Request Forgery) attacks, typically by using a CSRF token.
- Two-Factor Authentication (2FA): Support 2FA. Prefer an authenticator app (like Google Authenticator) or passkeys; an OTP via SMS/email is better than nothing but easier to intercept.
- "Remember me" support: If you have this feature, use a refresh token or a long-lived token to maintain the login session.
- Account lockout policy: Temporarily lock an account after a certain number of failed login attempts (e.g., 5) to prevent brute-force attacks. Banking apps are very strict about this.
- Rate limiting: Limit the number of login attempts allowed from a single IP address within a specific time frame.
- CAPTCHA: Require the user to complete a "I'm not a robot" challenge after several consecutive failed logins.
- Device Fingerprinting:
- Collect device characteristics (user-agent, canvas, WebGL, timezone, etc.).
- Hash this information to create a device ID.
- If a login from an unrecognized device is detected => Send a warning email, require OTP verification.
- GeoIP Tracking: Detect logins from an IP in an unusual country (e.g., a user from Vietnam logging in from a different location) => Send a warning email, require OTP verification.
- Session management:
- Sessions must have a finite Time-To-Live (TTL).
- You must "regenerate" the session ID immediately after a successful login to prevent session fixation attacks.
- Frontend error feedback: Keep messages clear but generic. Show the same “Incorrect email or password” whether or not the account exists, so the form can’t be used to discover registered emails. Send lockout details to the account owner by email instead.
- Logging: Log ALL login behaviors (both successful and failed), saving information like: user ID, email, timestamp, IP address, device info, etc., for auditing purposes.
2. The Logout Function
You might think it's just about deleting the token on the client, but that's not enough.
- Session invalidation: If using sessions, you must ensure the session is destroyed on the server-side.
- CSRF protection: The logout API endpoint must also be protected against CSRF.
- Token revocation (if using tokens):
- This is a complex part of using JWT (as JWTs are stateless).
- You need a mechanism to "revoke" a token, e.g., using a blacklist (stored in Redis) or by setting a very short lifespan for access tokens (5-15 minutes) and using refresh tokens to issue new ones.
- Clear cookies: Clear all authentication-related cookies (access token, refresh token, session ID) from the client-side.
- Redirect: After a successful logout, redirect the user to the Login page or the homepage.
- Frontend state cleanup: Clear all user data from the client-side state (Redux/Context/etc.) to prevent sensitive information from leaking.
- Invalidate refresh token: If your system uses refresh tokens, ensure they are also invalidated or deleted from the database.
- Advanced "logout" options: Consider providing users with:
- Logout from the current device.
- Logout from a specific device (in a "manage devices" list).
- Logout from all devices (Logout all sessions).
- Frontend error feedback: Always use
try...catchblocks. Even if a logout fails, you need to inform the user. - Logging: Log logout behaviors (successful/failed) with details: user ID, email, timestamp, IP, device, etc.
3. The "Forgot Password" Function
This is an extremely sensitive feature. If not handled carefully, it can be exploited by attackers to take over accounts.
- Email/username verification: Look up the email/username on the server before sending a reset link, but always show the same response (“If an account exists, we’ve sent a reset link”) so the form can’t be used to check which emails are registered.
- Rate limiting and abuse protection: Limit the number of "forgot password" requests from a single IP or for a single email to prevent spam and abuse.
- Generate secure token: Create a reset token that is random, sufficiently long, hard to guess (32-64 characters), and, importantly, for one-time use only.
- Token expiration: This token must have a short expiration time (e.g., 10-15 minutes) to minimize risk.
- Send email with reset link: Send the user an email containing the link with the embedded token.
- Server-side token storage: Store the (hashed) token in your database, associating it with the user and its expiration time.
- Reset password form: The page where the user enters a new password must ensure:
- The token submitted is still valid (not expired, belongs to the user).
- The new password meets strength requirements (and includes a "confirm password" field).
- It has CSRF protection if necessary.
- Token one-time usage: Ensure the token can only be used once. Immediately invalidate it after a successful reset.
- Password security: The new password must also be hashed (bcrypt, Argon2, etc.) carefully before saving.
- Notify user after reset: Send the user an email notification saying, "Your password has just been changed" so they are aware of any unusual activity.
- Option to logout all sessions: You should provide an option (e.g., a checkbox) to "Log out from all other devices" after a successful password reset.
- Frontend error feedback: Provide specific, user-friendly error messages (e.g., "This token has expired," "Password is too weak").
- Logging: Log the request-to-reset and the reset status (success/fail) with details (user ID, email, timestamp, IP).
4. The Register Function
This initial user creation function also has many potential risks if not handled correctly.
- Input validation: Validate (on both client and server) the email format, password strength, username validity, and ensure the "password" and "confirm password" fields match.
- Duplicate check: Check if the email or username already exists in the system. If the email is taken, show the usual “check your inbox” message and email the existing owner instead of revealing it on screen.
- Password security: Apply the same password policy as login (length first, breached-password check) and hash passwords before saving.
- Email verification: Send a verification email (with a link or OTP code) to ensure the user's provided email is real.
- Rate limiting & bot protection: Limit the number of registrations from a single IP. It's almost mandatory to integrate CAPTCHA/reCAPTCHA to prevent bots from creating spam accounts.
- Username/email normalization: Standardize the input data. For example, convert emails to
lowercase, trim whitespace, etc., to ensure duplicate checks work correctly. - Set default user role/status: Assign a default role (e.g.,
user) and status (e.g.,unverified) to the new account. - Create related entities: Depending on your application's logic, you might need to create a user profile, an empty shopping cart, a favorites list, etc., for the new user.
- Send welcome email: Send a welcome email, possibly with usage instructions or helpful information.
- Secure session/token issuance: After a successful registration, you might want to automatically log the user in by issuing a session or token.
- CSRF protection: The registration form also needs to be protected against CSRF attacks.
- Email/phone confirmation reminder UI: If the user hasn't verified their account, you should display a reminder in the UI with a "Resend verification email" button.
- Terms of Service & Privacy Policy agreement: It's mandatory to require users to check a box agreeing to your "Terms of Service" and "Privacy Policy" before completing registration.
- Frontend error feedback: Give clear messages for things the user can fix (e.g., "Password is too short," "This password has appeared in a data breach") without confirming whether an email is already registered.
- Logging: Log the registration event (user ID, email, timestamp, IP).
Real-World Scenarios to Consider
The list above is technical, but in a real-world implementation, you might encounter these situations:
- User switching networks: A user is on Wi-Fi, loses connection, and switches to 4G/5G. Their IP address will change. Should the system send an alert or force re-authentication? (This needs careful discussion with your SA/Senior).
- User using a VPN: When using a VPN, the IP address can change frequently. You might consider allowing users to "trust" a device (Whitelist) to avoid constant alerts.
- Private mode: Browsers in private/incognito mode can interfere with Device Fingerprinting mechanisms. In this case, the system may need to "fallback" to checking IP and user-agent.
- Privacy: If your system collects IPs and fingerprints, you must clearly and transparently disclose this in your "Privacy Policy."
- User Experience (UX): Security is paramount, but don't let security measures negatively impact the user experience. Forcing users to verify themselves too often can be frustrating. We always need to find a balance between Security and Usability.
Conclusion
Well, we've gone through a pretty long list.
After reading all this, you can probably see that there are many details to handle correctly, right? This is precisely why "Auth-as-a-Service" platforms (like Auth0, Firebase, Cognito) are so useful. They have done all this complicated work for us.
Therefore, my advice is, if your project doesn't have any special or mandatory requirements, you should prioritize using these available services.
But in cases where you must build it yourself, I hope this checklist has given you a broader, more comprehensive view of the issues to be aware of, helping you avoid basic security pitfalls.
If you have experience or know of any other points to consider, please share them in the comments so we can all learn!
Related Articles
- The Rise of Quiet UX: Designing Calm, Frictionless ExperiencesDiscover the rise of Quiet UX—designing calm, intuitive, and frictionless digital experiences that reduce user stress and enhance satisfaction.
- The State of Frontend Career Growth in the AI EraHow AI is changing frontend careers: what’s shifting, which skills matter most, and how to future-proof your work as a developer.
- What's New in Next.js 16: Turbopack, AI Debugger & PPR GuideExplore the major changes in Next.js 16: Turbopack replaces Webpack, proxy.ts replaces middleware.ts, and the future of rendering with Cache Components.
Comments (0)
No comments yet. Start the conversation.