Generated using AI. Be aware that everything might not be accurate.



Chapter 6: Authentication Strategies

Authentication adds accountability to comments and reduces spam. This chapter explores various authentication approaches that balance security with user convenience.

Authentication Spectrum

From simplest to most secure:

Level Method Spam Risk User Friction
0 Anonymous Very High None
1 Name + Email High Very Low
2 Email Verification Medium Low
3 Social OAuth Low Low
4 Account Required Very Low Medium
5 Verified Identity Minimal High

Anonymous Comments

Implementation

No authentication required:

@router.post("/comments")
async def create_comment(comment: CommentCreate, request: Request):
    # Capture what we can
    return await service.create_comment(
        comment,
        ip_address=request.client.host,
        user_agent=request.headers.get("user-agent"),
        fingerprint=request.headers.get("x-fingerprint")
    )

Risk Mitigation

Without auth, rely heavily on:

  • Rate limiting by IP
  • Content analysis
  • Honeypot fields
  • CAPTCHA challenges
  • Manual moderation queue

Email-Based Authentication

Simple Email Field

Collect but don’t verify:

// Frontend
<input type="email" name="email" 
       placeholder="Email (for Gravatar, not published)">

Benefits:

  • Low friction
  • Enables Gravatar
  • Can block known spam emails

Email Verification Flow

Verify before displaying:

  1. User submits comment with email
  2. System sends verification link
  3. User clicks link
  4. Comment becomes visible
# Generate verification token
import secrets
from datetime import datetime, timedelta

def create_verification(comment_id: str, email: str) -> str:
    token = secrets.token_urlsafe(32)
    expiry = datetime.utcnow() + timedelta(hours=24)
    
    # Store token
    redis.setex(
        f"verify:{token}",
        86400,  # 24 hours
        json.dumps({"comment_id": comment_id, "email": email})
    )
    
    return token

def verify_comment(token: str) -> bool:
    data = redis.get(f"verify:{token}")
    if not data:
        return False
    
    info = json.loads(data)
    # Approve the comment
    db.execute(
        "UPDATE comments SET status = 'approved' WHERE id = %s",
        [info["comment_id"]]
    )
    redis.delete(f"verify:{token}")
    return True

Passwordless email login:

async def send_magic_link(email: str, redirect_url: str):
    token = secrets.token_urlsafe(32)
    
    # Store with expiry
    await redis.setex(f"magic:{token}", 600, email)  # 10 minutes
    
    link = f"https://comments.site.com/auth/verify?token={token}&redirect={redirect_url}"
    
    await send_email(
        to=email,
        subject="Sign in to comment",
        body=f"Click to sign in: {link}"
    )

async def verify_magic_link(token: str):
    email = await redis.get(f"magic:{token}")
    if not email:
        raise AuthenticationError("Invalid or expired link")
    
    await redis.delete(f"magic:{token}")
    
    # Create or get user
    user = await get_or_create_user(email=email, provider="email")
    
    # Generate session
    session_token = create_session(user.id)
    return session_token

OAuth Social Login

Supported Providers

Popular choices for comment systems:

Provider Audience Trust Level
GitHub Developers High
Google General Medium
Twitter/X Social Medium
Discord Communities Medium
Facebook General Medium

OAuth Flow

┌─────────┐     ┌─────────────┐     ┌──────────┐
│  User   │────▶│ Comment API │────▶│ Provider │
│ Browser │     │   Server    │     │  (OAuth) │
└─────────┘     └─────────────┘     └──────────┘
     │                │                   │
     │ 1. Click       │                   │
     │    "Login"     │                   │
     │                │                   │
     │◀───────────────│                   │
     │ 2. Redirect to │                   │
     │    Provider    │                   │
     │────────────────────────────────────▶│
     │ 3. User authorizes                  │
     │◀────────────────────────────────────│
     │ 4. Redirect with code               │
     │────────────────▶│                   │
     │ 5. Exchange code│                   │
     │                 │──────────────────▶│
     │                 │◀──────────────────│
     │                 │ 6. Access token   │
     │◀────────────────│                   │
     │ 7. Session      │                   │
     │    created      │                   │

GitHub OAuth Implementation

# config.py
GITHUB_CLIENT_ID = os.environ["GITHUB_CLIENT_ID"]
GITHUB_CLIENT_SECRET = os.environ["GITHUB_CLIENT_SECRET"]
GITHUB_REDIRECT_URI = "https://comments.site.com/auth/github/callback"

# routes/auth.py
@router.get("/auth/github")
async def github_login(redirect: str = "/"):
    state = secrets.token_urlsafe(16)
    await redis.setex(f"oauth_state:{state}", 600, redirect)
    
    params = urlencode({
        "client_id": GITHUB_CLIENT_ID,
        "redirect_uri": GITHUB_REDIRECT_URI,
        "scope": "read:user user:email",
        "state": state
    })
    
    return RedirectResponse(f"https://github.com/login/oauth/authorize?{params}")

@router.get("/auth/github/callback")
async def github_callback(code: str, state: str):
    # Verify state
    redirect = await redis.get(f"oauth_state:{state}")
    if not redirect:
        raise HTTPException(400, "Invalid state")
    
    # Exchange code for token
    async with httpx.AsyncClient() as client:
        token_response = await client.post(
            "https://github.com/login/oauth/access_token",
            data={
                "client_id": GITHUB_CLIENT_ID,
                "client_secret": GITHUB_CLIENT_SECRET,
                "code": code
            },
            headers={"Accept": "application/json"}
        )
        tokens = token_response.json()
        
        # Get user info
        user_response = await client.get(
            "https://api.github.com/user",
            headers={"Authorization": f"Bearer {tokens['access_token']}"}
        )
        github_user = user_response.json()
    
    # Create or update user
    user = await upsert_user(
        provider="github",
        provider_id=str(github_user["id"]),
        name=github_user["name"] or github_user["login"],
        email=github_user.get("email"),
        avatar=github_user["avatar_url"]
    )
    
    # Create session
    session = create_session(user.id)
    
    response = RedirectResponse(redirect)
    response.set_cookie("session", session, httponly=True, secure=True, samesite="lax")
    return response

Google OAuth

from google.oauth2 import id_token
from google.auth.transport import requests

GOOGLE_CLIENT_ID = os.environ["GOOGLE_CLIENT_ID"]

@router.post("/auth/google")
async def google_auth(credential: str):
    """Verify Google Sign-In token"""
    try:
        idinfo = id_token.verify_oauth2_token(
            credential, 
            requests.Request(), 
            GOOGLE_CLIENT_ID
        )
        
        user = await upsert_user(
            provider="google",
            provider_id=idinfo["sub"],
            name=idinfo.get("name"),
            email=idinfo.get("email"),
            avatar=idinfo.get("picture")
        )
        
        session = create_session(user.id)
        return {"session": session}
        
    except ValueError:
        raise HTTPException(400, "Invalid token")

Session Management

JWT Tokens

Stateless authentication:

import jwt
from datetime import datetime, timedelta

SECRET_KEY = os.environ["JWT_SECRET"]
ALGORITHM = "HS256"

def create_access_token(user_id: str, expires_delta: timedelta = timedelta(days=7)):
    expire = datetime.utcnow() + expires_delta
    payload = {
        "sub": user_id,
        "exp": expire,
        "iat": datetime.utcnow()
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def verify_token(token: str) -> str:
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        return payload["sub"]
    except jwt.ExpiredSignatureError:
        raise AuthenticationError("Token expired")
    except jwt.InvalidTokenError:
        raise AuthenticationError("Invalid token")
from itsdangerous import URLSafeTimedSerializer

serializer = URLSafeTimedSerializer(SECRET_KEY)

def create_session(user_id: str) -> str:
    return serializer.dumps({"user_id": user_id})

def get_session(token: str, max_age: int = 604800) -> dict:
    try:
        return serializer.loads(token, max_age=max_age)
    except:
        return None

Session Storage

For server-side sessions:

class SessionStore:
    def __init__(self, redis: Redis):
        self.redis = redis
        self.ttl = 604800  # 7 days
    
    async def create(self, user_id: str) -> str:
        session_id = secrets.token_urlsafe(32)
        await self.redis.setex(
            f"session:{session_id}",
            self.ttl,
            json.dumps({"user_id": user_id, "created": time.time()})
        )
        return session_id
    
    async def get(self, session_id: str) -> dict | None:
        data = await self.redis.get(f"session:{session_id}")
        return json.loads(data) if data else None
    
    async def destroy(self, session_id: str):
        await self.redis.delete(f"session:{session_id}")

Frontend Authentication UI

Login Component

class AuthManager {
    constructor(apiUrl) {
        this.apiUrl = apiUrl;
        this.user = null;
        this.checkSession();
    }
    
    async checkSession() {
        try {
            const res = await fetch(`${this.apiUrl}/auth/me`, {
                credentials: 'include'
            });
            if (res.ok) {
                this.user = await res.json();
                this.onAuthChange();
            }
        } catch (e) {}
    }
    
    renderLoginButtons() {
        return `
            <div class="auth-buttons">
                <button onclick="auth.loginWith('github')" class="btn-github">
                    <svg>...</svg> Sign in with GitHub
                </button>
                <button onclick="auth.loginWith('google')" class="btn-google">
                    <svg>...</svg> Sign in with Google
                </button>
            </div>
        `;
    }
    
    loginWith(provider) {
        const returnUrl = encodeURIComponent(window.location.href);
        window.location.href = `${this.apiUrl}/auth/${provider}?redirect=${returnUrl}`;
    }
    
    async logout() {
        await fetch(`${this.apiUrl}/auth/logout`, {
            method: 'POST',
            credentials: 'include'
        });
        this.user = null;
        this.onAuthChange();
    }
}

User Avatar Display

function renderUser(user) {
    if (!user) {
        return auth.renderLoginButtons();
    }
    
    return `
        <div class="user-info">
            <img src="${user.avatar}" alt="${user.name}" class="user-avatar">
            <span class="user-name">${user.name}</span>
            <button onclick="auth.logout()" class="btn-logout">Sign out</button>
        </div>
    `;
}

Anonymous with Optional Auth

Best of both worlds:

function renderCommentForm(user) {
    if (user) {
        // Authenticated user - pre-fill and lock fields
        return `
            <form class="comment-form">
                <div class="user-badge">
                    <img src="${user.avatar}">
                    Commenting as <strong>${user.name}</strong>
                </div>
                <textarea name="content" required></textarea>
                <button type="submit">Post</button>
            </form>
        `;
    }
    
    // Guest form with optional login
    return `
        <form class="comment-form">
            <p class="auth-prompt">
                <button type="button" onclick="auth.loginWith('github')">
                    Sign in with GitHub
                </button>
                or comment as guest:
            </p>
            <input name="author_name" placeholder="Name *" required>
            <input name="author_email" type="email" placeholder="Email">
            <textarea name="content" required></textarea>
            <button type="submit">Post as Guest</button>
        </form>
    `;
}

Security Considerations

CSRF Protection

# Generate CSRF token
def get_csrf_token(session_id: str) -> str:
    return hmac.new(
        SECRET_KEY.encode(),
        session_id.encode(),
        hashlib.sha256
    ).hexdigest()

# Middleware to verify
@app.middleware("http")
async def csrf_middleware(request: Request, call_next):
    if request.method in ["POST", "PUT", "DELETE"]:
        token = request.headers.get("X-CSRF-Token")
        session = request.cookies.get("session")
        
        if not token or token != get_csrf_token(session):
            return JSONResponse({"error": "Invalid CSRF token"}, 403)
    
    return await call_next(request)

Rate Limiting Auth Endpoints

# Stricter limits for auth endpoints
auth_limiter = RateLimiter(
    key_func=get_client_ip,
    limits=[
        ("5/minute", "login_attempts"),
        ("20/hour", "login_attempts"),
    ]
)

Chapter Summary

Method Best For Implementation
Anonymous Low-risk blogs Minimal
Email Verification Medium traffic Easy
GitHub OAuth Developer blogs Medium
Multiple Providers General sites Complex
Magic Links Privacy-focused Medium

In the next chapter, we’ll tackle spam prevention and detection.


Navigation:



>> You can subscribe to my mailing list here for a monthly update. <<