You build a student portal, but every login tutorial recommends a different approach. Some use cookies, small values a browser stores and sends back to a website; others use tokens, pieces of data presented as evidence of access. Then “Sign in with Google” adds redirects between websites. Understanding where sessions, JWTs, and OAuth2 fit helps you choose a design and explain what logout actually does.
The core idea: three tools with different jobs
Authentication verifies identity: “This is Aasha’s account.” Authorization decides permissions: “Aasha may read her marks, but cannot edit them.” An application programming interface (API) exposes operations other programs can call, such as fetching those marks.
Technology What it is Question it helps answer
Session A mechanism for remembering login state across requests Has this caller already logged in? JSON Web Token (JWT) A format for carrying claims, or statements about a subject What does this trusted issuer assert? OAuth 2.0, often written OAuth2 A protocol for delegated authorization What may this application access on someone’s behalf?
Think of a campus library. A server-side session resembles a numbered receipt that the librarian checks against a register. A signed JWT resembles a pass with printed information and a seal that can be verified. OAuth2 resembles permission for another person to borrow particular books on your behalf.
Our running example is CampusDesk, a portal where Aasha, account ID student-42, reads her marks. A successful login never grants permission to read every student’s records; CampusDesk must check ownership on each request.
How the login flows work
HTTP, the request-and-response protocol used by browsers and servers, does not remember a previous login. POST submits data; GET retrieves it. Each protected request needs evidence the server can validate. HTTPS adds transport encryption and server authentication to protect the exchange.
Sessions: keep the login record on the server
Here, “session” means a server-side record referenced by an opaque ID: a random value with no meaningful user information inside it.
- Aasha submits her username and password to CampusDesk over HTTPS.
- The server checks the password against its stored password hash, a result from a password-processing function designed to resist guessing.
- After success, it generates a fresh, unpredictable session ID and stores a record mapping that ID to student-42 and an expiry time.
- It sends the ID in a cookie using the HTTP Set-Cookie response header. A header carries information about a request or response.
- The browser sends that cookie on matching requests. CampusDesk looks up the record, checks expiry, and applies permissions.
- Logout deletes the server record and clears the browser cookie.
Aasha's browser CampusDesk Session store | POST /login | | |--------------------------->| Check password | | | Save random ID -> user | | |-------------------------->| |<---------------------------| Set-Cookie: sid=... | | GET /me + Cookie: sid=... | | |--------------------------->| Look up ID, check expiry | |<---------------------------| Aasha's account details |
The ID acts as a temporary credential. Someone who steals it may impersonate Aasha until expiry or invalidation. Regenerating it after login prevents session fixation, where an attacker arranges for a victim to authenticate with an ID the attacker already knows.
JWTs: carry verifiable claims
Suppose CampusDesk adds a marks API. Its login service could issue a signed JWT as an access token, a credential for calling a protected API. The API verifies it without looking up a session record for every request.
A typical signed JWT has three dot-separated parts:
encoded-header.encoded-payload.signature
The header describes the signing algorithm; the payload contains claims. The signature lets the recipient detect changes and verify the token’s source using a trusted cryptographic key. Some algorithms use a shared secret; others use a private, secret key for signing and a public, shareable key for verification. The header and payload use Base64url, a text encoding safe for web addresses. Signed JWTs are normally readable: encoding does not hide contents. Encrypted JWTs exist, but signing alone provides no secrecy. See the JWT specification.
The claims use JSON, JavaScript Object Notation, a text format for structured data. A hypothetical decoded payload could be:
{ "sub": "student-42", "iss": "https://login.campusdesk.example", "aud": "campusdesk-marks-api", "exp": 1700000000 }
sub identifies the subject; iss identifies the issuer; aud identifies the intended audience, here the marks API. exp gives expiry as seconds since the Unix epoch, 00:00 UTC on 1 January 1970. This example is not a usable token.
The client, the program making the request, commonly presents the token through this header:
Authorization: Bearer <access-token>
A bearer token can be used by whoever possesses it. The API verifies the signature, expected issuer and audience, expiry, and other required claims before checking permissions.
JWTs do not require this transport: a JWT can travel in a cookie. Likewise, a bearer access token can be opaque rather than a JWT. Token format and storage are separate decisions.
OAuth2 and OpenID Connect: involve another provider
Suppose Aasha wants CampusDesk to read her external calendar. OAuth2 lets her grant limited calendar access without giving CampusDesk her calendar password. A scope names a requested permission; actual scope names depend on the provider. The provider issues an access token, which CampusDesk presents to that provider’s API. This delegation is defined by the OAuth2 framework.
For “Sign in with Google,” CampusDesk uses OpenID Connect (OIDC), an authentication layer built on OAuth2. OIDC adds an ID token describing the authentication event. An ID token establishes identity for its intended client; an access token permits access to its intended API. See the OIDC specification.
For a browser app with server-side code, login typically uses the authorization code flow, exchanging a temporary code for tokens:
- CampusDesk generates a random state value tied to Aasha’s browser login attempt, plus an OIDC nonce, a one-time value binding the ID token to that attempt.
- It creates a PKCE verifier: a secret random value. PKCE means Proof Key for Code Exchange; the request includes a derived challenge so the provider can later check that the code exchanger knows the verifier.
- It redirects Aasha with state, nonce, the challenge, scopes including openid to request OIDC, and a registered callback address, where the provider returns her browser.
- Aasha authenticates with the provider, which obtains any necessary consent and returns a short-lived, single-use authorization code and state.
- CampusDesk verifies state, then exchanges the code using its verifier. The provider checks the PKCE challenge.
- CampusDesk validates the ID token’s signature, issuer, audience, expiry, and matching nonce. It maps the verified (iss, sub) pair to Aasha’s local account and creates a session.
Use an established OIDC library and PKCE’s S256 method. It derives the challenge using SHA-256, a cryptographic function producing a fixed-length fingerprint. See current OAuth2 security guidance.
Browser and mobile apps cannot safely keep a client secret, a credential authenticating the application. A backend, the application’s server-side code, can protect one. CampusDesk can therefore use OAuth2, an OIDC JWT, and a session together.
Hands-on: run a session login
This exercise needs Python 3, pip, Python’s package installer, and curl, an HTTP client. In Bash, a command shell available on Linux and macOS, create a virtual environment for isolated dependencies. Install Flask, a framework for handling web requests:
python3 -m venv .venv source .venv/bin/activate python -m pip install Flask
Save this as app.py. Werkzeug, installed with Flask, supplies password-hashing helpers; their defaults can change by version. The fixed credentials are teaching fixtures.
import secrets import time from flask import Flask, jsonify, request from werkzeug.security import check_password_hash, generate_password_hash
app = Flask(name) app.config["MAX_CONTENT_LENGTH"] = 4096 sessions = {} ttl = 900 origin = "http://127.0.0.1:8000" password_hash = generate_password_hash("demo-pass-only")
@app.before_request def protect_posts(): if request.method == "POST" and request.headers.get("Origin") != origin: return jsonify(error="Forbidden origin"), 403
@app.after_request def prevent_caching(response): response.headers["Cache-Control"] = "no-store" return response
def current_user(): sid = request.cookies.get("sid") entry = sessions.get(sid) if not entry or entry[1] <= time.time(): sessions.pop(sid, None) return None return entry[0]
@app.post("/login") def login(): data = request.get_json(silent=True) if not isinstance(data, dict) or not isinstance(data.get("password"), str): return jsonify(error="JSON password required"), 400 password_ok = check_password_hash(password_hash, data["password"]) if data.get("username") != "aasha" or not password_ok: return jsonify(error="Invalid credentials"), 401 sessions.pop(request.cookies.get("sid"), None) sid = secrets.token_urlsafe(32) sessions[sid] = ("student-42", time.time() + ttl) response = jsonify(message="Logged in") response.set_cookie("sid", sid, max_age=ttl, httponly=True, samesite="Lax", secure=False, path="/") return response
@app.get("/me") def me(): user = current_user() if user is None: return jsonify(error="Login required"), 401 return jsonify(user=user)
@app.post("/logout") def logout(): sessions.pop(request.cookies.get("sid"), None) response = jsonify(message="Logged out") response.delete_cookie("sid", path="/") return response
if name == "main": app.run(host="127.0.0.1", port=8000, debug=False)
Run it:
python app.py
It reports http://127.0.0.1:8000; startup wording varies by version. In another terminal, run:
curl -sS -c cookies.txt
-H 'Origin: http://127.0.0.1:8000'
-H 'Content-Type: application/json'
-d '{"username":"aasha","password":"demo-pass-only"}'
http://127.0.0.1:8000/login
curl -sS -b cookies.txt http://127.0.0.1:8000/me
curl -sS -b cookies.txt
-H 'Origin: http://127.0.0.1:8000'
-X POST http://127.0.0.1:8000/logout
curl -sS -b cookies.txt -w 'HTTP %{http_code}\n'
http://127.0.0.1:8000/me
Expected output, with JSON whitespace possibly differing:
{"message":"Logged in"} {"user":"student-42"} {"message":"Logged out"} {"error":"Login required"} HTTP 401
-c saves cookies; -b sends them. Reusing the old cookie after logout proves that deleting its server record makes it useless. HTTP 401 means authentication is required or invalid.
An origin is a scheme, hostname, and port. Every POST here requires the exact Origin header to reject requests from other browser origins; missing it produces 403, meaning access is refused. This check does not authenticate clients: curl can supply the header.
Sessions expire 900 seconds after login; activity does not extend that deadline. Restarting loses every session. Production needs an appropriate server and session store, HTTPS, secure=True, limits on login attempts, and session cleanup. Multiple server processes need shared storage. Secure-only cookies are disabled solely for local HTTP.
Tradeoffs: choose for your actual problem
Approach Use it when Avoid choosing it when Alternative
Server-side session CampusDesk controls its browser interface and server; immediate logout matters A third-party client needs standardized delegated API access OAuth2 for delegation, sessions for browser login Signed JWT access token Several APIs need to verify a trusted issuer’s tokens locally Immediate permission changes or revocation matter more than avoiding lookups Opaque access tokens checked against server state OAuth2 with OIDC for login You need external identity, shared organizational login, or delegated access A small local-password app gains no benefit from provider integration Local authentication and sessions
Sessions can scale through a shared store. Its lookup adds a dependency; central state makes invalidation straightforward.
JWT verification can be stateless, meaning it needs no per-user session lookup. However, a valid JWT can retain outdated permissions until expiry. Immediate revocation, making a previously valid token unusable, generally requires additional state or coordination.
Short-lived access tokens limit exposure. A refresh token obtains replacement access tokens without another login; it is a powerful credential requiring secure storage, revocation, and often rotation, which replaces it after use. Deleting a JWT from Aasha’s device does not invalidate an attacker’s copy.
OAuth2 adds configuration, token handling, and a service dependency. Often, the backend can manage provider tokens while the browser holds a session cookie.
Common security mistakes and their fixes
- Trusting decoded JWT data
Reading sub proves nothing. Fix: use a maintained verification library, explicitly allow trusted signing algorithms and keys, require expected claims, and validate issuer, audience, expiry, and token purpose. Never accept unsigned tokens for authentication. Follow JWT security guidance.
- Storing secrets where scripts can read them
Cross-site scripting (XSS) lets attacker-controlled JavaScript run inside your website. It can read tokens in localStorage, persistent browser storage accessible to scripts. Fix: for many browser apps, prefer an HttpOnly session cookie, which scripts cannot read. Prevent XSS too: HttpOnly does not stop malicious scripts from sending authenticated requests. Keep passwords and sensitive student details out of readable JWTs.
- Assuming cookies prevent forged requests
Cross-site request forgery (CSRF) tricks a browser into sending an unwanted authenticated request because cookies travel automatically. Fix: use your framework’s CSRF defenses for state-changing actions. SameSite limits cookie sending across sites; Secure restricts cookies to HTTPS. Choose appropriate settings rather than relying on either alone. Never change account data through GET, which should retrieve information. See OWASP’s session guidance.
- Keeping credentials alive accidentally
Reusing an ID after login enables fixation; clearing only local credentials leaves stolen copies usable. Fix: rotate session IDs after login and permission changes, invalidate sessions server-side, enforce expiry, and define access-token and refresh-token logout behavior. Keep passwords and tokens out of logs, and access tokens out of URLs.
- Building provider login from bare OAuth2
An access token for a calendar is not automatically proof of identity to CampusDesk. Fix: use OIDC and validate the ID token. Register exact callback addresses and check request-bound values. Avoid new designs using the implicit flow, which returns access tokens through the browser redirect, or the password grant, which gives the application the user’s provider password.
- Ignoring password and permission checks
Plaintext passwords and missing permission checks expose accounts and records. Fix: use suitable password-hashing functions with a salt, random per-password data preventing identical passwords from producing identical stored hashes. Limit login attempts and enforce record ownership on every request. Use HTTPS throughout production.
Quick recap
- Sessions remember login state; JWTs define a token format; OAuth2 delegates access.
- OIDC adds authentication to OAuth2 and can lead to a local session.
- Signed JWTs are usually readable; verify them before trusting claims.
- Sessions support immediate invalidation; JWT revocation requires deliberate design.
- Every approach needs safe credential handling and separate authorization checks.
Test yourself
- Can CampusDesk use all three technologies?
Answer: Yes. It can use an OAuth2-based OIDC flow, validate an ID token formatted as a JWT, and create a server-side session for browser requests.
- Aasha deletes her JWT after logout. Is a stolen copy invalid?
Answer: No. It may remain valid until expiry unless the API implements revocation or another check that rejects it.
- A valid token identifies Aasha. May the API return any student’s marks?
Answer: No. Authentication identifies the caller; authorization must check which records that caller may access.
Where to go next
- Browser security: practice cookie settings, CSRF defenses, and XSS prevention together.
- API authorization: compare roles, which group permissions, with record-level ownership checks.
- Account recovery and passkeys: learn how password resets and device-based cryptographic credentials affect the complete account lifecycle.