Overview
On the OAuth authorization platform, the access_token endpoint is a sensitive API that is called by third-party servers to obtain tokens. In addition to validating standard OAuth parameters, the server must also verify that the request is initiated by a registered client, ensure the request has not been tampered with during transmission, and prevent replay attacks. To achieve this, a client-side request signing mechanism based on HMAC-SHA256 is introduced, providing:
Client authentication
Request integrity protection
Replay attack prevention
Header name Required Description Client-Id Yes Identifier for the client, corresponding to the registered client_id Sign Yes The request signature Timestamp Yes The request initiation timestamp in milliseconds Nonce Yes A unique, single-use random string to prevent replay attacks
Signing rules
Field description
timestamp: The request timestamp (in milliseconds) must match the Timestamp header
nonce: A one-time random string. Must match the Nonce header and is used to prevent replay attacks.
method: The HTTP request method (such as POST, GET), in uppercase.
requestPath: The API endpoint path, such as /v1/oauth/token.
queryString: The query string after ? in the request URL. Leave empty if not present.
body: The request payload as a string. If there is no request body (typically for GET requests), use an empty string.
Signature format rules if queryString is empty
timestamp + nonce + method.toUpperCase() + requestPath + body
Signature format rules if queryString is not empty
timestamp + nonce + method.toUpperCase() + requestPath + "?" + queryString + body
Example
Example 1: Without query string (token request)
timestamp = 1710000000000
nonce = abc123xyz
method = "POST"
requestPath = "/v1/oauth/token"
body = {"grantType":"authorization_code","code":"code123","redirectUri":"https://client.example.com/callback","codeVerifier":"verifier123"}
Generate the string to be signed:
1710000000000abc123xyzPOST/v1/oauth/token{"grantType":"authorization_code","code":"code123","redirectUri":"https://client.example.com/callback","codeVerifier":"verifier123"}
Steps to generate the final signature
Use the clientSecret to compute an HMAC-SHA256 hash of the baseString and encode the result using Base64.
Sign = Base64(HMAC-SHA256(clientSecret, baseString))
Signature example
Java
package com.weex.utils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;
public class OAuthApiClient {
/**
* Replace with your actual clientId / clientSecret
*/
private static final String CLIENT_ID = "your-client-id" ;
private static final String CLIENT_SECRET = "your-client-secret" ;
/**
* Replace with your OAuth authorization server base URL
*/
private static final String BASE_URL = "https://auth.example.com" ;
/**
* HMAC-SHA256 algorithm
*/
private static final String HMAC_SHA256 = "HmacSHA256" ;
/**
* Generate signature
*
* Signing rules:
* 1. Without queryString:
* timestamp + nonce + method.toUpperCase() + requestPath + body
*
*2. With queryString:
* timestamp + nonce + method.toUpperCase() + requestPath + queryString + body
*
* Notes:
* - Use "" if queryString is empty
* - If not empty, queryString should include "?a=1&b=2"
* - Use "" if body is empty
*/
public static String generateSignature (String clientSecret ,
String timestamp ,
String nonce ,
String method ,
String requestPath ,
String queryString ,
String body ) throws Exception {
String safeQueryString = queryString == null ? "" : queryString;
String safeBody = body == null ? "" : body;
String message = timestamp
+ nonce
+ method. toUpperCase ()
+ requestPath
+ safeQueryString
+ safeBody;
return hmacSha256Base64 (clientSecret, message);
}
/**
* Computes HMAC-SHA256 and encodes the result in Base64.
*/
private static String hmacSha256Base64 (String secretKey , String message ) throws Exception {
SecretKeySpec secretKeySpec = new SecretKeySpec (
secretKey. getBytes (StandardCharsets.UTF_8),
HMAC_SHA256
);
Mac mac = Mac. getInstance (HMAC_SHA256);
mac. init (secretKeySpec);
byte [] signatureBytes = mac. doFinal (message. getBytes (StandardCharsets.UTF_8));
return Base64. getEncoder (). encodeToString (signatureBytes);
}
/**
* Generates a timestamp in milliseconds
*/
private static String generateTimestamp () {
return String. valueOf (System. currentTimeMillis ());
}
/**
* Generates a random nonce
*/
private static String generateNonce () {
return UUID. randomUUID (). toString (). replace ( "-" , "" );
}
/**
* Sends a POST request
*
* Notes:
* - The body must be the exact JSON string sent in the request
* - The body used for signing must match the request body exactly
*/
public static String sendPost (String clientId ,
String clientSecret ,
String requestPath ,
String queryString ,
String body ) throws Exception {
String timestamp = generateTimestamp ();
String nonce = generateNonce ();
String signature = generateSignature (
clientSecret,
timestamp,
nonce,
"POST" ,
requestPath,
queryString,
body
);
String url = BASE_URL + requestPath + (queryString == null ? "" : queryString);
HttpPost postRequest = new HttpPost (url);
postRequest. setHeader ( "Client-Id" , clientId);
postRequest. setHeader ( "Sign" , signature);
postRequest. setHeader ( "Timestamp" , timestamp);
postRequest. setHeader ( "Nonce" , nonce);
postRequest. setHeader ( "Content-Type" , "application/json" );
StringEntity entity = new StringEntity (body, StandardCharsets.UTF_8);
postRequest. setEntity (entity);
try (CloseableHttpClient httpClient = HttpClients. createDefault ();
CloseableHttpResponse response = httpClient. execute (postRequest)) {
int statusCode = response. getStatusLine (). getStatusCode ();
String responseBody = EntityUtils. toString (response. getEntity (), StandardCharsets.UTF_8);
System.out. println ( "HTTP Status: " + statusCode);
return responseBody;
}
}
/**
* Example: Exchange authorization_code for token
*/
public static void main ( String [] args ) {
try {
String requestPath = "/v1/oauth/token" ;
/**
* Note:
* This must be the exact JSON string sent in the request.
* The same body must be used for both signing and sending.
*/
String body = "{"
+ " \" grantType \" : \" authorization_code \" ,"
+ " \" code \" : \" auth_code_xxx \" ,"
+ " \" redirectUri \" : \" https://client.example.com/callback \" ,"
+ " \" codeVerifier \" : \" code_verifier_xxx \" "
+ "}" ;
String response = sendPost (
CLIENT_ID,
CLIENT_SECRET,
requestPath,
"" ,
body
);
System.out. println ( "Response: " + response);
} catch (Exception e ) {
e. printStackTrace ();
}
}
}