
# Request signing

## 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

## Request headers

| 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

<strong>Field description</strong>

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

<strong>Signature format rules if queryString is empty</strong>

`timestamp + nonce + method.toUpperCase() + requestPath + body`

<strong>Signature format rules if queryString is not empty</strong>

`timestamp + nonce + method.toUpperCase() + requestPath + "?" + queryString + body`


<strong>Example</strong>

<strong>Example 1: Without query string (token request)</strong>

- timestamp = `1710000000000`
- nonce = `abc123xyz`
- method = `"POST"`
- requestPath = `"/v1/oauth/token"`
- body = `{"grantType":"authorization_code","code":"code123","redirectUri":"https://client.example.com/callback","codeVerifier":"verifier123"}`

<strong>Generate the string to be signed:</strong>

`1710000000000abc123xyzPOST/v1/oauth/token{"grantType":"authorization_code","code":"code123","redirectUri":"https://client.example.com/callback","codeVerifier":"verifier123"}`


<strong>Steps to generate the final signature</strong>

1. 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
```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();
        }
    }
}
```

