OAuth 1.0 Request Signing — Signatures, Nonces & Replay Protection
A deep dive into OAuth 1.0 request signing — why bearer tokens aren't enough, how HMAC-SHA1 signatures work, the complete signing process, provider-side verification, nonces, timestamps, replay attacks, and why signing doesn't replace HTTPS.
Before We Begin
In the previous parts, you learned the full OAuth 1.0 authorization flow. At every step, we said “the request is signed” without explaining how.
Now we explain signing — arguably the most important and complex aspect of OAuth 1.0.
Our Running Example
| Entity | Property | Value |
|---|---|---|
| PhotoApp (Consumer) | consumer_key | photo-app-123 |
consumer_secret | SECRET_APP | |
| Alice’s Access Token | access_token | ACCESS_123 |
access_token_secret | ACCESS_SECRET |
The OAuth 1.0a simulation uses these same values. Steps in that player link to the signing, replay, and security scenes.
Why Sign Requests?
The Problem — Bearer-Style Credentials
Suppose OAuth 1.0 worked like this:
GET /photos HTTP/1.1
Host: api.photoprovider.example.com
Authorization: OAuth oauth_token=ACCESS_123
This is a bearer token approach. Whoever possesses ACCESS_123 can use it.
Why isn’t this enough?
- Token theft: If an attacker captures a classic bearer token like
ACCESS_123, they can immediately use it to make requests on Alice’s behalf. (In OAuth 1.0, stealing the Access Token alone is not enough; the attacker also needs the signing secrets. But stealing all three is a much more serious credential compromise). - Request tampering: An attacker who intercepts the request could modify it (e.g.,
GET /photos?page=1→GET /photos?delete=all). The server has no way to detect the modification. - No proof of sender: The server knows the token is valid, but it cannot verify that the request was actually constructed by PhotoApp.
OAuth 1.0’s answer to all three problems is request signing.
Every protected request is cryptographically signed using secrets that only PhotoApp and PhotoProvider possess. The signature proves the request’s authenticity and integrity.
Signing ≠ Encryption
This distinction is critical and must be completely clear.
| Encryption | Signature |
|---|---|
| Provides: Confidentiality (“Nobody can read the message”) | Provides: Integrity (“Nobody can modify the request without detection”) and Authentication (“The request was constructed by someone with the secrets”) |
| Hides the content of the message | Does NOT hide the content. Anyone can read the request |
| Example: TLS/HTTPS | Example: OAuth 1.0 signature |
OAuth 1.0 signatures do not encrypt the request. The request parameters, the URL, and the method are all visible. What the signature does is prove:
- The request was not modified after being signed.
- The request was created by an entity that possesses the Consumer Secret and Token Secret.
OAuth 1.0 does not encrypt requests. It signs them.
This is why HTTPS is still important — the signature protects integrity and authenticity, while HTTPS protects confidentiality. We will explore this relationship in detail later in this post.
The Complete Signing Process
Here is the full pipeline that produces an OAuth 1.0 signature:
Let’s walk through each piece.
Step 1 — Collect the Parameters
For a request like:
GET /photos?page=1 HTTP/1.1
Host: api.photoprovider.example.com
The OAuth parameters are:
oauth_consumer_key=photo-app-123oauth_token=ACCESS_123oauth_signature_method=HMAC-SHA1oauth_timestamp=1780000120oauth_nonce=b2a8c7e4oauth_version=1.0
Plus the query parameter:
page=1
Step 2 — Normalize and Sort Parameters
The normalized parameter set includes:
- OAuth parameters
- query parameters
- applicable form-encoded body parameters (if
application/x-www-form-urlencoded)
The oauth_signature parameter itself is excluded. These parameters are:
- Percent-encoded (both names and values)
- Sorted first by encoded name, then by encoded value
- Joined with
&
| Parameter | Value |
|---|---|
oauth_consumer_key | photo-app-123 |
oauth_nonce | b2a8c7e4 |
oauth_signature_method | HMAC-SHA1 |
oauth_timestamp | 1780000120 |
oauth_token | ACCESS_123 |
oauth_version | 1.0 |
page | 1 |
Step 3 — Construct the Signature Base String
The signature base string concatenates three pieces:
HTTP_METHOD & NORMALIZED_URL & NORMALIZED_PARAMETERS
Normalized URL Rule: The query string itself is NOT simply appended to the normalized URL. Instead, query parameters go into the normalized parameters from Step 2. Also, the normalized URL strips the port if it is the default for the scheme.
- Request URL:
https://api.photoprovider.example.com:443/photos?page=1 - Normalized URL:
https://api.photoprovider.example.com/photos
Concretely, the final base string concatenates the method, the normalized URL, and the normalized parameters:
GET&https%3A%2F%2Fapi.photoprovider.example.com%2Fphotos&oauth_consumer_key%3Dphoto-app-123%26oauth_nonce%3Db2a8c7e4%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1780000120%26oauth_token%3DACCESS_123%26oauth_version%3D1.0%26page%3D1
The & separators and percent-encoding ensure that the base string is unambiguous.
Step 4 — Construct the Signing Key
The signing key combines two secrets:
Signing Key = consumer_secret & token_secret
For a protected resource request:
Signing Key = SECRET_APP & ACCESS_SECRET
For a Request Token request (no token secret yet):
Signing Key = SECRET_APP &
(Note the trailing & — the token secret is empty but the separator is still present.)
Step 5 — Compute HMAC-SHA1
HMAC-SHA1(
key = "SECRET_APP&ACCESS_SECRET",
data = <signature base string>
)
SHA-1 is the hash function, and HMAC-SHA1 is the keyed authentication mechanism built using HMAC + SHA-1. This step produces a binary hash (the MAC).
Step 6 — Base64 Encode
The binary hash is Base64-encoded, and OAuth percent-encodes the resulting value to produce the final signature string:
oauth_signature = "kYjzVBB8Y0ZFabxSWbWovY3uYSQ%3D"
The Final Signed Request
GET /photos?page=1 HTTP/1.1
Host: api.photoprovider.example.com
Authorization: OAuth
oauth_consumer_key="photo-app-123",
oauth_token="ACCESS_123",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1780000120",
oauth_nonce="b2a8c7e4",
oauth_version="1.0",
oauth_signature="kYjzVBB8Y0ZFabxSWbWovY3uYSQ%3D"
What Exactly Is Being Signed?
Because beginners often think, “The token is signed,” it’s crucial to understand what is actually happening.
The request representation is signed, not the token:
- Access Token ≠ Signature
- Access Token → identifies the authorization
- Signature → authenticates and integrity-protects this particular request
This distinction is extremely important.
Where the Secrets Participate
This is a crucial point that must be completely clear:
[!IMPORTANT] The Consumer Secret and Token Secret are NOT sent as part of the API request. They are used LOCALLY to generate the signature.
Here is exactly where each secret is used:
What IS sent in the request:
oauth_consumer_key = photo-app-123(identifier, not secret)oauth_token = ACCESS_123(identifier, not secret)oauth_signature = kYjzVBB8Y0...(derived from secrets)
What is NOT sent:
consumer_secret = SECRET_APP(used locally for signing)access_token_secret = ACCESS_SECRET(used locally for signing)
The secrets participate only in the signing key:
The secrets never travel over the network. They exist only on PhotoApp’s server and PhotoProvider’s server.
PhotoProvider has its own copies of SECRET_APP and ACCESS_SECRET. When it receives a request, it independently constructs the same signing key, computes the expected signature, and compares it with the received signature.
Provider-Side Verification
When PhotoProvider receives a signed request, it performs the following verification:
What Happens If One Character Changes?
Suppose a request is for:
GET /photos?page=1
The signature covers the entire normalized request, including page=1. If an attacker intercepts the request and changes it to:
GET /photos?page=2
What happens?
Original request:
- Signature base string includes
"page=1" - Signature =
HMAC-SHA1(key, ...page%3D1...) - Result:
kYjzVBB8Y0ZFabxSWbWovY3uYSQ=
Modified request:
- Signature base string would include
"page=2" - Expected signature =
HMAC-SHA1(key, ...page%3D2...) - Result: completely different value
But the attacker sent the ORIGINAL signature. PhotoProvider recalculates → different result → Signature mismatch → REQUEST REJECTED
The attacker cannot generate a new valid signature because they don’t have SECRET_APP and ACCESS_SECRET.
This is the integrity property of request signing. Any modification to the HTTP method, URL, query parameters, or OAuth parameters invalidates the signature.
What Is a Replay Attack?
Before explaining nonces, let’s first understand the attack they prevent.
The Attack
A replay attack occurs when an attacker captures a valid, signed request and re-sends it:
The attacker doesn’t need to know the secrets. They don’t need to forge a signature. They simply re-send a legitimate request.
Why This Is Dangerous
- Read requests → data leakage
- Write requests → unauthorized actions repeated
- Transfer requests → money moved multiple times
OAuth 1.0 mitigates replay attacks using nonce + timestamp.
Nonce
A nonce (number used once) is a value generated by the Consumer. The Consumer should generate a nonce that is unique for each request, and the Provider rejects nonce reuse within its validity window.
oauth_nonce = b2a8c7e4
How It Prevents Replay
Original request:
oauth_nonce = ABC123oauth_timestamp = 1780000120oauth_signature = XYZ...↓ PhotoProvider accepts the request ↓ PhotoProvider records the nonce within its replay-detection state for the applicable validity window.
Replayed request:
oauth_nonce = ABC123(same)oauth_timestamp = 1780000120(same)oauth_signature = XYZ...(same, still valid) ↓ PhotoProvider checks its records ↓ “I’ve already seennonce=ABC123withtimestamp=1780000120” ↓ REQUEST REJECTED
The Nonce Is Not a Secret
The nonce does not need to be confidential. It appears in plain text in the request. Its power comes from being unique, not from being hidden.
Nonce is useful because:
- It is unique per request
- The provider tracks which nonces it has seen
Nonce is NOT useful because:
- It is secret (it is not)
- It is unpredictable (although unpredictability helps)
How the Provider Tracks Nonces
PhotoProvider needs to maintain a record of recently used nonces for replay detection. But it does not need to store them forever.
Provider nonce tracking:
Store nonces for a validity window (e.g., 5 minutes).
Nonce + Timestamp → storedAfter the window expires, old nonces can be discarded because the timestamp check will reject old requests anyway.
The timestamp limits the window in which a nonce needs to be tracked.
Timestamp
oauth_timestamp = 1780000120
The timestamp is a Unix timestamp (seconds since January 1, 1970) representing when the request was created.
What It Does
PhotoProvider can reject requests whose timestamps are too far from the current time:
- Server time:
1780000200 - Request time:
1780000120 - Difference:
80seconds
If the provider allows ±5 minutes (300 seconds): 80 < 300 → ✅ Accepted
If the request is replayed 10 minutes later:
- Server time:
1780000720 - Difference:
600seconds 600 > 300→ ❌ Rejected (too old)
Why Timestamp Alone Is Not Enough
A timestamp prevents replays after the validity window, but within that window, the same request could be replayed. That is where the nonce comes in.
Nonce + Timestamp Together
Neither mechanism is sufficient alone. Together, they provide robust replay protection:
Nonce alone:
- ✅ Can detect replay of the same nonce
- ❌ Requires maintaining enough nonce history to detect reuse for as long as those requests could be accepted
Timestamp alone:
- ✅ Rejects old requests
- ❌ Cannot detect replays within the validity window
Nonce + Timestamp:
- ✅ Nonce detects replays within the window
- ✅ Timestamp eliminates the need to store old nonces
- ✅ Practical and effective
[!TIP] Replay Protection Model
- Nonce identifies this particular request.
- Timestamp limits when the request is valid.
- Together, replays are detected within the window, and rejected outside it.
Complete Replay Mitigation Example
Time T=0: PhotoApp sends request
nonce = NONCE_1,timestamp = 1780000000,signature = SIG_A- PhotoProvider: ✅ Accept, and store
(NONCE_1, 1780000000)
Time T=30s: Attacker replays
nonce = NONCE_1,timestamp = 1780000000,signature = SIG_A- PhotoProvider: nonce
NONCE_1at timestamp1780000000already seen → ❌ Reject
Time T=10min: Attacker replays
nonce = NONCE_1,timestamp = 1780000000,signature = SIG_A- PhotoProvider: timestamp
1780000000is too old → ❌ Reject
Both attacks are defeated.
HTTPS vs OAuth Signature
This distinction deserves careful attention because the two mechanisms protect different things.
What HTTPS/TLS Protects
- Encrypts communication between client and server
- Prevents eavesdropping on the network
- TLS with proper certificate validation provides protection against network MITM attacks
- Protects secrets while they travel over the wire
What OAuth Signatures Protect
- Proves the request was created by someone with the secrets
- Proves the request was not modified after signing
- Helps detect replayed requests (with nonce + timestamp)
- Provides request-level integrity/authentication in addition to transport security
Why One Doesn’t Replace the Other
| HTTPS protects | HTTPS does NOT protect |
|---|---|
| Secrets in transit (consumer_secret, token_secret are safe while traveling) | Secrets after they reach the client (compromised storage, logging, memory dumps) |
| Request content from eavesdroppers | Against a compromised client that sends malicious requests |
| Integrity of the connection | Against compromised endpoints (e.g., a rogue proxy on the server side after TLS termination) |
| OAuth signature provides | OAuth signature does NOT provide |
|---|---|
| Proof of request authenticity | Confidentiality (request content is NOT encrypted) |
| Proof of request integrity | Protection of secrets during transit (that’s HTTPS’s job) |
| Replay detection (with nonce + timestamp) |
The Bottom Line
OAuth 1.0 signing does NOT make HTTPS unnecessary.
HTTPS protects the secrets and content in transit. OAuth signing provides request-level authenticity and integrity.
In practice, OAuth 1.0 deployments should use HTTPS as well.
Why Every API Request Needs a New Signature
Each signature is specific to a particular request. Two identical-looking requests at different times will have different signatures because:
Request 1:
oauth_timestamp = 1780000120oauth_nonce = abc123oauth_signature= (computed from these values)
Request 2 (same endpoint, 5 seconds later):
oauth_timestamp = 1780000125oauth_nonce = def456oauth_signature= (computed from these NEW values)
The timestamp and nonce are different, so the signature base string is different, and the resulting signature is different.
This is by design. If every request to the same endpoint produced the same signature, replay detection would be impossible.
- Same URL + Same method + Different timestamp/nonce
- = Different signature base string
- = Different signature
This is why two identical API calls have different signatures. This is why each request must be freshly signed.
Common Signature Errors
When implementing OAuth 1.0 signing, the most common error is:
"Invalid Signature"
This typically means the signature PhotoApp computed does not match the signature PhotoProvider computed. Common causes:
| Cause | What went wrong |
|---|---|
| Parameter sorting | Parameters not in alphabetical order |
| Percent-encoding | Characters not encoded correctly (e.g., spaces) |
| URL normalization | Port number included when it shouldn’t be, or scheme mismatch |
| Wrong secret | Using request_token_secret instead of access_token_secret |
| Missing trailing & | Signing key should be secret& even when token secret is empty |
| Clock skew | Client/server clocks differ significantly |
| Including oauth_signature in the base string | The signature itself must NOT be included in the parameters being signed |
What Comes Next
You now understand:
- Why requests must be signed — bearer tokens are vulnerable.
- How signatures are computed — the full HMAC-SHA1 pipeline.
- How the provider verifies — independent signature recalculation.
- How nonces and timestamps prevent replay attacks.
- Why HTTPS and signing are both necessary.
In the final part, we will examine the security model as a whole — what happens when secrets are compromised, how OAuth 1.0a improved on OAuth 1.0, how OAuth 1.0 compares to OAuth 2.0 architecturally, and a comprehensive set of interview questions organized by difficulty and topic.
Key Takeaways
- OAuth 1.0 SIGNS requests, it does NOT ENCRYPT them. Signing = request integrity + authentication of the party possessing the signing secrets. It does NOT authenticate the Resource Owner. Encryption = confidentiality (that’s HTTPS).
- The signing pipeline: Method + URL + Parameters → Base String, Consumer Secret + Token Secret → Signing Key, HMAC-SHA1(Key, Base String) → Base64 → Signature
- Secrets (Consumer Secret, Token Secret) are NEVER sent in the request. They are used locally to sign.
- The provider independently recalculates the signature using its own copies of the secrets.
- Changing ONE character in the request invalidates the signature.
- Nonce = value intended to be unique for each request within the relevant Consumer/token context and validity window. Timestamp = limits the window in which a nonce needs to be tracked. Together = robust replay protection.
- HTTPS protects data in transit. OAuth signature proves request authenticity. In practice, OAuth 1.0 deployments should use HTTPS as well.