Building an authentication service
Implement secure registration and login, JWT issuance and local verification, refresh-token rotation with revocation, and defenses against common attacks.
Registration and login
def register(email, password):
if users.exists(email): raise EmailTaken()
salt = secrets.token_bytes(16)
users.insert(email=email, salt=salt,
password_hash=argon2(password, salt)) # slow, salted hash
def login(email, password):
user = users.get(email)
if not user or not argon2_verify(password, user.salt, user.password_hash):
rate_limiter.record_fail(email) # throttle credential stuffing
raise InvalidCredentials() # same error for both cases (no enumeration)
access = issue_jwt(user.id, ttl="15m")
refresh = issue_refresh(user.id) # opaque, stored server-side
return access, refresh
Note: return the same error whether the email or the password is wrong, so an attacker can’t enumerate accounts.
JWT issuance and verification
Sign access tokens with a private key; services verify with the public key locally — no auth-service call per request:
def issue_jwt(user_id, ttl):
payload = {"sub": user_id, "exp": now()+ttl, "iat": now(), "roles": roles(user_id)}
return jwt.encode(payload, PRIVATE_KEY, algorithm="RS256")
# in any service / the gateway:
def verify(token):
claims = jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"]) # checks sig + exp
return claims["sub"] # trusted user id, no DB hit
Keep tokens short-lived so a stolen one expires fast, and rotate signing keys (publish keys via a JWKS endpoint).
Refresh tokens with rotation + revocation
Refresh tokens are long-lived, so they must be revocable and rotated to detect theft:
def refresh(refresh_token):
rec = refresh_store.get(hash(refresh_token)) # store hashes, not raw tokens
if not rec or rec.revoked or rec.expired():
raise InvalidRefresh()
refresh_store.revoke(rec.id) # one-time use (rotation)
new_refresh = issue_refresh(rec.user_id)
if rec.already_used: # reuse → token was stolen
revoke_all_sessions(rec.user_id) # nuke the family, force re-login
return issue_jwt(rec.user_id, "15m"), new_refresh
Rotation (each refresh invalidates the last) means a stolen-then-reused refresh token is detected — the legit user’s next refresh fails the reuse check and you can revoke the whole session family.
Logout and revocation
- Logout → delete/revoke the refresh token server-side; the access token expires on its own within minutes.
- Global logout / “log out everywhere” → revoke all refresh tokens for the user; optionally maintain a small token blocklist (by jti) for immediate access-token invalidation when truly needed.
Storage and scale
- Users in a durable, sharded SQL store (by user id); email uniquely indexed.
- Refresh tokens and the blocklist in Redis (fast, TTL’d).
- The verification path is stateless (local JWT check), so app/API tiers scale horizontally; only login/refresh touch the stores.
Security hardening (mention these)
- Cookies: refresh token in an httpOnly, Secure, SameSite cookie (not JS- readable) to blunt XSS token theft; CSRF tokens for cookie-based auth.
- Brute force: per-account + per-IP rate limiting, lockout/CAPTCHA after N fails.
- MFA / TOTP: verify a second factor before issuing tokens.
- Transport: everything over TLS; never log tokens or passwords.
- Password reset: single-use, expiring, signed link emailed to the user.
The takeaway
Concrete signals: Argon2/bcrypt salted hashing, RS256 JWTs verified locally by services, rotating + revocable refresh tokens (with reuse detection), httpOnly cookies, rate-limited login, and non-enumerable errors. The access/refresh split is the crux — stateless speed for the hot path, a revocation point for safety — and every later system assumes this identity layer underneath.