OAuth2: where login breaks and why it costs more than it seems
OAuth2 almost never breaks "at login" — it breaks on the details that aren't visible in the happy path.
The classic: a public client without PKCE, and the authorization code travels in the clear via redirect interception — on a mobile device or in a browser, it's captured by any app that has registered the same URI scheme.
Then the attacker exchanges the code for a token before the legitimate client does, and in the logs it looks like an ordinary successful login.
The second landmine is eternal tokens.
An access token without a short lifetime and a refresh without rotation lives until manual revocation: a leak from logs, from device memory, or via a third-party script grants permanent access, and there's no way to invalidate it — no revocation list, no device binding. Revocation has to be done by rotating secrets, which takes down all users at once.
The third scenario is session desynchronization between services. The user has logged in to one application, but a neighboring service doesn't see them: different token issuers, mismatched audiences, diverged signing keys. Externally, this is a "login denied" with no clear error, and support troubleshoots it blind.
Separately — spikes of failures during token refresh. When refresh requests come in parallel from several tabs or replicas, rotation triggers on the first request, and the rest get denied and log the user out.
The peak of such failures falls in the morning and during deployments, when sessions massively re-request access.
The cost of all four scenarios is the same: lost orders, support load, and reputational damage that isn't visible in availability metrics. Let's break down how this gets closed.
Login flow: PKCE, token rotation, social login
PKCE instead of implicit flow
The classic implicit flow hands the token straight into the address bar — it settles in browser history, proxy logs, and the Referer header.
PKCE closes off this class: the client generates a random code_verifier, sends its hash (code_challenge) to the authorization endpoint, and the provider verifies the pair when exchanging the code for a token. An intercepted code without the verifier is useless — a redirect attack stops working in principle.
Token rotation and session lifetime
The refresh token is single-use: when exchanged, a new pair is issued and the old one is invalidated. If the same refresh arrives again, it's a sign of theft, and the user's entire session chain is revoked automatically.
The access token lives for minutes, so a compromise gives an attack window of a few minutes rather than days. Session lifetime is set by policy: for administrative sections — hours, for regular ones — days with extension based on activity.
Social login and access revocation
An external provider returns a signed identifier, by which we find or create a local account — the link is made by a stable identifier, not by email, so a change of address doesn't create a duplicate.
Access revocation is instant: an entry in the revocation list is checked on every request, and the session dies within the verification cache.
| Option | When it fits | Limitations |
|---|---|---|
| PKCE + rotation | Public clients, mobile and SPA | Requires server-side code exchange |
| Classic server-side flow | Confidential clients with a secret | The secret leaks along with the backend |
| Long-lived token | Internal services without a user | Revocation lags, the attack window grows |
We assemble the flow on Keycloak or on a built-in provider — the choice depends on whether single sign-on to third-party systems is needed.
Workflow: from current sign-in audit to settings handover
The work proceeds sequentially: each subsequent stage relies on the artifacts of the previous one, so intermediate decisions don't have to be revisited midway. Below is how the process is structured from sign-in audit to settings handover.
-
Audit of the current sign-in. We analyze how the application issues and stores tokens, where sessions live, which providers are already connected, and which OAuth2 flows are in use.
We identify weak points: long-lived refresh tokens without rotation, missing PKCE for public clients, tokens in localStorage, no revocation on sign-out.
-
Flow design. We choose Authorization Code + PKCE for clients that cannot store a secret, and define the lifetimes of access and refresh tokens. We design refresh token rotation with reuse detection: when an already revoked token is used, the entire chain is invalidated.
-
Provider and policy configuration. We register the application with providers, configure redirect URIs, scopes, and account linking rules for social login. We define policies for revocation, storage, and linking multiple sign-in methods to a single user.
-
Failure scenario testing. We run through what typically breaks in production: an expired refresh token, access revocation on the provider side, clock skew, a repeated callback, account conflicts when signing in through different services. Each scenario must end in a predictable state, not a stuck session.
-
Settings handover. We deliver provider configurations, flow schemas, token policies, and operational documentation — so the client's team can change parameters without us.
Describe your current sign-in scheme — we'll get back to you with an assessment and a work plan.
How does authorization code with PKCE differ from implicit flow?
Implicit flow was a compromise of an era when browsers could not reliably store secrets, and authorization servers did not support exchanging a code for a token without a backend.
In it, the client application receives an access token directly in the address bar fragment — bypassing the intermediate step with a code.
This reduced the number of requests, but the price turned out to be high: the token settled in browser history, proxy logs, and referer headers, and intercepting it via an injected script or a compromised extension was no trouble at all.
That is precisely why OAuth2 in its current revision explicitly does not recommend implicit flow for client applications.
Authorization code with PKCE solves the same task differently. The application generates a random value (code_verifier), computes a derivative from it (code_challenge), and sends it along with the login request.
The server returns a one-time code, and exchanging the code for a token is possible only upon presenting the original verifier. Even if an attacker intercepts the code — via a redirect, logs, or a malicious application on the device — without the verifier it is useless.
This closes the class of attacks involving interception of the authorization code, which in implicit flow simply did not exist as a stage.
The key difference is where the token ends up. In implicit flow, it immediately lands in the client environment and lives there until expiration.
In the PKCE flow, the token is issued only after the verifier is validated, and the code itself is one-time and tied to a specific request.
For mobile and single-page applications, where there is no trusted backend, this is the only correct option: there is nowhere to store the client secret there, and PKCE replaces it with cryptographic binding.
There is also a second line of defense — refresh rotation. A long-lived refresh token is replaced with a new one on each refresh, and the old one is invalidated.
If the token was stolen and used, the legitimate client will receive a rejection on the next refresh, and the entire session chain is revoked.
In implicit flow, a refresh token was not issued at all — the session could only be extended by logging in again, which pushed developers toward lengthening the access token lifetime and growing the vulnerability window.
In practice, the choice looks like this: for server applications with a trusted backend — authorization code with a client secret; for single-page and mobile — authorization code with PKCE; implicit flow remains only in legacy systems where replacing it requires rebuilding the client.
Login security here is measured not by ease of integration, but by how many classes of attacks are closed at the protocol level rather than at the level of developer discipline.
Case: login stopped failing after token rotation
The client — a service with a personal account and a mobile app — came with a complaint that support couldn't close: users regularly complained that "login keeps dropping." The share of failed logins hovered around 8%: some token refresh requests arrived with an already-used value, the server responded with a rejection, and the person saw the login form instead of the account.
A separate pain point was access revocation: after an employee was blocked, their session lived until the token expired, meaning access persisted for hours.
What was changed
We broke down the authentication flow step by step and found the root cause: the app refreshed the token in parallel from multiple tabs and background tasks, while the old token was not invalidated atomically.
We introduced refresh token rotation: each exchange issues a new pair, the previous value is marked as used, and a repeated presentation is treated as a sign of leakage — the user's entire session chain is revoked.
The race between tabs was closed on the client side: the exchange goes through a single point with locking, while other requests wait for the result instead of initiating their own.
For login via external providers, we added PKCE — the authorization code no longer passes in the open, and an intercepted redirect without the verifier is useless.
Access revocation was moved to a server-side registry of active sessions: an administrator closes access, and all related tokens become invalid at the moment of revocation, not upon expiration.
What became
| Metric | Before | After |
|---|---|---|
| Share of failed logins | ~8% | less than 0.5% |
| Access revocation time | up to token lifetime | seconds after revocation |
| Support requests for session resets | regular stream | isolated cases |
We separately closed the password change scenario: it now terminates all other sessions except the current one — previously, a user changed their password and didn't understand why the old session on another device kept working.
The logic was built on the standard authorization protocol OAuth 2.0 with PKCE extensions and refresh token rotation — without homemade schemes that are later hard to maintain and verify for security.
Doubts before implementation: compatibility, migration, rollback
OAuth2 implementation almost never starts from scratch: the client already has a user base, their own sessions, roles, and external services that access the application according to the old rules.
Therefore, the first question is not "how to configure the provider," but "what will happen to current logins while we migrate the system to the new setup." We design login migration so that the old and new mechanisms work in parallel: the user gets into the system in any familiar way, and the account is gradually linked to the new identifier.
We separately define a rollback plan — if an incompatibility surfaces in production, reverting to the previous scheme takes minutes, not hours.
What if we have a non-standard provider or our own user base? We extend OAuth2 through a custom identity provider: it returns the same data as external ones and connects to the existing database without rebuilding it. What if some services can't work with tokens?
For them, we leave a compatible access layer that accepts the previous format and validates the new one internally — the service isn't rewritten entirely.
Can we roll back if the new login breaks some scenarios?
Yes. We keep both setups active and switch traffic by user percentage: if there's a problem, we revert to the previous login with a single setting, without rolling back the entire release. All issued tokens remain valid, and sessions aren't broken.
The client receives not just "it works, that's fine," but a complete package that allows the system to be maintained and handed over to another team:
- provider configurations — which parameters are set and why;
- login flow diagrams: where the token is issued, where it's validated, where it's refreshed;
- token lifetime policies and revocation rules;
- access matrix: who gets access to which data after login;
- documentation for connecting new services and external providers;
- rollback plan with switch points and trigger criteria;
- support instructions: how to read login logs and find the cause of a failure.
Describe your current login scheme and the list of services connected to it — we'll come back with an estimate and a migration plan without stopping operations.
Timelines for OAuth2 implementation stages without tying to dates
OAuth2 implementation is broken down into stages of varying duration — from quick checks to work where most of the time goes into agreeing on scenarios and testing.
Below are approximate timelines for a typical project: they depend on the number of external providers, the number of roles, and whether there is already a ready-made authentication infrastructure.
Integration takes the longest — that is exactly where mismatches surface between the provider's user model and your internal role structure.
Verification is a separate block: scenarios with token expiration and simultaneous refresh from multiple tabs break most often, and they are best closed out before handover.
Can it be sped up? Yes, if there is a single external provider and the roles are already described — the design stage is cut in half. If there are multiple providers and a single account is needed, the timelines grow proportionally to the number of integrations.
What if part of the work is already done? The audit will show what we can reuse and what will have to be rewritten — this determines whether we start from design or from the middle of setup.
Describe your task: how many entry points, which providers, whether there is a current session scheme — we will come back with an estimate and a stage-by-stage plan.







