Authorization
Published
Most of the guards in this series are library defaults. OAuth is different, and worse: the permissive fallbacks are in the specifications themselves. The state parameter is recommended rather than required, PKCE is an extension you opt into per request, and an omitted scope is something the authorization server is explicitly allowed to fill in for you. So an OAuth client that quietly stops sending half its options is not making malformed requests. It is making legal ones.
What the sample actually does
The file builds the option object an authorization-code client sends - response type, client id, redirect URI, scope, state, nonce, a PKCE challenge and the method used to derive it - and a second object for the token exchange carrying the verifier. It then runs four scenarios against a model authorization server: an honest sign-in, a login-CSRF delivery in which an attacker's authorization code arrives at the victim's callback, a stolen authorization code redeemed by somebody who read the URL, and a captured id_token replayed later.
The server and the client's own callback validator are copied unprotected. Every default they apply is one a specification states, not one the model invented: an omitted challenge method defaults to plain, a request carrying no challenge at all is simply not bound to one, an omitted scope may be filled in by the server from a pre-defined default, and both state and nonce are optional parameters whose absence means there is no check to perform.
Protection alone, on all five presets, produced behaviour identical to the unprotected file. Everything below required member renaming.
The callback that stops being bound to anything
Renaming state took the login-CSRF scenario from refused to accepted. The validator reported no-state-binding-to-check - not a mismatch it tolerated, but a comparison it never made, because the client had not sent a state and therefore had nothing to compare against.
This is the failure mode that makes the state parameter unusual. A client that sends state and gets it wrong fails loudly. A client that stops sending state does not fail at all, and neither does an attacker who simply stops sending it too. The honest sign-in in our sample kept working throughout; the only observable change was in the arm where an authorization code that belonged to somebody else was accepted into the victim's session.
Renaming nonce produced the same shape one layer up: a captured id_token from an earlier sign-in, replayed later, went from refused to accepted with the reason no-nonce-binding-to-check. Fresh sign-ins carried on succeeding.
PKCE, and the specification's own permissive default
This is the result we would show first. The PKCE extension says that when a request carries a challenge but no challenge method, the method defaults to plain - the challenge and the verifier are the same string. So renaming codeChallengeMethod alone changed what an observer of the authorization URL sees from a SHA-256 digest to a value the server will accept back verbatim.
What it did to the two parties is the interesting part, because it did opposite things. The honest client's own token exchange failed, with code_verifier_mismatch, because it kept sending the real verifier while the server was now expecting the challenge. And in the very same run, the stolen authorization code was redeemed successfully by an attacker who had only ever seen the URL. The party doing the work breaks; the party attacking gets in.
Renaming codeChallenge instead is the silent version. With no challenge in the authorization request, the server binds the code to nothing, the verifier in the token exchange is ignored rather than refused, and the honest flow keeps working end to end - while the stolen code is redeemable by anyone. Enforcement went from S256 to none and there is no error anywhere in the transcript to notice.
Renaming codeVerifier on its own is the loud arm: the server holds a challenge, the client sends no verifier, and the exchange is refused outright. And renaming all three together - which is exactly what a pattern scoped to a PKCE options block does - removes the extension entirely, silently, with the honest flow intact and the stolen code redeemable. Half the group fails safe; the whole group fails open. We have now measured that same shape in four unrelated areas.
A one-scope request that comes back with everything
Renaming scope produced the result with the widest consequences and the least noise. The authorization request stopped naming a scope, so the server applied the pre-defined default it holds for that client, which is every scope the client is registered for. The granted set went from profile:read to profile:read profile:write billing:manage admin:all.
Nothing failed. The sign-in worked, the token worked, and every API call the application makes carried on working - with an access token four times more powerful than the one the application intended to ask for. If that token is ever logged, cached, exfiltrated or handed to a subsystem you did not write, the blast radius is the registered scope list rather than the requested one.
This is the only result in the series so far where a rename adds a capability rather than removing a restriction, and it is a good argument for keeping registered scope lists narrow at the provider. A client registered for exactly the scopes it uses cannot be over-granted by omission.
The arms that fail loudly, and the one that does nothing
Renaming redirectUri is refused at the authorization endpoint, because the client has more than one registered URI and the server cannot choose. That is a genuine loud failure and it is the arm most likely to be caught in staging. It is also the arm whose behaviour varies most between real providers - a client with exactly one registered URI would have the request succeed against a server that fills the value in, so do not treat this as a general safety property.
Renaming responseType changed nothing, because the value pinned in the sample is identical to the default the server assumes. That is the expected result, and its usefulness is entirely in the caveat: the arm is inert because of the value, not the name. A client doing anything other than plain authorization code would find the same rename turning its request into an unsupported one.
Renaming the fields on what the token endpoint hands back was mild here rather than fatal, because the calling code reads them with equality checks rather than method calls. It reported no PKCE enforcement and skipped the id_token check - the sort of degradation that reads as a configuration change rather than as a fault.
What to do about it
Put the option names your OAuth client sends on the exclusion list, along with the fields it reads back off the token response. Member renaming is off unless you enable it, and it only touches names your expression matches, so this is a pattern-scoping decision rather than a decision about whether to protect the file.
Then test the security properties directly, because none of them are visible on the happy path. Assert that a callback carrying a state your client did not issue is rejected. Assert that the authorization URL your client builds contains a challenge method of S256 rather than nothing. Assert that the granted scope on the returned token equals the scope you asked for, not merely that a token came back.
That last assertion is the cheapest and it catches the arm with the widest consequences. Comparing requested scope to granted scope is one line, it is meaningful in production as well as in tests, and no other check in this article would have found an over-granted token.
Frequently asked questions
Does obfuscation break an OAuth sign-in?
Not by itself. Protection alone, on all five presets we tested including both output targets and the compressed profile, produced behaviour identical to the unprotected file. Every result in this article required member renaming pointed at the option names the client sends.
Why do the OAuth failures not produce errors?
Because the permissive fallbacks are in the specifications rather than in one library. State is recommended rather than required, PKCE is opt-in per request, and an omitted scope is something the authorization server is explicitly permitted to fill in. A client that stops sending those parameters is making legal requests.
What happens if the PKCE challenge method is renamed?
The extension's own default applies, which is plain - the challenge and the verifier become the same string. In our measurement the honest client's token exchange then failed while a stolen authorization code was successfully redeemed by an observer of the URL, so the effect is opposite for the two parties.
Which PKCE arm was the quiet one?
Renaming the challenge itself. With no challenge in the authorization request the server binds the code to nothing and ignores the verifier, so the honest flow keeps working end to end while the code becomes redeemable by whoever sees it. Renaming all three PKCE keys together behaved the same way.
How can a rename make a token more powerful?
By removing the scope parameter. The authorization server then applies its pre-defined default for that client, which in our model is every registered scope. The request went from one scope to four, including an administrative one, and nothing failed.
What should I exclude from member renaming?
The option keys your client sends - state, nonce, scope, redirect URI, response type and the PKCE trio - and the fields you read off the token response. Keeping registered scope lists narrow at the provider limits the damage independently.
What is the single most useful test to add?
Compare the scope granted on the returned token against the scope you asked for. It is one line, it is meaningful in production as well as in tests, and none of the other checks in this article would have caught an over-granted token.
Related reading