Authentication adds accountability to comments and reduces spam. This chapter explores various authentication approaches that balance security with user convenience.
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 |
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")
)
Without auth, rely heavily on:
Collect but don’t verify:
// Frontend
<input type="email" name="email"
placeholder="Email (for Gravatar, not published)">
Benefits:
Verify before displaying:
# 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
Popular choices for comment systems:
| Provider | Audience | Trust Level |
|---|---|---|
| GitHub | Developers | High |
| General | Medium | |
| Twitter/X | Social | Medium |
| Discord | Communities | Medium |
| General | Medium |
┌─────────┐ ┌─────────────┐ ┌──────────┐
│ 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 │ │
# 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
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")
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
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}")
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();
}
}
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>
`;
}
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>
`;
}
# 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)
# Stricter limits for auth endpoints
auth_limiter = RateLimiter(
key_func=get_client_ip,
limits=[
("5/minute", "login_attempts"),
("20/hour", "login_attempts"),
]
)
| 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. <<