Writing

SSO, OAuth, and JWT: what each one actually does

These three get used interchangeably and they are not the same thing. What each layer is responsible for, and the mistakes that follow from confusing them.

SSO, OAuth, and JWT get used as if they were three names for the same thing. They are not. One is a user experience, one is an authorization protocol, and one is a file format. Most authentication bugs I have reviewed trace back to that confusion.

The one-line version

  • SSO is an outcome: sign in once, reach many applications.
  • OAuth 2.0 is a protocol for delegated authorization — getting an application permission to call an API on a user's behalf.
  • OIDC is a thin authentication layer on top of OAuth 2.0. This is what actually tells you who the user is.
  • JWT is a token format. It is not a protocol and it grants nothing by itself.

You can build SSO without JWTs. You can use JWTs with no SSO anywhere. They sit at different layers.

OAuth is about access, not identity

This is the distinction that causes real vulnerabilities. OAuth 2.0 was designed so that an application could get a scoped access token to call an API without ever handling the user's password. The access token says this bearer may call these APIs with this scope. It says nothing reliable about who the user is.

The classic mistake is treating a successful OAuth flow as proof of identity: the app receives an access token, calls a userinfo endpoint, gets back an ID, and logs the user in as that ID. The problem is that an access token is a bearer credential. If an attacker obtains a token issued to a different application and presents it, a naive server may accept it and authenticate them as the token's owner. This is the confused-deputy shape behind a whole family of "sign in with X" bugs.

OpenID Connect exists precisely to fix this. It adds an ID token — a JWT with a defined set of claims, issued to a specific client, that you validate rather than merely possess. If you are doing authentication, you want OIDC and an ID token, not raw OAuth and a userinfo call.

What to validate in a JWT

A JWT is three base64url segments: header, payload, signature. Anyone can read the payload. Decoding is not verifying, and the gap between those two verbs is where the bugs live.

At minimum, verify:

  • Signature, against a key you fetched from the issuer's JWKS endpoint.
  • alg — against an allowlist you control. Never take the algorithm from the token header. The alg: none attack and the RS256-to-HS256 confusion attack both work by letting the token choose how it is checked.
  • iss — the issuer is who you expect.
  • aud — the audience is your client. This is the check that stops a token minted for another application being replayed at yours.
  • exp and nbf — it is currently valid, with only small clock skew allowed.
  • nonce, for OIDC ID tokens, matching the value you sent.

Skipping aud is the single most common omission I see, and it is the one that turns a token leak somewhere else into an account takeover on your system.

JWTs are hard to revoke

A signed token is valid until it expires because validation is offline by design — that is the whole appeal. It is also the drawback. If you use long-lived JWTs as your session, then logout, password change, role removal, and account suspension do not take effect until expiry.

The usual resolution is boring and correct: short-lived access tokens, minutes not days, plus a long-lived refresh token held server-side that you can revoke. If you genuinely need instant revocation on the access token itself, you need a check against server state, at which point you have given up the stateless property and should be honest about that trade rather than pretending you still have it.

Where to put the token in a browser

localStorage is readable by any JavaScript running on the page, which means one XSS is one token theft. A cookie with HttpOnly, Secure, and SameSite is not reachable from script and is the better default for browser sessions — with CSRF protection, since cookies are sent automatically.

The often-repeated advice to avoid cookies "because CSRF" trades a well-understood, well-tooled problem for a worse one. XSS-stealable credentials are harder to defend than CSRF.

SAML has not gone away

In enterprise SSO you will still meet SAML 2.0 constantly. Signed XML assertions, an identity provider, a service provider. It predates OAuth and it is not going anywhere.

The security-relevant thing to know is that SAML's use of XML signatures is genuinely difficult to implement safely — XML signature wrapping attacks, canonicalisation subtleties, comment-truncation bugs in assertion parsing. Use a maintained library. Do not hand-roll assertion validation, and be sceptical of any code that parses the assertion separately from verifying it.

A short checklist

  • Doing authentication? Use OIDC and validate the ID token. Not raw OAuth.
  • Validate iss, aud, exp, and the signature, with a server-side alg allowlist.
  • Keep access tokens short-lived; keep revocable state server-side.
  • Prefer HttpOnly cookies over localStorage for browser sessions.
  • Never accept a token without checking it was minted for you.