Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Sponsored Reserved space — layout preview until AdSense is connected
Tutorial

A Practical Guide to Setting Up Authentication for Your Web Application

Learn how to implement secure authentication for your web application, covering password hashing, session management, attack protection, and best practices for login, registration, password resets, and two-factor authentication.

July 2026 12 min read 1 views 0 hearts

Let's be honest — authentication is one of those things that sounds simple until you actually have to implement it. You think "just a login form, how hard can it be?" Then you're three weeks deep into password hashing, session management, and wondering why your JWT tokens keep expiring at the worst possible moment.

I've been there. At PythonSkillset, we've helped dozens of developers get their authentication right. Here's what I've learned.

Why Authentication Matters More Than You Think

Before we dive into code, let's talk about why this matters. Every day, thousands of web applications get compromised because of weak authentication. Just last year, a popular note-taking app had a data breach because they stored passwords in plain text. That's not just embarrassing — it's potentially career-ending for the developers involved.

Authentication isn't just about keeping bad guys out. It's about building trust with your users. When someone signs up for your app, they're trusting you with their email, their password, and often their personal data. That trust is fragile.

The Three Pillars of Web Authentication

After working on authentication systems for years, I've found that most successful implementations rest on three things:

  1. Password security — how you store and verify passwords
  2. Session management — how you keep users logged in
  3. Protection against common attacks — CSRF, XSS, and brute force

Let's walk through each one.

Setting Up Password Hashing the Right Way

Never, ever store passwords in plain text. I know that sounds obvious, but you'd be surprised how many tutorials still show code that does exactly that.

Here's what you should do instead:

import hashlib
import os

def hash_password(password):
    salt = os.urandom(32)
    key = hashlib.pbkdf2_hmac(
        'sha256',
        password.encode('utf-8'),
        salt,
        100000
    )
    return salt + key

This uses PBKDF2 with a random salt and 100,000 iterations. The salt ensures that even if two users have the same password, their hashes will be different. The iterations make brute-force attacks painfully slow.

But honestly, you're better off using a library like bcrypt or argon2-cffi. They handle all the complexity for you:

import bcrypt

def hash_password(password):
    return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())

def check_password(password, hashed):
    return bcrypt.checkpw(password.encode('utf-8'), hashed)

That's it. Three lines of code and you're already more secure than most web applications out there.

Building a Login System That Actually Works

Now let's talk about sessions. When a user logs in, you need a way to remember who they are across multiple requests. There are two main approaches:

Session-based authentication stores a session ID in a cookie, and the server keeps track of who that session belongs to. It's simple and works well for traditional web apps.

Token-based authentication (like JWT) stores all the user information in a token that the client sends with every request. It's great for APIs and single-page applications.

Here's a simple session-based approach using Flask:

from flask import Flask, session, redirect, url_for, request
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
app.secret_key = 'your-secret-key-here'

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']

    user = get_user_from_database(username)
    if user and check_password_hash(user['password'], password):
        session['user_id'] = user['id']
        return redirect(url_for('dashboard'))
    else:
        return 'Invalid credentials', 401

Notice I'm using werkzeug.security for password hashing. It's built into Flask and handles all the complexity of salting and hashing for you.

The Session Management Trap

Here's something most tutorials don't tell you: session management is where things get tricky. You need to decide:

  • How long should sessions last?
  • What happens when a user closes their browser?
  • How do you handle multiple devices?

For most web applications, I recommend using server-side sessions with a reasonable timeout. Here's a pattern that works well:

from flask import session
from datetime import timedelta

app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=2)
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'

The HttpOnly flag prevents JavaScript from accessing the cookie, which stops XSS attacks from stealing session data. The SameSite attribute helps prevent CSRF attacks. These are small changes that make a huge difference.

Protecting Against Common Attacks

Let me share something I learned the hard way. Early in my career, I built a login system that worked perfectly — until someone pointed out it was vulnerable to timing attacks. The server took slightly longer to respond when the password was wrong versus when the username was wrong. An attacker could use that tiny difference to figure out valid usernames.

Here's how to avoid that:

import secrets

def verify_login(username, password):
    user = get_user_from_database(username)
    if not user:
        # Still hash a dummy password to prevent timing attacks
        dummy_hash = bcrypt.hashpw(b'fake', bcrypt.gensalt())
        bcrypt.checkpw(password.encode('utf-8'), dummy_hash)
        return False

    return bcrypt.checkpw(password.encode('utf-8'), user['password'])

This way, the response time is the same whether the user exists or not. Small detail, huge security impact.

Building a Registration Flow That Doesn't Annoy Users

Registration is where most authentication systems fail. They either ask for too much information or not enough. Here's what I've found works best:

  • Email and password only for the initial signup
  • Email verification within 24 hours
  • Optional profile completion after they're logged in

Here's a registration endpoint that follows this pattern:

from flask_mail import Mail, Message
import secrets

@app.route('/register', methods=['POST'])
def register():
    email = request.form['email']
    password = request.form['password']

    if len(password) < 8:
        return 'Password must be at least 8 characters', 400

    hashed = generate_password_hash(password)
    user_id = save_user_to_database(email, hashed)

    # Send verification email
    token = secrets.token_urlsafe(32)
    save_verification_token(user_id, token)
    send_verification_email(email, token)

    return 'Account created. Please check your email to verify.', 201

The verification email step is crucial. It confirms the user owns that email address and prevents people from signing up with someone else's email.

Handling Password Resets Securely

Password reset flows are surprisingly tricky. Here's a pattern that works:

  1. User requests a reset
  2. Generate a one-time token with an expiration time
  3. Send it via email
  4. When the user clicks the link, verify the token hasn't expired
  5. Let them set a new password

The key is making the token single-use and time-limited:

import secrets
from datetime import datetime, timedelta

def generate_reset_token(user_id):
    token = secrets.token_urlsafe(32)
    expires = datetime.now() + timedelta(hours=1)
    save_token_to_database(user_id, token, expires)
    return token

def verify_reset_token(token):
    record = get_token_from_database(token)
    if not record:
        return None
    if datetime.now() > record['expires']:
        return None
    return record['user_id']

Never use the user's ID or email as the reset token. Always generate a random one. And always set an expiration time — one hour is standard.

Adding Two-Factor Authentication

Two-factor authentication (2FA) adds a second layer of security. Even if someone steals a password, they can't log in without the second factor.

The most common approach is time-based one-time passwords (TOTP). Here's how to implement it:

import pyotp

def generate_secret():
    return pyotp.random_base32()

def get_totp_uri(secret, email):
    return pyotp.totp.TOTP(secret).provisioning_uri(
        name=email,
        issuer_name="YourAppName"
    )

def verify_totp(secret, code):
    totp = pyotp.TOTP(secret)
    return totp.verify(code)

The provisioning URI generates a QR code that users can scan with Google Authenticator or Authy. Once they do, they'll have a six-digit code that changes every 30 seconds.

Handling Edge Cases Gracefully

Here's something most tutorials skip: what happens when things go wrong. Users will forget passwords, lose their phones, and accidentally lock themselves out. Your authentication system needs to handle all of this gracefully.

Password reset flow: - Send a reset link, not the actual password - Make the link expire after one hour - Invalidate all existing sessions after a password change - Log the user out of all devices

Account lockout: - After 5 failed attempts, lock the account for 15 minutes - Send an email notification when an account is locked - Allow users to unlock immediately via email verification

Session management: - Show users their active sessions - Let them revoke sessions remotely - Force re-authentication for sensitive actions (like changing email or password)

Real-World Example: What PythonSkillset Uses

At PythonSkillset, we use a combination of Flask-Login for session management and Flask-Security for role-based access control. Here's our basic setup:

from flask_login import LoginManager, UserMixin, login_user, logout_user

login_manager = LoginManager()
login_manager.init_app(app)

class User(UserMixin):
    def __init__(self, id, email, role):
        self.id = id
        self.email = email
        self.role = role

@login_manager.user_loader
def load_user(user_id):
    user_data = get_user_by_id(user_id)
    if user_data:
        return User(user_data['id'], user_data['email'], user_data['role'])
    return None

This gives us session management out of the box, plus the login_required decorator for protecting routes.

Protecting Your API Endpoints

If your web app has an API, you need to think about authentication differently. APIs don't have sessions in the traditional sense. Instead, you'll want to use token-based authentication.

Here's a pattern that works well:

import jwt
from functools import wraps
from datetime import datetime, timedelta

SECRET_KEY = 'your-secret-key'

def generate_token(user_id):
    payload = {
        'user_id': user_id,
        'exp': datetime.utcnow() + timedelta(hours=1),
        'iat': datetime.utcnow()
    }
    return jwt.encode(payload, SECRET_KEY, algorithm='HS256')

def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get('Authorization')
        if not token:
            return 'Token is missing', 401
        try:
            payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            current_user = get_user_by_id(payload['user_id'])
        except:
            return 'Token is invalid', 401
        return f(current_user, *args, **kwargs)
    return decorated

The iat (issued at) claim in the JWT lets you invalidate all tokens for a user if needed. Just store the timestamp of the last password change and check it against the token's iat.

What About OAuth?

If you're building an app that needs to integrate with Google, GitHub, or Facebook, OAuth is the way to go. It lets users log in with their existing accounts, which reduces friction and improves security.

Here's a minimal OAuth flow using Flask-Dance:

from flask_dance.contrib.google import make_google_blueprint, google

google_bp = make_google_blueprint(
    client_id='your-client-id',
    client_secret='your-client-secret',
    scope=['profile', 'email']
)
app.register_blueprint(google_bp, url_prefix='/login')

@app.route('/google-login')
def google_login():
    if not google.authorized:
        return redirect(url_for('google.login'))

    resp = google.get('/oauth2/v2/userinfo')
    if resp.ok:
        user_info = resp.json()
        user = find_or_create_user(user_info['email'], user_info['name'])
        login_user(user)
        return redirect(url_for('dashboard'))

    return 'Failed to authenticate', 401

The beauty of OAuth is that you don't have to handle passwords at all. Google, GitHub, or whoever handles the authentication, and you just get the user's email and name.

Common Mistakes I See All the Time

After reviewing hundreds of authentication implementations, here are the most common mistakes:

Storing passwords in plain text. I still see this in production code. It's 2024. There's no excuse.

Not validating email addresses. If you don't verify emails, anyone can sign up with someone else's email and cause chaos.

Using weak session IDs. If your session IDs are predictable, an attacker can guess them and hijack sessions. Use secrets.token_urlsafe() or similar.

Not rate-limiting login attempts. Without rate limiting, attackers can brute-force passwords. Implement a delay after failed attempts and lock accounts after too many failures.

Testing Your Authentication

Before you deploy, test these scenarios:

  • Can a user log in with correct credentials?
  • Are incorrect credentials rejected?
  • Does the session expire after inactivity?
  • Can a user access protected routes without logging in?
  • Does the password reset flow work end-to-end?
  • What happens when someone tries to brute-force the login?

At PythonSkillset, we use automated tests for all of these. It saves us from embarrassing bugs in production.

The Bottom Line

Authentication doesn't have to be complicated. Use established libraries, follow best practices, and test everything. Your users will thank you — and so will your future self when you're not dealing with a security breach at 2 AM.

Start simple, add features as you need them, and never roll your own cryptography. The internet has enough broken authentication systems already.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.