“Login with Google/GitHub” flows break in a handful of predictable ways, and most of them come down to encoding: a redirect_uri that fails an exact-match check, a state value that arrives corrupted, or a value that got encoded twice. Here’s what each parameter needs and how to keep the flow intact.
RFC 6749 specifies that the client builds the authorization request by adding parameters to the endpoint URL using the application/x-www-form-urlencoded format. Every value — client_id, redirect_uri, scope, state, and the PKCE fields — is a percent-encoded query-parameter value:
https://accounts.example.com/authorize
?response_type=code
&client_id=abc123
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=openid%20profile
&state=xyz789
&code_challenge=E9Me...S256hash
&code_challenge_method=S256
Notice redirect_uri is encoded as a whole (%3A%2F%2F for ://) and the space in scope is %20. Get this encoding wrong and the server either can’t parse the request or, worse, silently mismatches a parameter.
OAuth 2.0 Security Best Current Practice and OAuth 2.1 both require the authorization server to compare the redirect_uri against registered values using exact string matching — no substring, no wildcard, no normalization. This is a security rule (it prevents open-redirect and code-interception attacks), and it means encoding differences are fatal:
/callback vs /callback/) fails.%2F where the registered value has a literal /, or an encoded : as %3A where the stored one is literal — fails, because the raw strings differ.Then RFC 6749 §4.1.3 adds a second checkpoint: when you exchange the authorization code for a token, the redirect_uri you send must match the one from the authorization request. So the same URI has to be encoded identically in both steps. The fix is boring but reliable: register the exact URI, store it as a constant, and send that same constant through your HTTP library both times — never rebuild it by hand in one place and not the other.
The state parameter defends against CSRF by round-tripping an unguessable value the client verifies on return. Because it rides in the query string, two rules apply:
state contains anything beyond unreserved characters — and especially if you (unwisely) pack JSON or a return URL into it — its &, =, and # characters will break the query string unless encoded. On return, decode it exactly once.state and store the real context (return URL, form data) server-side or in a signed cookie keyed by that token. An opaque token has no characters that need encoding and never leaks data through the URL.If you must embed a return URL, that URL is a value inside another URL — encode the whole thing with a component encoder so its :// and ? become data, not structure. This is the one legitimate case for encoding a complete URL.
The most common OAuth encoding bug is encoding a value that a library will encode again. If you call encodeURIComponent(redirectUri) and then hand the result to an SDK that also encodes query parameters, %2F becomes %252F. The request still “works” syntactically, but the authorization server’s exact-match check sees https%3A... as literal text and rejects it. The rule: encode once, at one layer. Either you assemble the query string and encode the values yourself, or the library does — never both. When debugging, decode the redirect_uri from a captured request with our decoder (turn on “Decode recursively”); if it takes two passes to get back to a clean URL, you have double-encoding.
PKCE (RFC 7636) protects the code exchange for public clients. The client creates a random code_verifier, and for the S256 method sends code_challenge = the base64url encoding of the SHA-256 hash of the verifier, with padding removed. Base64url (RFC 4648 §5) replaces + and / with - and _ precisely so the value is URL-safe. A frequent mistake is using standard Base64: its +, /, and = then either get percent-encoded (changing the string) or corrupt the parameter, and the server’s verification fails. Generate the challenge with a base64url function that omits padding.
// Correct: base64url, no padding
const challenge = base64url(sha256(verifier)); // uses - and _, no '='
// Wrong: standard base64 in a URL
const challenge = btoa(sha256Bytes); // has + / = -> breaks
The old implicit flow returned the access token in the redirect fragment, exposing it in browser history, referrer headers, and logs. OAuth 2.1 deprecates it in favour of the authorization-code flow with PKCE, where the token is exchanged over the back channel and never appears in a URL. Whatever your flow, treat the URL as public: no tokens, no secrets in query strings or fragments.
application/x-www-form-urlencoded format and the redirect_uri match requirement at the token endpoint (§4.1.3). rfc-editor.org/rfc/rfc6749 §4.1.3code_verifier, code_challenge, and the base64url-encoded S256 method. rfc-editor.org/rfc/rfc7636Honest scope: this guide covers the encoding pitfalls in OAuth 2.0 flows — it is not a full security tutorial. Provider implementations (Google, GitHub, Auth0, etc.) add their own rules on top of the RFCs; always check your provider’s docs, and treat OAuth 2.1 and the Security BCP as the current baseline.
Most often because it doesn’t exactly match a registered URI. OAuth 2.0 security guidance (and OAuth 2.1) require exact string matching — no substring or pattern matching. A trailing slash, a different case, or an encoding difference (%3A vs a literal :, or an extra %2F) makes the strings unequal even when they look the same. Register the precise URI and send it byte-for-byte.
When the redirect_uri is a value in the authorization request (which it is), the whole URI is percent-encoded once as a single query-parameter value — https://app.example.com/cb becomes https%3A%2F%2Fapp.example.com%2Fcb. Let your HTTP or OAuth library encode it; don’t hand-concatenate it into the query string un-encoded, and don’t encode it twice.
The state value travels as a query parameter, so any character in it that isn’t URL-safe must be percent-encoded — and decoded once on return. If you pack JSON or a URL into state without encoding it, its &, =, or # characters break the query string. Encode state as a single value, or use an opaque random token and store the real data server-side keyed by that token.
Encoding a value that a library will encode again. If you call encodeURIComponent on redirect_uri and then pass it to a client that also encodes query parameters, %2F becomes %252F, and the authorization server’s exact-match check fails. Encode once, at one layer — either you build the query string manually and encode, or the library does, never both.
Yes. The PKCE code_verifier is a high-entropy string, and the code_challenge for the S256 method is the base64url (RFC 4648 §5) encoding of its SHA-256 hash — with +, /, and = replaced/removed so it’s URL-safe. Using standard Base64 instead of base64url puts + and / into a URL, which then get percent-encoded or mismatched, breaking verification. Use a base64url encoder with no padding.
No. The implicit flow returned access tokens in the redirect URI fragment, which exposed them in browser history and logs; it’s deprecated in OAuth 2.1 in favour of the authorization-code flow with PKCE, where the token is exchanged over the back channel and never appears in a URL. Keep secrets out of query strings and fragments.
Written and maintained by the urlencodedecode.com team. Every technical claim on this page is verified against primary sources — the RFCs (3986, 3629, 4648, 7578), the WHATWG URL Standard, and official vendor or language documentation — rather than second-hand summaries. When a source contradicts a common assumption, we follow the source and note the discrepancy. Corrections: contactus@urlencodedecode.com.