Updated on 2024-03-05 GMT+08:00

Developing an Authorization Signature Generation Mechanism

You need to develop an authentication mechanism for generating an authorization signature. The CEC needs to use the generated authorization signature for authentication.

Before the development, you need to learn about the internal rules for generating an authorization signature, as shown in Figure 1.

Figure 1 Main rules of the authorization signature generation mechanism

After learning about the preceding principles and rules, perform the following steps to develop the authorization signature authentication mechanism:

  1. Generate SignedHeaders by traversing the header names involved in encoding in the HTTP header.

    1. Change the header names to lowercase, that is, invoke the lowerCase() function. For details about the tool class SignerUtils, see 1. The following is an example:
      private Map<String, String> lowerCaseSignedHeaders(Map<String, String> signedHeaders) {
           if ((null == signedHeaders) || signedHeaders.isEmpty()) {
               throw new IllegalArgumentException("signedHeaders cann't be null.");
           }
           Map<String, String> headers = new HashMap<>(SignerUtils.HASH_MAP_INITIALIZATION_SIZE);
           for (Map.Entry<String, String> e : signedHeaders.entrySet()) {
               String name = e.getKey();
               String value = e.getValue();
               headers.put(name.toLowerCase(Locale.ENGLISH), value.trim());
           }
           return headers; 
      }
    2. Use semicolons (;) to separate the header names in 1-1 to generate a record. Do not add a semicolon (;) to the last field.
    3. Sort all records in 1-1 in alphabetical order and combine them into a large string in sequence. The following is an example:
      private String appendSignedHeaders(StringBuilder buffer) {
           int start = buffer.length();
            Set<String> headerNames = new TreeSet<>(this.signedHeaders.keySet());
           for (String name : headerNames) {
               buffer.append(name).append(';');
           }
           buffer.deleteCharAt(buffer.length() - 1);
            int end = buffer.length();
           String signedHeadersStr = buffer.substring(start, end);
           return signedHeadersStr; 
      }

      Encode the following header:

      Content-Length="***"

      Content-Type="application/json;charset=UTF-8"

  2. Generate authStringPrefix.

    Combine the following fields with slashes (/): authVersion, accessKey, timestamp, and SignedHeaders. The format is as follows:

    authStringPrefix="auth-v2/{accessKey}/{timestamp}/{SignedHeaders}";
    • auth-v2: Authentication version number. In this version, the value is fixed to auth-v2.
    • accessKey: The third-party system uses configId (channel ID) as the unique ID.
    • timestamp: Time when the third-party system initiates a service. The value is a string. The time string is formatted to the yyyy-MM-dd'T'HH:mm:ss.SSS'Z format.
    • SignedHeaders: Header names involved in encoding in the HTTP header, which is generated in 1.

  3. Generate signingKey.

    Encrypt the value of authStringPrefix generated in 2 using SHA256HEX. SecretKey is the key configured by the third-party system on the channel configuration page. The following is an example of the SHA256HEX algorithm. For details about the tool class SignerUtils, see 1.

    public static String sha256Hex(String key, String toSigned) throws NoSuchAlgorithmException,InvalidKeyException, UnsupportedEncodingException {
         Mac mac = Mac.getInstance("HmacSHA256");
         mac.init(new SecretKeySpec(key.getBytes(SignerUtils.CHARSET), "HmacSHA256"));
         String digit = new String(SignerUtils.encodeHex(mac.doFinal(toSigned.getBytes(SignerUtils.CHARSET))));
         return digit; 
    }

  4. Generate CanonicalHeaders.

    The encoding rule is the same as that of SignedHeaders, but header value encoding is added.

    1. Traverse the header names involved in encoding in the HTTP header and change the header names to lowercase. That is, invoke the lowerCase() function. For details, see 1.
    2. Invoke the normalize function to format the converted lowercase string. For details about the tool class PathUtils, see 2.
      /**
        * normalize
        * @param value Payload information
        * @return builder
        */
      public static String normalize(String value) {
           try {
               StringBuilder builder = new StringBuilder(PathUtils.DEFAULT_CAPACIT);
               for (byte b : value.getBytes(PathUtils.CHARSET)) {
                   if (PathUtils.URI_UNRESERVED_CHARACTERS.get(b & 0xFF)) {
                       builder.append((char) b);
                   } else {
                       builder.append(PathUtils.PERCENT_ENCODED_STRINGS[b & 0xFF]);
                   }
               }
               return builder.toString();
           } catch (UnsupportedEncodingException e) {
               throw new RuntimeException(e);
           } 
      }
    3. Sort the records in 4-1 in alphabetical order.
    4. Traverse the sorted records and add the string "\n" between the records to form a large string. Do not add the string "\"n to the last record.

  5. Generate canonicalRequest.

    Combine the HttpMethod, HttpURI, SignedHeaders, CanonicalHeaders, and NormalizePath fields with "\n". Do not add "\n" to the last record. For details about the tool class PathUtils, see 2.

    private String canonicalRequest() {
         StringBuilder buffer = new StringBuilder(PathUtils.DEFAULT_CAPACITY);
         buffer.append(this.httpMethod).append(System.lineSeparator());
         buffer.append(this.uri).append(System.lineSeparator());
           this.appendSignedHeaders(buffer);
         buffer.append(System.lineSeparator());
           this.appendCanonicalHeaders(buffer);
         buffer.append(System.lineSeparator());
           if (this.isNotEmpty(this.payload))
          {
             buffer.append(PathUtils.normalize(this.payload));
          }
          return buffer.toString();
     }

    The format is as follows:

    CanonicalRequest = $HttpMethod + "\n" + $HttpURI+ "\n" + SignedHeaders($HttpHeaders) + "\n" + CanonicalHeaders ($HttpHeaders) + "\n" + NormalizePath($HttpBody)
    • The CanonicalRequest parameter is described as follows:

      $HttpMethod: GET, PUT, and POST requests defined in the HTTPS protocol. The value must be all in uppercase.

      $HttpURI: API request URI. The value must start with a slash (/). If the value does not start with a slash (/), add it. An example value is /service-cloud/webclient/chat_client/js/newThirdPartyClient.js. The value / indicates an empty path.

      SignedHeaders: SignedHeaders generated in 1.

      CanonicalHeaders: CanonicalHeaders generated in 4.

      NormalizePath: Body after formatting.

    • Only the following parameters in NormalizePath are encoded:

      thirdUserName: Enterprise username.

      thirdUserId: Enterprise user ID.

      tenantSpaceId: Tenant space ID provided by the enterprise.

      channelConfigId: Enterprise access channel ID.

  6. Generate signature. Encrypt CanonicalRequest generated in 5 using SHA256HEX. The encryption key is signingKey generated in 3.
  7. Generate Authorization (signature). Combine authStringPrefix generated in 2 and signature generated in 6 with a slash (/). The format is as follows:

    Authorization:$authStringPrefix/$Signature

Reference

The authentication mechanism for generating an authorization signature involves the tool classes SignerUtils and PathUtils. Their formats are as follows:

  1. SignerUtils
    import java.nio.charset.StandardCharsets;
    import java.util.HashMap;
    import java.util.Locale;
    import java.util.Map;
    
    public class SignerUtils {
        private static final int HASH_MAP_INITIALIZATION_SIZE = 5;
        private static final int ONE_CHAR_BITS_NUM = 4;
        private static final String CHARSET = "UTF-8";
        private static final char[] DIGITS_LOWERS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a',
            'b', 'c', 'd', 'e', 'f'};
        private SignerUtils() {
        }
    
        private static char[] encodeHex(final byte[] data) {
            final int le = data.length;
            final char[] outs = new char[le << 1];
            for (int i = 0, j = 0; i < le; i++) {
                outs[j++] = SignerUtils.DIGITS_LOWERS[(0xF0 & data[i]) >>> ONE_CHAR_BITS_NUM];
                outs[j++] = SignerUtils.DIGITS_LOWERS[0x0F & data[i]];
            }
            return outs;
        }
    }
  2. PathUtils
    import java.io.UnsupportedEncodingException;
    import java.util.BitSet;
    import java.util.Locale;
    import java.util.concurrent.CompletionException;
    
    public class PathUtils {
        private static final String CHARSET = "UTF-8";
        private static final int NUM_256 = 256;
        private static final int DEFAULT_CAPACITY = 16;
        private static final BitSet URI_UNRESERVED_CHARACTERS = new BitSet();
        private static final String[] PERCENT_ENCODED_STRINGS = new String[NUM_256];
        static {
            for (int i = 97; i <= 122; i++) {
                PathUtils.URI_UNRESERVED_CHARACTERS.set(i);
            }
            for (int i = 65; i <= 90; i++) {
                PathUtils.URI_UNRESERVED_CHARACTERS.set(i);
            }
            for (int i = 48; i <= 57; i++) {
                PathUtils.URI_UNRESERVED_CHARACTERS.set(i);
            }
            PathUtils.URI_UNRESERVED_CHARACTERS.set(45);
            PathUtils.URI_UNRESERVED_CHARACTERS.set(46);
            PathUtils.URI_UNRESERVED_CHARACTERS.set(95);
            PathUtils.URI_UNRESERVED_CHARACTERS.set(126);
    
            for (int i = 0; i < PathUtils.PERCENT_ENCODED_STRINGS.length; i++) {
                PathUtils.PERCENT_ENCODED_STRINGS[i] = String.format(Locale.ROOT, "%%%02X", new Object[]{Integer.valueOf(i)});
            }
        }
        private PathUtils() {}
    }