I once spent three hours debugging an authentication flow where a client was sending the client_secret in the URL query parameters. It worked—until it didn't, because the secret got logged by a proxy, cached by a browser, and eventually showed up in a support ticket. The developer had copy-pasted a Stack Overflow answer from 2014 and called it a day. OAuth 2.0 and OpenID Connect power basically every modern login flow, yet most engineers treat them like magic incantations they repeat without understanding. That's how you end up with secrets in URLs, tokens in localStorage, and refresh tokens that never expire.

I've reviewed dozens of OAuth implementations, broken a few intentionally, and fixed more than I can count. Here's what actually matters when you're wiring up "Sign in with Google" or building your own identity layer.

1. Know the Flows, or You'll Pick the Wrong One

OAuth 2.0 isn't one protocol — it's a framework with multiple flows, and choosing wrong is a category of vulnerability. Authorization Code Flow with PKCE is the default for almost everything now. It's what you use for SPAs, mobile apps, and server-side web apps. The client gets a code, exchanges it for tokens server-side, and PKCE prevents authorization code interception attacks. Implicit Flow — where tokens get returned directly to the browser — was deprecated for a reason. It exposes access tokens to the browser history, referrer headers, and malicious JavaScript. Client Credentials Flow is for service-to-service auth, not users. Password Grant exists but shouldn't — if you're collecting user passwords in your app, you've missed the point of OAuth entirely. I still see teams use Implicit Flow because some legacy tutorial told them to. Stop. Migrate to Authorization Code + PKCE. The spec recommends it. Your security team recommends it. Future you will thank you.

2. Tokens Are Credentials — Treat Them Like It

An access token is a password with a timer. A refresh token is a master key. If I steal your refresh token, I am you until it expires or gets revoked. I see teams store access tokens in localStorage because it's convenient. It's also accessible to any XSS payload that runs on your domain. Session cookies with HttpOnly, Secure, and SameSite attributes are harder to steal from JavaScript. For SPAs that need tokens in the browser, I prefer Backend-for-Frontend patterns where the server holds tokens and the browser holds only a session identifier. For refresh tokens, bind them to the client via rotation: every time you use a refresh token, you get a new one and the old one is invalidated. One stolen token gets you one session, not indefinite access.

3. ID Tokens and Access Tokens Are Different Animals

OpenID Connect adds an identity layer on top of OAuth 2.0, and the ID token is its signature deliverable. It's a JWT that contains claims about the user — sub, email, name, issued by the identity provider, signed with their key. Here's what trips people up: the ID token proves who the user is. The access token proves what they're allowed to do. You don't send an ID token to an API to authorize a request. You send an access token. I've seen APIs accept ID tokens because "it's also a JWT" and the developer didn't understand the distinction. That's a bug. ID tokens are for the client to learn about the user. Access tokens are for resource servers to enforce authorization. Mixing them up means your API can't tell the difference between "this user exists" and "this user is allowed to delete this record."

4. Validate Everything, Trust Nothing

JWTs are self-contained, which means clients can validate them locally without calling the identity provider every request. But that only works if you actually validate them. Signature verification with the right public key. Issuer claim matching your expected IDP. Audience claim matching your application. Expiration and not-before timestamps. I've found vulnerabilities where apps accepted any well-formed JWT without checking the signature, or where they accepted tokens intended for a different application because the audience wasn't validated. Use a established library — python-jose, jsonwebtoken, Microsoft.IdentityModel.Tokens—and configure it strictly. Don't roll your own JWT parsing. The spec has edge cases around algorithm confusion attacks that will bite you if you get clever.

5. Consent and Scope Are Security Controls, Not UX Annoyances

OAuth scopes define what a client is allowed to do on behalf of a user. "Read your email." "Access your calendar." "Manage your files." Users click through consent screens without reading them, but as a security engineer, you should care deeply. I review third-party OAuth integrations for scope creep: a calendar app that requests https://mail.google.com/ access is a red flag. I also implement incremental authorization—request minimal scopes at login, escalate only when needed. From the provider side, validate that the client isn't asking for scopes it wasn't registered for. One bug I found: a client could request admin scopes during the authorization request even though it was registered as a standard user app, and the IDP didn't enforce the restriction. Scope validation on both sides matters.

The Takeaway

OAuth 2.0 and OIDC aren't magic, and they aren't secure by default. They're a framework that gives you building blocks, and it's on you to assemble them correctly. Use Authorization Code + PKCE. Protect tokens like credentials. Separate identity from authorization. Validate every claim. Scope permissions minimally. The teams that get breached via OAuth don't get breached because the protocol failed. They get breached because they treated it like a black box, copied a tutorial, and moved on. Understand the flow. Understand the tokens. Build like someone is actively trying to steal them — because they are.