Decode any JWT header and you'll spot a field like "alg": "RS256". Most devs glance at it and move on , but that one field decides how your tokens get signed, verified, and whether your verification code will actually work in production. Here's the short version.
Why the signing algorithm matters
A JWT's header and payload are just Base64URL anyone can read them. The signature is the only part that proves a token is genuine and untampered. The algorithm determines how that signature is created and checked, and getting it wrong in your verification logic is a real vulnerability, not a theory problem.
HS256: one shared secret
HS256 (HMAC + SHA-256) is symmetric the same secret key signs and verifies the token. Fast, simple, and fine when one backend both issues and checks its own tokens.
The catch: every service that verifies tokens needs that same secret. Share it with more services and you widen the attack surface and rotating it isn't painless either.
RS256: public/private key pair
RS256 (RSA + SHA-256) is asymmetric. A private key signs the token; a public key verifies it. Only the auth server ever holds the private key — everyone else just gets the public one, which is safe to hand out freely.
That makes RS256 the better fit for microservices and public APIs: each service verifies independently, and providers typically expose their public keys via a JWKS endpoint a standard URL of current keys, matched by kid in the token header. The trade-off is a bit more overhead: RSA math is slower, and there's a network dependency for key fetching.
What your auth provider is actually doing
| Provider | Algorithm | Notes |
|---|---|---|
| Supabase | HS256 | Shared secret in your dashboard; can even verify client-side |
| Clerk | RS256 | Rotating key pair, JWKS handled by their SDK |
| Firebase | RS256 | Keys rotate every few hours via Google's JWKS endpoint |
| NextAuth | HS256 (default) | Uses NEXTAUTH_SECRET; also wraps tokens in JWE, which is why you'll see 5 parts instead of 3 |
The one security trap to know
Some older libraries will accept alg: none meaning no signature check at all. Always explicitly whitelist the algorithm you expect server-side. Never trust whatever the token header claims.
It's not really your choice
The algorithm isn't something you pick at sign-in , it's whatever your auth provider decided. Your job is just to know which one you're dealing with so you debug the right problem when verification fails.
That's the concept. What I didn't get into here: how to actually migrate a live app from HS256 to RS256 without breaking active sessions, a full decision framework for picking one when you're rolling your own auth, the exact JWKS URL format for each provider, and answers to the "can I," "is it always," and "how do I verify without a library" questions that come up constantly. I wrote all of that up on the full post.
If you just want a quick answer right now: paste your own token into the AuthParse decoder , it reads the alg field for you, flags it if it's none, and tells you whether you're looking at HS256 or RS256 without you having to think about it.
Top comments (1)
I found the explanation of the security trade-offs between HS256 and RS256 to be particularly insightful, especially the point about HS256 requiring a shared secret key, which can widen the attack surface when used across multiple services. The use of a JWKS endpoint to manage public keys in RS256 is a great solution for microservices and public APIs, and it's interesting to see how different auth providers like Firebase and Clerk handle key rotation. When implementing token verification, do you think it's more important to prioritize performance or security, and are there any specific scenarios where one might take precedence over the other?