Authorization

OAuth 2.0 redirect_uri & state encoding.

“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.

The authorization request is form-encoded

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.

redirect_uri: exact-match is unforgiving

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:

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.

state: encode it, or keep it opaque

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:

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 double-encoding trap

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: use base64url, not Base64

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

Keep tokens out of URLs

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.

Sources & standards

Honest 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.

Common questions

About OAuth encoding.

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.

Related

Keep reading.

About this page

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.