Basic auth was always the placeholder. It worked, but sending credentials on every single request never sat right — no real session, no expiry, no way to log someone out without changing their password. This week I finally replaced it with JWT, and wired up OAuth2 alongside it.
Why Move Off Basic Auth
The app was already stateless, so on paper basic auth and JWT look similar — no server-side session either way. The difference is what's actually being sent. With basic auth, the raw username and password go out with every request, base64-encoded, which is not encryption, just encoding. With JWT, credentials are exchanged once at login, and after that a signed token carries the identity and role claims — the password never has to leave the client again after that first request.
Implementing JWT
The flow ended up looking like this: a user logs in with username/password, the server verifies it, and issues a signed JWT containing the username and role as claims. Every request after that carries the token in the Authorization: Bearer <token> header instead of credentials.
On the server side, I added a filter that runs before Spring Security's normal authentication step — it pulls the token out of the header, validates the signature, checks it hasn't expired, and if everything checks out, builds an Authentication object from the claims and sets it into the security context. From that point on, the rest of the role-based access control I'd already built (USER vs ADMIN) just works unchanged, because it was always checking the authority on the authenticated principal, not caring how that principal got authenticated in the first place.
That was actually the most satisfying part — the RBAC layer didn't need to be touched at all. Swapping the authentication mechanism underneath it and having the authorization layer keep working exactly as before is a good sign the earlier design was reasonably sound.
Adding OAuth2
Alongside JWT, I set up OAuth2 login so users can authenticate through an external provider instead of only the app's own username/password flow. Spring Security's OAuth2 client support handles most of the heavy lifting here — the redirect to the provider, the callback, and pulling back user info — but I still had to decide how an OAuth2-authenticated user maps onto the same role system as everyone else, since a first-time OAuth2 login doesn't come with a role attached the way a normal registration does.
The approach I landed on: on first OAuth2 login, create a local user record with a default role, so from that point forward they're indistinguishable from a normal registered user as far as the rest of the app is concerned. Two different front doors, same house once you're inside.
Where I Got Stuck
Token expiry handling was the annoying part. It's easy to issue a token that works — it's less obvious, at first, how to handle a client whose token has expired mid-session without just throwing a generic 401 and leaving them confused. I ended up making sure expired-token responses are distinguishable from invalid-token responses, so the client side can actually tell "log in again" apart from "something is wrong," instead of treating every failure the same way.
What's Different Now
Basic auth is gone. Every request now carries a token instead of raw credentials, that token has a real expiry, and there are two legitimate ways to authenticate — direct login or OAuth2 — both of which land in the same role-based system underneath. It's a lot more moving parts than the app had a week ago, but each part is doing something basic auth genuinely couldn't.
Next up: refresh tokens, since right now an expired token just means logging in again from scratch, which isn't going to hold up once this is actually deployed somewhere.
Top comments (0)