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

EntityPropertyValue
PhotoApp (Consumer)consumer_keyphoto-app-123
consumer_secretSECRET_APP
Alice’s Access Tokenaccess_tokenACCESS_123
access_token_secretACCESS_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?

  1. 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).
  2. 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.
  3. 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.

EncryptionSignature
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 messageDoes NOT hide the content. Anyone can read the request
Example: TLS/HTTPSExample: 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:

  1. The request was not modified after being signed.
  2. 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:

HTTP Method

Signature Base String

Normalized URL

Normalized Parameters

HMAC-SHA1

Signing Key

Base64 Encode

OAuth 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-123
  • oauth_token = ACCESS_123
  • oauth_signature_method = HMAC-SHA1
  • oauth_timestamp = 1780000120
  • oauth_nonce = b2a8c7e4
  • oauth_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:

  1. Percent-encoded (both names and values)
  2. Sorted first by encoded name, then by encoded value
  3. Joined with &
ParameterValue
oauth_consumer_keyphoto-app-123
oauth_nonceb2a8c7e4
oauth_signature_methodHMAC-SHA1
oauth_timestamp1780000120
oauth_tokenACCESS_123
oauth_version1.0
page1

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:

HTTP Method + Normalized URL + Normalized Parameters + Secrets

HMAC

Signature

  • 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:

Consumer Secret

Signing Key

Token Secret

HMAC-SHA1

Signature

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:

Receive request

Extract OAuth parameters

Identify Consumer

Look up oauth_consumer_key

Identify Token

Look up oauth_token

Validate Timestamp

Validate Nonce

Normalize request

Generate expected signature

HMAC-SHA1

Compare signatures

Validate token permissions

Accept or Reject

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:

PhotoProviderAttackerPhotoAppPhotoProviderAttackerPhotoApp✅ Valid → Photos returnedReplays exact same requestSignature is mathematically validGET /photos?page=1 (oauth_signature=XYZ...)Intercepts requestGET /photos?page=1 (oauth_signature=XYZ...)

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 = ABC123
  • oauth_timestamp = 1780000120
  • oauth_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 seen nonce=ABC123 with timestamp=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 → stored

After 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: 80 seconds

If the provider allows ±5 minutes (300 seconds): 80 < 300 → ✅ Accepted

If the request is replayed 10 minutes later:

  • Server time: 1780000720
  • Difference: 600 seconds
  • 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_1 at timestamp 1780000000 already seen → ❌ Reject

Time T=10min: Attacker replays

  • nonce = NONCE_1, timestamp = 1780000000, signature = SIG_A
  • PhotoProvider: timestamp 1780000000 is 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 protectsHTTPS 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 eavesdroppersAgainst a compromised client that sends malicious requests
Integrity of the connectionAgainst compromised endpoints (e.g., a rogue proxy on the server side after TLS termination)
OAuth signature providesOAuth signature does NOT provide
Proof of request authenticityConfidentiality (request content is NOT encrypted)
Proof of request integrityProtection 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 = 1780000120
  • oauth_nonce = abc123
  • oauth_signature = (computed from these values)

Request 2 (same endpoint, 5 seconds later):

  • oauth_timestamp = 1780000125
  • oauth_nonce = def456
  • oauth_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:

CauseWhat went wrong
Parameter sortingParameters not in alphabetical order
Percent-encodingCharacters not encoded correctly (e.g., spaces)
URL normalizationPort number included when it shouldn’t be, or scheme mismatch
Wrong secretUsing request_token_secret instead of access_token_secret
Missing trailing &Signing key should be secret& even when token secret is empty
Clock skewClient/server clocks differ significantly
Including oauth_signature in the base stringThe 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

  1. 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).
  2. The signing pipeline: Method + URL + Parameters → Base String, Consumer Secret + Token Secret → Signing Key, HMAC-SHA1(Key, Base String) → Base64 → Signature
  3. Secrets (Consumer Secret, Token Secret) are NEVER sent in the request. They are used locally to sign.
  4. The provider independently recalculates the signature using its own copies of the secrets.
  5. Changing ONE character in the request invalidates the signature.
  6. 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.
  7. HTTPS protects data in transit. OAuth signature proves request authenticity. In practice, OAuth 1.0 deployments should use HTTPS as well.