Authentication explained: sessions, JWTs and API keys, and when to use each
Mosaddik Billah 5 min read

On this page
Every app with accounts has to answer two questions on every request: who is this? and are they allowed to do this? There are three common ways to carry the answer to the first question from one request to the next: session cookies, JWTs and API keys. Each fits a different job, and picking the wrong one causes problems that are hard to fix later.
Authentication and authorisation are different#
- Authentication (often shortened to authn) is proving who you are: a password, a code from an app, “Sign in with Google”.
- Authorisation (authz) is deciding what you’re allowed to do: can this user see this invoice, or delete this product?
They fail differently, and HTTP has a code for each: 401 means “I don’t know who you are”, and 403 means “I know who you are, and you can’t do that”. Most security bugs in real apps are authorisation bugs: a logged-in user changes /invoices/41 to /invoices/42 and sees someone else’s invoice. Check ownership on every request, not only whether someone is logged in.
Step zero: store passwords properly#
Whatever you choose below, passwords are stored with a slow, salted hashing function made for passwords: Argon2id, bcrypt or scrypt. Never plain text, never encrypted (encryption can be reversed), and never a fast hash like SHA-256 on its own (it can be guessed billions of times per second).
Better still, let someone else hold the passwords: a sign-in provider such as Auth0, Clerk, Firebase Auth or Supabase Auth, or the members area of a platform like Wix.
Option 1: session cookies#
The classic approach, and still the best default for a website:
- The user signs in. The server checks the password.
- The server creates a session, a record in its database or Redis saying “session
a8f3…belongs to user 42”, and sends the session ID in a cookie. - The browser sends the cookie with every request, and the server looks the session up.
The cookie settings are where the security lives:
Set-Cookie: session=a8f3c2...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1209600HttpOnly: JavaScript on the page can’t read it, so a cross-site scripting bug can’t steal it.Secure: only sent over HTTPS.SameSite=Lax: not sent on most requests started by other sites, which blocks most cross-site request forgery.Max-Age: how long it lasts, here 14 days.
Strengths: logging out, or kicking out a stolen session, is instant: delete the record. Simple and well understood. Trade-off: every request looks up the session. In practice that’s a fast key lookup, and rarely a real problem.
Option 2: JWTs (JSON Web Tokens)#
A JWT is a signed piece of JSON. Instead of storing a session, the server signs a token that says who the user is, and checks the signature on each request without looking anything up.
A JWT has three parts separated by dots: header, payload and signature. The payload is only encoded, not encrypted. Anyone holding the token can read it:
const [, payload] = token.split(".");
console.log(JSON.parse(Buffer.from(payload, "base64url").toString()));
// { sub: 'user_42', role: 'admin', exp: 1793000000 }So never put secrets in a JWT. The signature only proves the server issued it and nobody changed it.
Strengths: any server with the key can verify it without a database call, which helps when many separate services need to know who the user is. It’s also the format behind OAuth and “Sign in with…” flows. Trade-off: you can’t easily cancel one. A JWT stays valid until it expires, even after logout or a password change, unless you keep a block list, which brings back the lookup you were avoiding. The usual answer is short-lived access tokens (5 to 15 minutes) plus a longer-lived refresh token that can be revoked.
Rules if you use them:
- Always check the signature and the
exp(expiry) claim, and only accept the algorithm you expect. Use a well-known library such asjoserather than your own code. - Keep access tokens short-lived.
- In a browser, keeping the token in an
HttpOnlycookie is safer thanlocalStorage, which any script on the page can read.
Option 3: API keys#
API keys are for programs, not people: a server calling your API, a script, a partner’s system.
- Generate long random keys, and show each one only once, when it’s created.
- Store a hash of the key, not the key itself, the same way you’d store a password. Then a leaked database doesn’t leak working keys.
- Let each customer have several keys, with names and scopes (“read-only”, “orders only”), so one can be revoked without breaking everything.
- Send them in a header (
Authorization: Bearer ...), never in the URL, where they end up in logs.
And keep them out of frontend code. A key in browser JavaScript is a public key. See calling an external API from Wix Velo without exposing your API key and environment variables in Node.js.
Which one to use#
| Situation | Use |
|---|---|
| A normal website or web app with logins | Session cookies |
| A mobile app talking to your API | Short-lived access token plus refresh token (often JWTs) |
| Many separate services that each need to know the user | JWTs, issued by one auth service |
| “Sign in with Google/Microsoft” | OAuth / OpenID Connect, through a library or provider |
| Another company’s server calling your API | API keys |
If you’re unsure, use sessions, or a hosted provider that handles all of this for you. More teams get hurt by a home-made JWT setup than by the extra database lookup of a session.
Two more protections belong in front of any login form: rate limiting to slow down password guessing, and two-factor authentication for accounts that matter.
If you need member logins, roles or a secure API built into your site, I do that as part of Wix Velo development and custom API integration. For Wix specifically, showing each member only their own data covers the authorisation side.
Comments