Education › Security › Stage 1: Foundations

Identity & access

Authentication vs authorisation, MFA, sessions and tokens, OAuth 2.0 and OIDC, least privilege.

Beginner ~35 min read Module 3 of 16

Most breaches are not about breaking encryption; they are about logging in. A phished password, a token that never expires, a service account with admin rights nobody remembers granting. Identity and access management is where those doors are, and it is the control that pays back fastest when done well. This module separates the two questions every request must answer (who are you, and what may you do), shows how modern web authentication actually works with sessions, tokens, OAuth 2.0 and OpenID Connect, and lays out the least-privilege habits that keep a compromised identity from becoming a compromised company.

After this module you can
  • Distinguish authentication from authorisation and place each correctly in a request's life
  • Explain how sessions, bearer tokens and JWTs carry identity, and the trade-offs between them
  • Describe the OAuth 2.0 authorization code flow with PKCE and what OpenID Connect adds
  • Choose MFA methods by phishing resistance and apply them where they matter most
  • Design roles and policies that follow least privilege, for humans and for services

Two questions, two failures

Authentication (authn) answers "who are you?" and produces an identity: a user id, a service name. Authorisation (authz) answers "is this identity allowed to do this to that?" and produces a yes or no. They fail differently. A weak authentication lets a stranger become someone; a weak authorisation lets a legitimate someone do things they should not. The single most common web vulnerability for years has been broken authorisation: an endpoint checks that you are logged in, then forgets to check that the record you asked for is yours.

The clean design puts authentication at the edge once (a gateway or middleware validates the credential and attaches an identity to the request) and authorisation next to every data access, where the code knows what the resource is and who owns it. Authorisation that lives only in the UI ("the button is hidden") is not authorisation; the API can be called directly.

Authorisation next to the data, not next to the login: the query itself is scoped to the caller.
python
def get_order(order_id: int, caller: User) -> Order:
    # Wrong: fetch, then hope someone checks ownership later
    #   order = db.get(Order, order_id)
    # Right: the identity is part of the lookup, so a foreign id simply does not exist
    order = db.query(Order).filter(Order.id == order_id, Order.user_id == caller.id).first()
    if order is None:
        raise NotFound()          # 404, not 403: do not confirm that the id exists
    return order
Tip

Return the same 404 whether the record does not exist or belongs to someone else. A 403 tells an attacker that they guessed a valid id.

Sessions, tokens and where identity travels

After a successful login the server needs a way to recognise the user on later requests. Two families exist. A session is a random opaque id stored in a cookie; the server keeps the session data (user id, expiry) in a store and looks it up per request. Revocation is trivial (delete the row) and nothing sensitive leaves the server. A bearer token, typically a JWT, carries the claims itself, signed by the issuer; any service with the public key can verify it without a lookup, which scales across many services but makes revocation hard — the token is valid until it expires.

PropertyServer session (cookie)Signed token (JWT)
RevocationImmediate: delete the sessionOnly via short expiry or a denylist
Cross-service useNeeds a shared session storeVerified anywhere with the public key
What leaks if stolenAn opaque idThe claims, readable by anyone
Typical lifetimeHours to days, slidingMinutes; refreshed with a refresh token

For cookies, set HttpOnly (JavaScript cannot read it), Secure (HTTPS only) and SameSite=Lax or Strict (blocks most cross-site request forgery). For tokens, keep access tokens short-lived (five to fifteen minutes), issue a longer-lived refresh token that can be revoked, and store tokens where cross-site scripts cannot reach them. In browsers that usually means an HttpOnly cookie rather than local storage. Whatever carries identity, treat it as a credential: never in URLs, never in logs.

Note

A JWT's payload is base64, not encrypted. Anyone who sees the token can read the claims. Put identifiers in it, not secrets or personal data you would not want in a log.

OAuth 2.0 and OpenID Connect

OAuth 2.0 is a delegation protocol: it lets an application obtain limited access to a user's resources at another service without ever seeing the user's password. OpenID Connect (OIDC) is a thin layer on top that turns the same flow into authentication, adding an ID token (a JWT describing the user) and a standard userinfo endpoint. When you "sign in with" a corporate identity provider, that is OIDC.

The flow you should use for web and mobile applications is the authorization code flow with PKCE. PKCE (Proof Key for Code Exchange) binds the code to the client that started the flow, which closes the code interception attacks that made the older implicit flow unsafe.

  1. The app generates a random code_verifier, hashes it into a code_challenge, and redirects the browser to the identity provider's authorize endpoint with the challenge, the requested scopes, a state value and its redirect_uri.
  2. The user authenticates at the provider (password, MFA, whatever policy applies). The provider redirects back to the redirect_uri with a short-lived, single-use authorization code and the same state.
  3. The app checks state matches what it sent (this is the CSRF protection), then exchanges the code plus the original code_verifier at the token endpoint, over TLS, for an access token, an ID token and usually a refresh token.
  4. The app validates the ID token: signature against the provider's published keys, iss, aud (must be this app's client id), exp, and the nonce if one was sent. Only then is the user considered signed in.
YOUR APPLICATIONIDENTITY PROVIDER1. redirect · 4. callback2. sign in (MFA)3. code + state5. code + verifier6. id, access, refresh7. validate, sign inBrowsercarries only the codeApp serverholds code_verifier/authorizelogin + MFA/tokenchecks verifierSessionHttpOnly cookie
The authorization code flow with PKCE: the browser only ever carries a short-lived, single-use code; the app exchanges it server-side, with the verifier, for tokens that the browser never sees.
The two redirects of the authorization code flow, with the parameters that matter.
text
1. Browser -> Identity provider
   GET /authorize?response_type=code&client_id=web-app&scope=openid%20profile%20email
       &redirect_uri=https://app.example.com/callback&state=k3j2...&code_challenge=E9Mr...
       &code_challenge_method=S256

2. Identity provider -> Browser -> App
   GET https://app.example.com/callback?code=SplxlOBe...&state=k3j2...

3. App (server side) -> Identity provider
   POST /token  grant_type=authorization_code&code=SplxlOBe...&redirect_uri=...
                &client_id=web-app&code_verifier=dBjftJeZ...
   <- { access_token, id_token, refresh_token, expires_in: 600 }

Scopes describe what the access token may do (read:orders), and the resource server enforces them; a token with the wrong audience or missing scope must be rejected. Use a maintained library for every step, and register exact redirect_uri values at the provider: a wildcard redirect is an open door for token theft. Machine-to-machine calls use the client credentials grant (no user involved), and CI systems increasingly use OIDC federation to obtain cloud credentials without stored keys, which the DevOps track covers under secrets.

MFA, passwords and phishing resistance

Passwords will be phished, reused and leaked. Multi-factor authentication adds something the attacker does not have, but factors differ enormously. SMS codes can be intercepted by SIM-swapping and relayed by phishing pages in real time. Time-based one-time codes (TOTP apps) resist SIM swaps but not real-time phishing: the victim types the code into the fake page and the attacker uses it within thirty seconds. Push notifications suffer from MFA fatigue, where the attacker triggers prompts until the victim taps approve. Only phishing-resistant methods — FIDO2 security keys and platform passkeys — bind the login to the genuine site's origin, so a lookalike domain receives nothing usable.

  • Require MFA on everything reachable from the internet: email, source control, cloud consoles, VPN, the identity provider itself.
  • Prefer passkeys or security keys for administrators and anyone with production access; allow TOTP as a fallback, avoid SMS.
  • Enforce number matching or similar on push approvals to blunt fatigue attacks.
  • For passwords: minimum length (12+), check against breached-password lists, no forced periodic rotation, no composition rules that produce Summer2026!.
  • Rate-limit and alert on failed logins per account and per source; lock out slowly, not instantly, to avoid self-inflicted denial of service.
Watch out

Recovery flows are part of authentication. A strong login with a "reset via email" path means your security equals your email security, and a support desk that resets MFA over the phone is the attacker's favourite entry point.

Least privilege for humans and machines

Least privilege means every identity has exactly the permissions its current task needs, and no more. It limits the blast radius when (not if) a credential is stolen. In practice you achieve it with role-based access control (RBAC): permissions are attached to roles, roles to identities, and both are reviewed. Attribute-based rules (time of day, device posture, resource tags) refine it where roles are too coarse.

Machines need identities too, and they are where privilege creep hides. A CI runner with admin because a deploy failed once; a service account shared by six services because nobody wanted to create seven. Give every workload its own identity, scope it to the resources it touches, prefer short-lived credentials obtained at runtime (cloud instance roles, Kubernetes service account tokens, OIDC federation) over long-lived keys, and log every use so anomalies stand out.

A cloud IAM policy that says what it means: one action set, one resource, one condition. Compare with the wildcard version it replaces.
json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadInvoicesForThisService",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": ["arn:aws:s3:::acme-invoices", "arn:aws:s3:::acme-invoices/2026/*"],
      "Condition": {"Bool": {"aws:SecureTransport": "true"}}
    }
  ]
}

Grant temporarily where you can: just-in-time elevation for production access, approved and time-boxed, is far safer than standing admin rights. Review access on a schedule, and treat a role nobody can explain as a bug. When someone leaves or changes team, their access should change the same day; the identity provider should be the one place where that happens, with every system federated to it.

Hands-on practice

Inspect a real login and tighten a policy

  1. Sign in to a site that uses "Sign in with Google/GitHub/Microsoft" with your browser's developer tools open on the Network tab. Find the /authorize redirect and identify client_id, scope, state, redirect_uri and code_challenge in the URL.
  2. Find the callback request and confirm the state value matches. Note that you never see a token in the browser: the code exchange happens server side.
  3. Paste an ID token (from a test provider, or one you issue yourself with a library) into a local decoder script: split on ., base64-decode the payload, and list iss, aud, exp, iat. Do not paste real tokens into third-party websites.
  4. Take an IAM policy from a project you have access to (or the DevOps track's zero-to-production project) and rewrite any "Action": "*" or "Resource": "*" to the specific actions and resources actually used. Apply it in a test account and confirm the workload still works.
  5. Enable a passkey or security key on your own GitHub account and your cloud console, and demote SMS to a recovery method or remove it.
  6. Write down every place in a service you know where identity is checked. For each, note whether it is authentication or authorisation, and whether the authorisation check lives next to the data access.
Cheat sheet

Identity & access — at a glance

Main things to focus on

  • Authn = who are you (once, at the edge); authz = may you do this (every data access)
  • Broken authorisation is the top web vulnerability: scope every query by the caller
  • Sessions revoke instantly; JWTs scale across services but live until expiry — keep them short
  • Use authorization code + PKCE; validate state, signature, iss, aud, exp, nonce
  • Phishing-resistant MFA (passkeys, security keys) for anything that matters; avoid SMS
  • Every workload gets its own identity with short-lived credentials
  • Least privilege is specific actions on specific resources, reviewed and time-boxed

Cookies and tokens

Set-Cookie: sid=...; HttpOnly; Secure; SameSite=LaxSession cookie that scripts cannot read and cross-site forms cannot send
Authorization: Bearer <token>Token in a header; never in a query string
access token 5-15 min + refresh tokenShort exposure, revocable renewal
JWT = header.payload.signature (base64url)Readable payload; verify signature before trusting
verify iss, aud, exp, nbf, alg (pinned)The claims every verifier must check

OAuth 2.0 / OIDC

response_type=code + code_challenge (S256)Authorization code flow with PKCE, for web and mobile
stateRandom per request; must match on callback (CSRF)
grant_type=authorization_code + code_verifierServer-side exchange at the token endpoint
grant_type=client_credentialsMachine-to-machine, no user
id_token vs access_tokenWho the user is vs what the app may call
scope=openid profile emailopenid makes it OIDC; others request claims or API access

MFA by strength

passkeys / FIDO2 security keysOrigin-bound; phishing resistant; use for admins
TOTP appGood fallback; phishable in real time
push with number matchingBlunts MFA fatigue
SMSSIM swap and relay; last resort only
recovery: codes stored offline, no phone resetsRecovery is part of the login

Least privilege checks

no Action: * / Resource: *Name the actions and the resources
one identity per workloadBlast radius and attribution
short-lived credentials (instance roles, OIDC)Nothing to leak that lasts
just-in-time elevation, time-boxedNo standing production admin
quarterly access review; leaver = same-day removalFederate everything to the identity provider

Common pitfalls

  • Checking "is logged in" but not "owns this record", the classic insecure direct object reference.
  • Storing JWTs in browser local storage where any injected script can read them.
  • Registering a wildcard redirect URI at the identity provider; authorization codes can then be sent anywhere.
  • Treating SMS codes as strong MFA, or leaving the recovery path weaker than the login.
  • Sharing one service account across many services, so nobody can tell who did what or scope anything down.
  • Letting access accumulate; a role nobody can explain is a finding waiting to happen.
Quiz

Check your understanding

5 questions · 4 to pass · answers are explained as you go. Your best score is saved on this device only.

Progress and quiz scores are saved in this browser only. Back up or restore on the hub.

Was this lesson useful? Tell me what to improve →