"Implement authentication and authorization securely: password hashing with argon2/bcrypt, sessions vs JWT, secure cookies, CSRF, OAuth via Auth.js or Passport, rate limiting, server-side role checks and secret management. Use when the user asks for login, signup, accounts, auth, OAuth/social login, permissions, roles or securing an app."
Install
npx skillscat add v-groups-foundations/freetoken/auth-security Install via the SkillsCat registry.
Auth and security
Use a proven library for the hard parts; your job is wiring it correctly and checking permissions
on the server for every request.
1. Choose the approach
- Next.js: Auth.js (
npm i next-auth@beta, v5) for OAuth/email, or Better Auth (npm i better-auth)
for email+password with sessions in your DB. - Express:
express-session+ a DB session store, plus Passport (passport-local,passport-google-oauth20) for OAuth. - FastAPI: server-side sessions or short JWTs via
pyjwt, hashing withargon2-cffi(orpwdlib). - Hosted alternative when the user wants zero auth code: Clerk, Supabase Auth.
2. Sessions vs JWT
- Default: server sessions. Random id (32+ bytes) in a cookie, session row in DB/Redis. Easy to
revoke, logout works, small cookie. - JWT: when separate services must verify tokens without a shared DB. Keep access tokens short
(5–15 min), refresh tokens rotated and stored server-side so they can be revoked. - Never store tokens in localStorage (readable by any XSS). Put them in cookies.
3. Cookies
Set-Cookie: sid=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1209600
HttpOnlyalways,Securein production (HTTPS),SameSite=Laxdefault (Strictfor high-value
apps,None; Secureonly for real cross-site needs).- Regenerate the session id on login and privilege change (prevents session fixation). Destroy on logout.
4. Passwords
import argon2 from 'argon2'; // npm i argon2
const hash = await argon2.hash(password); // argon2id defaults
const ok = await argon2.verify(hash, password);bcrypt alternative: npm i bcrypt, cost 12. Python: from argon2 import PasswordHasher.
- Minimum 8 characters (12 recommended), allow long passphrases and all characters; check against
breached lists if possible. No forced composition rules. - Same error for wrong email and wrong password ("Invalid email or password").
- Password reset: random single-use token, store its hash, expires in 30–60 min, invalidate sessions after reset.
5. CSRF
Cookie-authenticated, state-changing requests (POST/PUT/PATCH/DELETE) need protection: SameSite=Lax
cookies plus either a CSRF token (synchronizer or double-submit) or a strict Origin header check.
Never change state on GET. Auth.js and Next Server Actions include protections; Express needs it added.
6. OAuth
Use the library's provider; never hand-roll the flow. Register the exact callback URL
(http://localhost:3000/api/auth/callback/google in dev, the real domain in prod). Use state/PKCE
(libraries do). Link accounts by verified email only.
7. Authorization (the part people forget)
- Check on the server for every request: authenticated? and allowed on this resource?
const task = await db.task.findUnique({ where: { id } });
if (!task || task.ownerId !== req.user.id) return res.status(404).end(); // 404 hides existence- Roles:
user.rolein DB ('user' | 'admin'), arequireRole('admin')middleware. Hiding a button
in the UI is not authorization. - Scope every query by the owner/tenant id. Validate ids from the client; never trust a
userIdin the body.
8. Hardening checklist
- Rate limit login, signup, reset: e.g. 5 attempts/min per IP+account (
express-rate-limit,@fastify/rate-limit,slowapifor FastAPI). Return 429. - Secrets (
AUTH_SECRET, DB URL, OAuth secrets) only in.env, generated randomly
(node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))");.envin.gitignore. - Security headers:
helmetfor Express; CSP where possible. HTTPS in production. - Parameterized queries only; escape output (React does by default; avoid
dangerouslySetInnerHTML). - Log auth events (login, failure, reset) without passwords or tokens.
Check before you finish
- Sign up, log in, refresh the page (still logged in), log out (session gone, back button cannot act).
- With user A's session, request user B's resource by id: expect 404/403.
- Call an admin endpoint as a normal user: expect 403. Call any protected endpoint with no cookie: 401.
- Inspect the cookie in the browser (inspect_preview / devtools): HttpOnly, SameSite set.
- 6 rapid wrong logins trigger 429. Search the repo for hard-coded secrets.
Common mistakes
- Checking auth in middleware but not ownership per resource.
- JWT in localStorage; long-lived JWTs that cannot be revoked;
alg: noneaccepted. - Plain SHA-256 or MD5 password hashes.
- Different errors that reveal which emails exist.
- Committing
.envor putting secrets inNEXT_PUBLIC_/VITE_variables.