Library
00/07 · ~36 min
GUIDEDECK · for letting the right people in

Authentication
& Authorization
done properly.

A 36-minute working session on proving who a user is and deciding what they may do — from password hashing and sessions vs tokens, through OAuth 2.0 and OpenID Connect, to RBAC, the standards, and the tools that implement them.

~36 MINBEGINNER → INTERMEDIATELANGUAGE-AGNOSTIC
SCROLL
01 · Two different questions 4 min

Who are you? is not the same as what may you do?

Almost every security bug starts by blurring these two. They run in a fixed order: a system first establishes identity, and only then checks permission. Get the order — and the words — right, and the rest of this session clicks into place.

Authentication (AuthN) — proving you are who you claim to be. It answers who are you? via something you know (a password), something you have (a phone, a passkey), or something you are (a fingerprint). Authorization (AuthZ) — deciding what an identity is allowed to do. It answers are you allowed to do this? and always happens after authentication.
request a user AuthN who are you? identity gate AuthZ may you? permission gate 200 fail → 401 fail → 403

Identity first, permission second. A failed identity check returns 401 Unauthorized; a failed permission check returns 403 Forbidden.

The airport analogy

  • Passport control proves who you are— that's authentication.
  • Your boarding pass says which flight and seat you may board— that's authorization.
  • A valid passport gets you into the airport; it does not let you sit in first class. Different checks, different failures.

Naming note  the HTTP 401status is literally called "Unauthorized" but actually means unauthenticated — a historical quirk worth knowing.

The three factors of authentication

knowledge

Something you know

A password, PIN, or recovery phrase. Cheap and universal — but reusable, phishable, and guessable. Never the only factor for anything that matters.

possession

Something you have

A phone running an authenticator app, a hardware security key, or a passkey on your device. A stolen password alone no longer gets the attacker in.

inherence

Something you are

A fingerprint or face scan. Convenient and hard to share — but you can't change your face if the template leaks, so treat biometrics as a local unlock, not a server secret.

Multi-factor authentication (MFA) simply means requiring two of these from different categories — typically a password plus a phone or passkey. It is the single highest-leverage control you can add to a login.

02 · Passwords done right 5 min

Never store a password.
Store proof you've seen it.

A breach is not if but when. The only question is what an attacker finds in your database. Store passwords correctly and a full dump is nearly worthless to them. This is the most common — and most damaging — thing teams get wrong.

Hashing running input through a one-way function that turns any password into a fixed-length fingerprint you cannot reverse. At login you hash what the user typed and compare fingerprints — so you verify the password without ever storing it. This is not encryption: there is deliberately no key to decrypt it back.
What attackers love to find
// plaintext — one breach and every account is gone id | email | password 1 | dani@work.io | Summer2024! // or fast hashes — cracked in minutes on a GPU sha256("Summer2024!") // no salt, billions/sec md5("Summer2024!") // broken — never use
What they should find
// a slow, salted hash — one row, one column id | email | password_hash 1 | dani@work.io | $argon2id$v=19$m=65536,t=3,p=4$ | c29tZXNhbHQ$R7x...k9Q // the algorithm, cost, salt & hash live together
Salt a unique random value added to each password before hashing. Because every user gets a different salt, two people with the same password get different hashes, and an attacker can't precompute a giant lookup table (a rainbow table) to crack everyone at once. The salt is stored alongside the hash — it is not a secret, just an anti-bulk-cracking device.
import { hash, verify } from "argon2" // on sign-up: salt is generated & embedded for you const stored = await hash(plaintext) // on login: re-hash the attempt & compare safely const ok = await verify(stored, attempt) if (!ok) throw Error("invalid credentials")
password + random salt argon2id slow · one-way $argon2id$...$hash stored in DB

Password + unique salt → slow one-way hash → store the whole string. A good library does all three steps for you.

Which algorithm?

argon2id

Today's default

Winner of the Password Hashing Competition; tunable on memory and time, which resists GPU and ASIC cracking. The recommended first choice for new systems.

bcrypt

The proven workhorse

Decades of battle-testing and available everywhere. Perfectly fine today; note the ~72-byte input cap. A safe, boring choice.

scrypt / PBKDF2

Acceptable fallbacks

Use when a compliance regime mandates them. PBKDF2 needs a high iteration count to stay slow — the whole point is to be deliberately slow.

The non-negotiables (OWASP-aligned)

  • Never store plaintext, and never use fast hashes (MD5, SHA-1, SHA-256) for passwords — they are built for speed, the opposite of what you want here.
  • Compare hashes with a constant-time check (the library's verify), never a plain ==, to avoid timing leaks.
  • Return the same errorfor "no such user" and "wrong password" so you don't reveal which emails are registered.
  • Add rate limiting and MFA on login; check new passwords against known-breached lists rather than enforcing odd character rules.
03 · Staying logged in 6 min

After login, how does the next
request remember you?

HTTP is stateless — each request arrives as a stranger. So after a successful login we hand the browser something to present on every later request. There are two dominant designs, and the trade-off between them shapes your whole architecture.

Server session the server stores the login state and gives the browser only an opaque ID (in a cookie). Token (JWT) the server hands the client a signed, self-contained credential carrying the user's identity and claims, and keeps no record of it. One trusts a lookup; the other trusts a signature.
Session — state on the server
browser cookie=abc server session store abc → user 7 sends id look up

The cookie is a meaningless ID; the truth lives in a store the server controls — so logging someone out is a single delete.

Token — state in the client
browser JWT (signed) server verify(sig) no DB hit sends token check key

The token carries its own claims; the server only checks the signature — fast and stateless, but you can't un-issue it.

Anatomy of a JWT

  • Header — the algorithm and token type.
  • Payload — the claims: who the user is (sub), when it expires (exp), and any roles or scopes.
  • Signature — proof the server issued it and nobody edited it.
  • Read me: the payload is only Base64-encoded, not encrypted — anyone can decode it, so never put secrets inside.
// three Base64 parts joined by dots eyJhbGciOiJIUzI1NiJ9 // header .eyJzdWIiOiI3IiwiZXhwIjoxNz...} // payload .R7xK9Q...signature // signature // decoded payload (the claims): { "sub": "7", "role": "editor", "exp": 1717999999 }

A JWT is header.payload.signature. Change one byte of the payload and the signature no longer matches.

Pick the model on purpose

session wins

Instant logout

Revocation is a delete in the store. Ideal for classic web apps, banking, anything where "kick this device out now" must be immediate.

token wins

Scale & many services

No shared session store; any service can verify a request with just the public key. Great for APIs, mobile, and microservices.

the catch

Tokens are hard to revoke

A leaked JWT is valid until it expires. The fix: keep access tokens short-lived (minutes) and pair them with a stored, revocable refresh token.

Where you store it matters more than which you pick. Prefer an HttpOnly, Secure, SameSite cookie so JavaScript can't read it (defends against XSS token theft). Putting a JWT in localStorage is the classic mistake — any injected script can steal it.

04 · Delegated auth & social login 6 min

"Log in with Google" — without
Google ever sharing your password.

OAuth and OIDC sound intimidating, but the core idea is simple: let a user grant one app limited access to their account on another, using a temporary key instead of a password. Once you see the four roles, the flow reads like a sentence.

OAuth 2.0 a standard for delegated authorization: it lets an app get a scoped access tokento act on a user's behalf, without ever seeing their password. OpenID Connect (OIDC) a thin identity layer on top of OAuth 2.0 that adds an ID tokenanswering "who is this user?". OAuth is about access; OIDC is about login.

The four roles

resource owner

You

The user who owns the account and grants access.

client

The app

The application that wants access — e.g. a photo printing site.

authorization server

The issuer

Logs the user in and issues tokens — e.g. Google's login.

resource server

The API

Holds the data and accepts the access token — e.g. Google Photos API.

user app (client) auth server resource server (API) 1 click login 2 redirect + scopes 3 user consents → code 4 code + secret → tokens 5 call API w/ access token

The Authorization Code flow: the app never sees the password — only a short-lived code it swaps (with its secret) for tokens.

Read the flow

  • The user clicks "Log in with Google" and is redirected to Google with a list of requested scopes.
  • The user authenticates and consentson Google's own page; Google sends the app a one-time authorization code.
  • The app exchanges that code (plus its client secret) for an access token and, with OIDC, an ID token.
  • Use PKCEfor mobile and single-page apps — it secures the exchange when there's no safe place to keep a secret.
access token

For calling APIs

A scoped key the app sends to the resource server. It says what the bearer may do (scope: photos.read) — not who they are. Keep it short-lived.

ID token (OIDC)

For knowing the user

A JWT describing who logged insub, email, name. This is what powers social login; your app reads it to create or match a local account.

Like a hotel: reception (auth server) checks your ID and gives you a room key card (access token) that opens only your room and the gym — not the safe, not other rooms — and expires at checkout. You never hand the cleaner your passport.

05 · Modeling permissions 5 min

Once you know who, how do you
decide what they may do?

Authorization needs a model, not a pile of if checks scattered through your code. Two approaches dominate; most real systems start with the first and add a pinch of the second as rules get nuanced.

RBAC Role-Based Access Control: permissions are attached to roles, and users are given roles. ABAC Attribute-Based Access Control: a policy decides at request time using attributes of the user, the resource, and the context (time, location, ownership). RBAC asks what role are you?; ABAC asks do your attributes satisfy the policy?.
RBAC — roles → permissions
// assign roles to users; permissions to roles user.roles = ["editor"] const can = { editor: ["post.read", "post.write"], admin: ["post.*", "user.manage"], } if (!allows(user, "post.write")) throw Forbidden
ABAC — policy on attributes
// decide from attributes & context, per request allow if action == "post.edit" and resource.ownerId == user.id and // ownership user.dept == resource.dept and // attribute now() < resource.lockAt // context
Dani Sam Lee editor admin post.write post.read user.manage

RBAC: users → roles → permissions. Change what an "editor" can do once, and every editor updates.

How to choose

  • Start with RBAC. It is simple, auditable, and covers the vast majority of apps. A few coarse roles go a long way.
  • Add ABAC rules when permissions depend on this specific resource— "owners can edit their own posts", "only during business hours".
  • Apply least privilege: grant the minimum, and deny by default — if no rule allows it, it's forbidden.
  • Enforce authorization on the server, every request. Hiding a button in the UI is not access control.
Scopes vs claims — two words you'll meet constantly. A scope is a coarse permission an access token carries (photos.read) — what the token may do. A claim is a statement about the user inside a token (role: admin, email_verified: true) — facts your authorization logic reads to make its decision.
06 · The tooling landscape 5 min

Don't hand-roll auth.
Pick the right partner.

Authentication is a solved problem with sharp edges — rolling your own means re-discovering every vulnerability the industry already patched. The real decision is which standard to speak and which tool to lean on. Here are the leaders and where each fits.

The standards, briefly. OAuth 2.0 = delegated access (API authorization). OIDC = identity/login built on OAuth 2.0 — use it for modern web and mobile sign-in. SAML = the older XML-based SSO standard — still everywhere in enterprise and universities, so support it when you sell to large organizations.

Auth0 (Okta) — the flexible incumbent

Pro
Mature, standards-complete, and extremely customizable (rules, every protocol, enterprise SSO).
Con
Pricing climbs fast at scale and the breadth of options can overwhelm a small team.

Choose it when you need broad protocol support and enterprise features and would rather configure than build.

Clerk — batteries-included for modern apps

Pro
Drop-in pre-built UI components, excellent React/Next.js fit, fast to ship.
Con
Younger ecosystem and most opinionated toward the JavaScript/edge stack.

Choose it whenyou're building a modern web app and want login screens, MFA, and user management in an afternoon.

Firebase Authentication — quick & mobile-friendly

Pro
Generous free tier, dead-simple social and phone login, great for mobile and MVPs.
Con
Ties you to Google's ecosystem; thinner on enterprise SSO and fine-grained authorization.

Choose it whenyou're shipping a mobile app or prototype and already live in Firebase/GCP.

Keycloak — open-source, self-hosted

Pro
Free, no per-user fees, full OAuth/OIDC/SAML, and your data stays on your servers.
Con
You own the operations — patching, scaling, availability, and upgrades.

Choose it when data residency, cost at scale, or no vendor lock-in outweigh the operational burden.

Build your own (with a library)

Pro
Total control and no per-user cost; libraries like Auth.js, Passport, or Spring Security do the heavy lifting.
Con
You inherit responsibility for every edge case — hashing, sessions, resets, MFA, breaches.

Choose it when requirements are simple and stable, or constraints rule out a hosted provider. Use a vetted library — never raw crypto.

How to choose, in one breath

  • Speak OIDC for user login and OAuth 2.0 for API access; add SAML only when an enterprise customer requires it.
  • Hosted provider (Auth0, Clerk, Firebase) buys speed and offloads risk — almost always right for a new product.
  • Self-host (Keycloak) when cost-at-scale, data residency, or lock-in concerns dominate.
  • Build-your-ownonly with a vetted library, simple needs, and eyes open to the maintenance you're signing up for.
07 · A worked flow & recap 5 min

One request, end to end.

Let's thread everything together: a user logs in, then tries to edit a post. Watch both gates do their job — identity, then permission — before the request is allowed through.

1 login form 2 verify hash 3 issue token AuthN verify signature AuthZ owner? role? edit post 200 OK later: PATCH /posts/42 + token

Login proves identity once and issues a token; every later request re-checks the token (AuthN) and the policy (AuthZ).

// runs on every protected request function guard(req) { const user = verifyToken(req.cookies.session) if (!user) throw Unauthorized // 401 · AuthN const post = load(req.params.id) if (post.ownerId !== user.id && !user.roles.includes("admin")) throw Forbidden // 403 · AuthZ }

Both checks live on the server. The token answers who; the ownership/role check answers may they.

Five rules to walk out with

1Authenticate, then authorize. Who you are (401) is a different question from what you may do (403).
2Never store passwords — store slow, salted hashes (argon2id or bcrypt). Add MFA and rate limiting.
3Sessions for instant revocation, tokens for scale. Keep access tokens short-lived in HttpOnly cookies.
4Use OIDC for login, OAuth 2.0 for API access. Let the auth server handle passwords; your app reads tokens.
5Start with RBAC and least privilege, enforced on the server. Don't hand-roll — pick a trusted provider or library.
Knowledge check

Did it stick?

Five quick questions on authentication, hashing, tokens, OAuth/OIDC, and authorization — instant feedback, no sign-in.

Rate this deck
be the first

Navigate with ← → or scroll · back to library