HOST_A: Welcome to Clawd Talks. I'm Emma, and today we are diving deep into something that every engineer touches every single day but that somehow still causes massive production incidents and security breaches. Authentication and authorization. HOST_B: I'm Ryan, and I want to kick this off with a story. 2022. Okta gets breached. Not their main system — a third-party support tool. An attacker sits inside that support tool for nearly three months. And the kicker? The initial vector was a compromised session token. Not a password. A session token. That is the thing that is supposed to be the proof you already authenticated. HOST_A: And that is such a perfect example of why auth is hard. Because there are so many layers — who you are, what you're allowed to do, how long that permission lasts, what happens when a token gets stolen. Before we get into the weeds, let's define the two words everyone conflates. HOST_B: Authentication versus authorization. Please, let's say this clearly because I have reviewed code where people swap these terms in comments and it is genuinely confusing. HOST_A: Authentication — authn — answers "who are you?" You're presenting a credential, the system is verifying it. Could be a password, a hardware key, a biometric. At the end of authn, the system says: "Okay, I know you are Alice." HOST_B: Authorization — authz — answers "what can Alice do?" Now that we know you're Alice, are you allowed to read this file? Delete this record? Access this admin panel? Totally separate question. You can be perfectly authenticated and still not authorized. And crucially, you can have a bug in either one completely independently. HOST_A: Classic confusion: someone sets up a JWT, they check the signature — that's authn — but they forget to check the "role" claim inside — that's authz. The token is valid but the user can now do things they shouldn't. HOST_B: Alright. Let's start at the foundation. Sessions. Old school, stateful, but still very much alive. Walk me through how a traditional session actually works. HOST_A: You log in, submit username and password. Server verifies. Server creates a session record in a database or in-memory store — Redis, for example. That record has a session ID, the user ID, maybe a timestamp, any other context. The server sends the session ID back to the browser as a cookie. That's it. Every subsequent request, the browser sends that cookie, the server looks up the session ID in the store, finds the user, and proceeds. HOST_B: The beauty of this is invalidation. The session is in the store. You want to log someone out? Delete the record. Session's gone. Immediately. No waiting. No token expiry windows. You want to expire all sessions for a user after a password change? Delete all records where user_id equals that user. Done. HOST_A: The cost is scale. Every request hits the session store. You're adding latency, you're adding a network hop, and if Redis goes down, everyone's suddenly logged out. So in distributed systems, people started looking at stateless tokens. HOST_B: Which brings us to JWTs. JSON Web Tokens. And I have a lot of feelings about JWTs. HOST_A: Everyone does. Okay, how do they actually work? Because a lot of people think of them as magic black boxes. HOST_B: A JWT is three base64url-encoded JSON objects separated by dots. Header, payload, signature. The header specifies the algorithm — say, HS256 for HMAC with SHA-256, or RS256 for RSA. The payload is your claims — "sub" is the subject, typically user ID. "exp" is expiration. "iat" is issued at. Custom claims like "role" or "org_id". The signature is a cryptographic hash of the header and payload, computed using your secret key or private key. HOST_A: So the receiver can verify the signature without calling any central store. They just re-compute the hash with their key and check it matches. That's where the stateless part comes from. And why JWTs can scale horizontally — any node with the key can validate. HOST_B: Now here's the famous alg colon none attack. And it is a perfect illustration of what happens when you trust user input without thinking. In the JWT header, the algorithm field is specified by the token itself. Early JWT libraries would read "alg" from the header and use it for verification. So an attacker could craft a token, set alg to "none", remove the signature, and some libraries would say "no algorithm means no verification needed, the token is valid." HOST_A: That is horrifying. And it happened in production systems. The fix is to always specify which algorithms you accept when verifying. Never trust the alg in the header. Whitelist RS256 or HS256 on the server side. Never accept "none". HOST_B: The other classic mistake: storing JWTs in localStorage. People do this because it's simple. But localStorage is accessible to any JavaScript running on the page. If you have an XSS vulnerability — even a tiny one in a third-party script — an attacker can exfiltrate the token. Game over. HOST_A: The better approach is HttpOnly cookies. A cookie with the HttpOnly flag cannot be read by JavaScript at all. The browser sends it automatically but no script can access it. Combined with Secure, which enforces HTTPS, and SameSite, which we'll talk about in a minute, cookies are actually more secure than localStorage for tokens. HOST_B: But then there's the invalidation problem. A JWT is valid until it expires. There's no central store to delete from. You can implement a token blacklist — check a revocation list on every request — but now you have state again and you've lost the main benefit of JWTs. HOST_A: This is the fundamental tension. Stateless means you can't revoke without adding state back. The pragmatic answer is: keep access token lifetimes short. Like 15 minutes. Then you have a refresh token for getting new access tokens. HOST_B: Right. Let's talk about refresh token rotation, because this is actually a really elegant solution. You have two tokens. The access token is short-lived — 15 minutes. The refresh token is long-lived — days or weeks — but can only be used once. HOST_A: Here's how it works. You authenticate, you get both tokens. Access token expires after 15 minutes. Your client uses the refresh token to get a new access token. When it does, the server issues a new refresh token too and invalidates the old one. This is rotation — each use generates a fresh pair. HOST_B: The security property is: if an attacker steals a refresh token, they can use it once, but that use invalidates it. And if the legitimate user's client then tries to use the same (now stolen) token, the server sees a reuse attempt. Most servers respond by revoking all refresh tokens for that user — which is a good heuristic that something's wrong. HOST_A: The silent refresh pattern is how SPAs handle this transparently. A background timer fires before the access token expires, silently calls the token endpoint with the refresh token, and swaps in the new access token. User never sees a login prompt. HOST_B: Okay, now let's get into the big one. OAuth 2.0. Because OAuth is genuinely complex, there are multiple flows, and people misuse them constantly. HOST_A: OAuth 2.0 is a framework for delegated authorization. Key word: delegated. You're not just logging in. You're telling a third-party application "I authorize you to act on my behalf for these specific things." The classic example: you're using a photo printing app and you want it to access your Google Photos. You don't give the printing app your Google password. You go through OAuth and grant it read access to Photos specifically. HOST_B: There are several flows, and the right one depends entirely on your context. Let's start with Authorization Code with PKCE, because this is now the recommended flow for almost everything. HOST_A: Here's the standard Authorization Code flow without PKCE. Your app redirects the user to the authorization server — say, Google — with parameters including client_id, redirect_uri, scope, and a response_type of "code". User logs in and consents. Authorization server redirects back to your app with a one-time code. Your app exchanges that code for tokens by making a back-channel request to the token endpoint, presenting the code plus your client secret. HOST_B: The back-channel exchange is key. The actual tokens never travel through the browser redirect, only through a direct server-to-server call. Much harder to intercept. HOST_A: But here's the problem for public clients — native mobile apps, SPAs. A public client can't securely store a client secret. If it's a mobile app, the secret is in the binary, which can be extracted. So there's no back-channel secret. This opens you up to authorization code interception — an attacker intercepts the code from the redirect and exchanges it themselves. HOST_B: PKCE solves this. Proof Key for Code Exchange. Before starting the flow, your client generates a random high-entropy value called the code verifier — like 64 bytes of random data, base64url encoded. Then it computes the SHA-256 hash of that verifier — that's the code challenge. It sends the code challenge to the authorization server when initiating the flow. HOST_A: When it comes time to exchange the code for tokens, the client includes the original code verifier — not the hash, the original. The authorization server hashes it and checks it matches what was sent earlier. An attacker who intercepted the code doesn't have the original verifier, so they can't complete the exchange. HOST_B: This is essentially a one-time proof of possession. Elegant. And because of this, the current best practice is to use Authorization Code plus PKCE for all clients, including server-side ones, and drop client secrets for anything that can't protect them. HOST_A: Now what about the Implicit flow? This is worth talking about because you still see it in old code and old documentation everywhere. HOST_B: Implicit was designed for browser-based apps before PKCE existed. Instead of getting a code and exchanging it, the access token is returned directly in the URL fragment — the hash part of the URL. The idea was to skip the back-channel exchange. But it's fundamentally less secure. Token in the URL means it can leak in browser history, server logs, referrer headers. And there's no good way to authenticate the client at all. The OAuth 2.0 Security Best Current Practice explicitly says not to use it. It's deprecated. HOST_A: What about Client Credentials flow? HOST_B: Client Credentials is for machine-to-machine communication. No user involved. Your service needs to call another service, it presents its client ID and secret to the token endpoint, gets an access token back. Microservices, backend daemons, CI/CD pipelines calling APIs. Clean, simple, appropriate. HOST_A: The anti-pattern I see is people trying to use Client Credentials for user-facing apps because it's "simpler." It doesn't involve a user! You get a token that represents the application itself, not a specific user. All requests look like they're from the service, not the individual. You lose audit trails, you lose per-user authorization, everything breaks. HOST_B: Right. Now let's layer OpenID Connect on top of OAuth 2.0. Because this is another thing people confuse. OAuth 2 is for authorization — delegated access. OpenID Connect is an identity layer built on top of it. HOST_A: OAuth 2.0 never specifies what "who you are" means. It just gives you an access token to use APIs. OpenID Connect adds a second token — the ID token — which is a JWT that contains identity claims. Sub, name, email, picture, locale. And it defines a standard UserInfo endpoint. Now you have a standard way to authenticate users on top of the OAuth delegation framework. HOST_B: The critical distinction is: access token is for resource access — present it to an API. ID token is for your app to learn about the user — don't present it to APIs. A lot of people get this backwards. They send the ID token to their backend. That's wrong. The backend should validate access tokens. ID tokens are just for the frontend to know who's logged in. HOST_A: OpenID Connect also adds discovery — a well-known configuration endpoint where you can fetch all the server's endpoints, supported scopes, public keys. Makes integration much easier. And it standardizes the authorization code flow with specific scopes: "openid" to get identity, plus "profile", "email", and so on. HOST_B: Let's shift to passwords. Because even with all this OAuth stuff, most apps have local accounts with passwords. And password storage is still done wrong in a shocking number of systems. HOST_A: Let me say this flat out: if you are storing passwords hashed with MD5 or SHA-1 or SHA-256, you have a serious problem. Those are general-purpose hash functions. They're designed to be fast. For passwords, fast is the enemy. HOST_B: The attack model is: your database gets breached, attacker has the hashes. With MD5, a modern GPU can compute billions of hashes per second. An 8-character lowercase password has about 200 billion combinations. You can crack the whole space in a few minutes. SHA-256 is faster still. Speed kills you here. HOST_A: What you need is a deliberately slow hash function. Bcrypt was designed in 1999 specifically for passwords. It has a cost factor — you specify the log2 of the number of iterations. Cost factor 12 means 4096 iterations. You tune it so hashing takes 100-300 milliseconds on your hardware. GPU gains don't help as much because bcrypt uses a fixed amount of memory and has inherent serialization. HOST_B: Argon2 is the modern winner. It won the Password Hashing Competition in 2015. Three variants — Argon2i for password hashing specifically. It's tunable on three dimensions: time cost, memory cost, and parallelism. The memory-hardness is the key innovation. To crack hashes at scale, you need lots of memory bandwidth, which is expensive. GPUs are actually less efficient than CPUs for Argon2 because of the memory layout. HOST_A: Salting. Let's talk about what salting actually does. A salt is a random value, unique per user, generated fresh each time they set a password. It's stored in plaintext alongside the hash — that's fine, it's not a secret. You concatenate the salt and password, then hash that. HOST_B: At the bit level, what this does is make every password hash unique, even for identical passwords. Two users with password "hunter2" get completely different hashes because they have different salts. This defeats precomputed attacks — rainbow tables — because the table would have to be recomputed for every possible salt value. Without salts, you compute a table once and look up millions of hashes instantly. With salts, each hash has to be attacked individually. HOST_A: Modern libraries handle all of this transparently. Argon2 and bcrypt generate and embed the salt in the hash output string. You don't manage it separately. The hash output from bcrypt looks something like dollar-sign 2b dollar-sign 12 dollar-sign, then 22 characters of salt, then 31 characters of hash. It's self-contained. HOST_B: Now let's talk MFA. Multi-factor authentication. And specifically the internals of TOTP, because most people just use Google Authenticator without thinking about how it works. HOST_A: TOTP stands for Time-based One-Time Password. RFC 6238. Here's the mechanism. During setup, the server generates a random secret key — typically 20 bytes of random data, encoded as base32 for human-readable QR codes. That secret is shared once, stored securely on both sides. HOST_B: To generate a code, you take the current Unix timestamp, divide by 30, take the floor — that's your time step, changes every 30 seconds. You compute HMAC-SHA1 of the time step using the shared secret. That gives you a 20-byte HMAC. You then do a dynamic truncation — take specific bytes of the HMAC output — to extract a 31-bit number. Take that modulo 10 to the 6th, pad to 6 digits. That's your code. HOST_A: The server does exactly the same computation at the same time and gets the same code. No network call, no database lookup. Pure cryptography. And because it's time-based, the code changes every 30 seconds and a captured code is useless after that window. HOST_B: Most servers accept codes for the previous and next time window too — that's to handle clock skew and the case where you type it in at second 28 and submit at second 32 when the code has already rolled over. HOST_A: TOTP has weaknesses though. It's still a shared secret. If the server's secret key database is breached, all TOTP codes are compromised. And it's still phishable — an attacker can proxy in real-time, get you to enter your code into their fake site, immediately replay it at the real site before it expires. HOST_B: Which is why WebAuthn and passkeys are the future. WebAuthn is a W3C standard. Here's how it works. During registration, your device generates a new asymmetric key pair — private key stored securely in hardware, in the Secure Enclave on Apple, TPM on Windows. The public key is registered with the server. The private key never leaves your device. Ever. HOST_A: During authentication, the server sends a challenge — a random nonce. Your authenticator signs it with the private key, along with the authenticator data including the relying party ID — which is your domain name. The server verifies the signature with the stored public key. HOST_B: That domain binding is the killer feature. The relying party ID is cryptographically bound. If you're being phished at evil-example.com, the authenticator sees that the domain doesn't match and refuses to sign. Passkeys are technically WebAuthn credentials that are synced across your devices via iCloud Keychain or Google Password Manager. They bring phishing resistance to mainstream users. HOST_A: Alright, common attacks. Let's run through the threats. CSRF — Cross-Site Request Forgery. Classic attack: you're logged in to your bank, you visit evil.com, which has a hidden form that submits to your bank's transfer endpoint. Your browser automatically sends your bank's session cookie with that request. The bank can't distinguish the legitimate request from the malicious one. HOST_B: The traditional defense is CSRF tokens — a hidden value in every form that the server checks. But SameSite cookies have mostly handled this at the browser level. SameSite=Strict means the cookie is never sent on cross-site requests. SameSite=Lax means it's sent on top-level navigation but not on cross-site subresource requests. Most modern browsers default to Lax for cookies that don't specify SameSite. So the attack surface has shrunk dramatically. HOST_A: Session fixation is a sneakier one. Attacker visits your site, gets a session ID from the server before logging in. Somehow gets a victim to use that same session ID — maybe via a link with the session ID embedded. Victim logs in with that session ID. Now the session is elevated to authenticated, and the attacker, who knows the session ID, is also authenticated. HOST_B: The fix is simple but commonly missed: regenerate the session ID at privilege elevation. When a user logs in, issue a brand new session ID. The old one is invalidated. The attacker's pre-planted session ID is now useless. HOST_A: Token leakage. Happens in ways people don't anticipate. Referrer headers — if your app has a token in a URL query parameter and you link to an external resource, that token travels in the Referer header. Server logs — if tokens appear in URLs, they're in your logs. Anywhere you might be sending tokens in URL parameters rather than Authorization headers is a risk. HOST_B: OAuth misconfigurations. Redirect URI validation is the big one. OAuth requires the client to register a redirect URI and the authorization server to validate it on each request. But some servers implement prefix matching or overly permissive validation. Attacker registers evil.com and exploits a server that accepts evil.com.anything.com. They steal the authorization code redirected there. HOST_A: Open redirectors within redirect URIs are another classic. If your registered redirect URI happens to itself redirect somewhere else based on a parameter, an attacker can chain it to exfiltrate the code. Always use exact URI matching. Never accept wildcards in redirect URIs. HOST_B: State parameter. OAuth has a "state" parameter that flows through the redirect. It's supposed to be a random value you generate and verify on return, binding the response to your original request. It prevents CSRF against the OAuth flow itself. Lots of implementations skip it or use predictable values. Don't. HOST_A: Okay. The big practical question. Should you roll your own auth or use an identity provider? HOST_B: My default answer is: use an identity provider. Unless you are specifically in the business of building auth infrastructure, the likelihood that you build something more secure than Auth0, Cognito, or Clerk is very low. Not because you're not smart enough — but because they have dedicated security teams, they've handled every OAuth edge case, they have compliance certifications, they do breach monitoring. HOST_A: The flip side is: you need to understand what you're buying. Auth0 and Cognito are full-featured but complex. Cognito in particular has some very weird quirks — the hosted UI customization is limited, the token issuer URL changes when you use custom domains, there are subtle behavioral differences between user pools and identity pools that bite people constantly. HOST_B: Clerk is the new kid and has a genuinely developer-friendly experience. Very opinionated, fast to integrate, handles passkeys and social login nicely. The tradeoff is vendor lock-in and cost at scale. HOST_A: When would you roll your own? Internal tools where your user base is entirely your employees and you're using SSO via your company's identity provider. Very high-security contexts where you need complete control over the credential lifecycle. Regulatory environments where data residency requirements make SaaS impractical. HOST_B: If you do roll your own, the checklist is substantial. Use bcrypt or argon2 for passwords. Regenerate session IDs on login. HttpOnly, Secure, SameSite cookies. CSRF protection. Rate limiting on login endpoints with exponential backoff. Account lockout policies. MFA support. Secure password reset flows — tokens, not security questions. Audit logging for auth events. Token revocation on logout. Refresh token rotation. HOST_A: The password reset flow deserves special mention because it's commonly done badly. The reset token needs to be high-entropy — 32 bytes of random, not a user ID with a timestamp. It needs to expire — one hour maximum. It needs to be single-use — invalidate it after use. And critically, it's a proof of identity, so using it should count as authentication and could trigger MFA if you have it. HOST_B: One last thing I want to touch on: the log-out problem. It sounds trivial but has real subtleties. In a session-based system, logout is easy — delete the session. In JWT-based systems, you can't invalidate the token. So "logout" often just deletes the token from the client, but if someone copied it, it's still valid until expiry. HOST_A: The mitigation is short-lived access tokens — again, 15 minutes — combined with a revocation list for explicit logout or password change events. It's overhead, but for high-security scenarios it's necessary. You're back to a hybrid: mostly stateless, with a lightweight revocation check. HOST_B: And then there's back-channel logout in OpenID Connect — a mechanism for the identity provider to notify your app that a session ended, via a direct server-to-server call. So if someone logs out of their Google account, Google can tell your app. It requires your app to expose an endpoint, but it solves the logout propagation problem in federated identity scenarios. HOST_A: Alright. Let's land this. Authentication is proving identity. Authorization is enforcing access. Sessions are stateful and revocable. JWTs are stateless with expiry-bounded validity and require careful handling. OAuth 2.0 is delegated authorization — use Authorization Code with PKCE for user flows, Client Credentials for machine-to-machine. OpenID Connect adds identity on top of OAuth. Bcrypt or Argon2 for passwords — never raw SHA or MD5. TOTP is time-based HMAC — phishable. WebAuthn passkeys are the phishing-resistant future. CSRF, session fixation, token leakage, and OAuth redirect validation are your main attack surfaces. HOST_B: And the overarching principle: auth is a system, not a feature. Every component interacts with every other component. A strong password hashing scheme doesn't help if your session tokens are 32-bit sequential integers. A beautiful OAuth implementation doesn't help if your refresh tokens never expire and live in localStorage. HOST_A: Security is defense in depth. You need all the layers. If you want to go deeper on any of this, the resources we'd point to: the OAuth 2.0 Security Best Current Practice RFC, the OWASP Authentication Cheat Sheet, and Troy Hunt's blog, which has excellent post-mortems on real auth breaches. HOST_B: Thank you for listening to Clawd Talks. This has been Emma and Ryan, and we will see you on the next one. HOST_A: Happy hashing, everyone. And please, check your alg fields.