Article · Mar 19, 2026 · 8 min read

URL encoding for OAuth signing

OAuth and AWS Signature v4 both use URL encoding as part of their cryptographic signing process. Both require strict RFC 3986 encoding — the kind that encodes more characters than the typical browser/framework default. Get the encoding wrong and your signature fails.

This article covers OAuth 1.0a signing, AWS SigV4 encoding, and the common pitfalls.

Why signing requires strict encoding

Signature algorithms work by:

  1. Building a canonical string from request data (the “signature base string”)
  2. Hashing or HMAC-signing that string with a secret
  3. Sending the resulting signature in the request

The server then rebuilds the same canonical string from the request it received, signs it the same way, and compares. If the two strings differ by even one byte, the signatures don’t match and the request is rejected.

URL encoding is part of building that canonical string. The client and server must produce byte-identical encoded forms — which means they must follow the exact same encoding rules.

OAuth 1.0a percent-encoding rules

From RFC 5849 Section 3.6:

Encode these as unreserved (no encoding):

A-Z   a-z   0-9   -   .   _   ~

Encode everything else as %XX with uppercase hex digits.

Specifically, OAuth 1.0a requires encoding these characters (which encodeURIComponent normally leaves alone):

!   *   '   (   )

These five characters are NOT in OAuth’s unreserved set, but most languages’ default URL encoding leaves them as-is. You have to manually encode them.

OAuth 1.0a percent-encode in code

JavaScript

function oauthPercentEncode(value) {
  return encodeURIComponent(value)
    .replace(/!/g, '%21')
    .replace(/'/g, '%27')
    .replace(/\(/g, '%28')
    .replace(/\)/g, '%29')
    .replace(/\*/g, '%2A');
}

Python

from urllib.parse import quote

def oauth_percent_encode(value):
    return quote(str(value), safe='-._~')

Python’s quote with safe='-._~' hits the OAuth spec exactly — it encodes everything except the four genuinely-unreserved characters.

PHP

function oauth_percent_encode($value) {
    return rawurlencode($value);
}

PHP’s rawurlencode is actually OAuth-compliant by default — it encodes !*'() properly.

The OAuth 1.0a signature base string

The signature base string is:

HTTP-METHOD & encoded-URL & encoded-sorted-parameters

Where:

Example:

GET&https%3A%2F%2Fapi.example.com%2Fresource&
oauth_consumer_key%3Dabc%26oauth_nonce%3Dxyz%26oauth_signature_method%3DHMAC-SHA1%26
oauth_timestamp%3D1234567890%26oauth_version%3D1.0%26q%3Dhello%2520world

The q=hello world parameter becomes q=hello%20world in the first encoding pass, then q%3Dhello%2520world in the second pass. Double-encoding is the spec for OAuth 1.0a.

AWS Signature Version 4

AWS SigV4 is similar but with subtly different rules. From the AWS docs:

For path segments: Encode everything except A-Z a-z 0-9 - . _ ~. Encode / UNLESS this is for S3 (S3 has different rules).

For query parameters: Encode everything except A-Z a-z 0-9 - . _ ~. Yes, encode = in values (NOT in the structural = between key and value).

Hex digits: Uppercase.

Spaces: Always %20, never +.

AWS SigV4 percent-encode in code

// JavaScript
function awsPercentEncode(value, encodeSlash = true) {
  let encoded = encodeURIComponent(value)
    .replace(/[!*'()]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase());
  if (!encodeSlash) {
    encoded = encoded.replace(/%2F/g, '/');
  }
  return encoded;
}

The function is similar to OAuth’s, with an option to preserve forward slashes for path encoding.

Canonical request for AWS SigV4

The canonical request includes:

HTTPMethod\n
CanonicalURI\n
CanonicalQueryString\n
CanonicalHeaders\n
SignedHeaders\n
HashedPayload

The CanonicalQueryString must have parameters sorted by key, each encoded with the strict rules above. If you encode using encodeURIComponent directly, the !, *, ', (, ) won’t be encoded — and your signature will fail.

OAuth 2.0: less strict, more variation

OAuth 2.0 uses bearer tokens and TLS, eliminating the signature complexity. But there are still URL encoding requirements:

The authorization request URL contains the redirect_uri parameter, which is a URL inside a URL. The inner URL must be properly encoded:

https://auth.example.com/authorize
  ?client_id=abc123
  &response_type=code
  &redirect_uri=https%3A%2F%2Fmyapp.com%2Fcallback
  &scope=read%20write
  &state=xyz

The %20 for space in scope=read write matters — some authorization servers accept +, others require %20.

Common signing mistakes

1. Using language defaults instead of strict encoding

Most languages’ URL encode functions leave !*'() alone. OAuth requires them encoded. The result: signature mismatch on requests that happen to contain those characters.

2. Not encoding twice for OAuth 1.0a

The OAuth 1.0a spec requires URL-encoding the joined parameter string a second time before signing. Skip this and your signature is wrong for any parameter containing a space.

3. Wrong sort order

OAuth and SigV4 both require alphabetically sorted parameters. If a request has both q and Q parameters, sort byte-wise (uppercase before lowercase in ASCII).

4. Including/excluding the signature parameter itself

The oauth_signature parameter goes in the final request but NOT in the signature base string (you can’t sign the signature). Watch for accidentally including it.

Tools that help

Postman has OAuth 1.0a and AWS SigV4 helpers built in. Useful for testing what the correct signature should be.

cURL with AWS CLI: aws s3 presign s3://bucket/key generates a presigned URL with proper encoding.

Our URL decoder: paste a failing signature’s base string, decode it, inspect what was actually canonicalized. Mismatches often jump out visually.

Bottom line

OAuth and AWS signing demand stricter URL encoding than your language’s default. Specifically: encode !*'() and use uppercase hex. For OAuth 1.0a, also double-encode the joined parameter list. For SigV4, sort parameters and use %20 for spaces.

Whenever possible, use a vendor SDK or signature library — they handle these details correctly. Hand-rolled signing is a debugging time-sink.


Found this useful? Try the URL decoder, the URL encoder, or browse all tools.

More reading

From the blog.