Search code examples
javaamazon-web-servicesaws-chime-sdk

Connect to websocket to consuming amazon chime API in real time


I want to expand my software, written in JavaFX, with Amazon Chime API to consume its messaging. I know there's JS SDK that allows establish messaging websocket session with no problems. But in java SDK there're no related classes. So I want to use STOMP library to consuming the websocket endpoint.

At the time I am struggling with making correct request, namely with signing AWS request (calculating X-AMZ-Signature)

According to the post I'm trying to calculate correct X-AMZ-Signature request parameter. Here's the class:

@Slf4j
@Service
public class Aws4Signer {
    
    private final static String REQUEST_CONTENT_TYPE = "application/json";
    private final static String AUTH_ALGORITHM = "AWS4-HMAC-SHA256";
    private final static String REQUEST_METHOD = "GET";

    @Data
    class AuthenticationData {
        @NonNull
        String timestamp;
        @NonNull
        String date;
        @NonNull
        String authorizationHeader;
    }
    
    private AppConfig appConfig = new AppConfig();

    /**
     * Gets the timestamp in YYYYMMDD'T'HHMMSS'Z' format, which is the required
     * format for AWS4 signing request headers and credential string
     *
     * @param dateTime
     *            an OffsetDateTime object representing the UTC time of current
     *            signing request
     * @return the formatted timestamp string
     *
     * @see <a href=
     *      "https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html">
     *      Examples of the Complete Version 4 Signing Process (Python)</a>
     */
    public String getTimeStamp(OffsetDateTime dateTime) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");
        String formatDateTime = dateTime.format(formatter);
        return formatDateTime;
    }

    /**
     * Gets the date string in yyyyMMdd format, which is required to build the
     * credential scope string
     *
     * @return the formatted date string
     */
    public String getDate(OffsetDateTime dateTime) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        String formatDateTime = dateTime.format(formatter);
        return formatDateTime;
    }

    public byte[] generateAws4SigningKey(String timestamp) {
        String secretKey = appConfig.getAwsAuthConfig().getSecretKey();
        String regionName = appConfig.getAwsAuthConfig().getServiceRegion();
        String serviceName = appConfig.getAwsAuthConfig().getServiceName();

        byte[] signatureKey = null;
        try {
            signatureKey = Aws4SignatureKeyGenerator.generateSignatureKey(secretKey, timestamp, regionName,
                    serviceName);
        } catch (Exception e) {
            log.error("An error has ocurred when generate signature key: " + e, e);
        }

        return signatureKey;
    }

    /**
     * Builds an {@link AuthenticationData} object containing the timestamp, date,
     * payload hash and the AWS4 signature
     * <p>
     *
     * The signing logic was translated from the Python implementation, see this
     * link for more details: <a href=
     * "https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html">Examples
     * of the Complete Version 4 Signing Process (Python)</a>
     *
     * @param target
     * @param requestBody
     *
     * @return
     * @throws NoSuchAlgorithmException
     * @throws UnsupportedEncodingException
     * @throws InvalidKeyException
     * @throws SignatureException
     * @throws IllegalStateException
     *
     */
    public AuthenticationData buildAuthorizationData() throws NoSuchAlgorithmException,
            UnsupportedEncodingException, InvalidKeyException, SignatureException, IllegalStateException {
        log.info("predict - start");

        // Starting building the lengthy signing data
        AwsAuthConfig awsAuthConfig = appConfig.getAwsAuthConfig();
        String payloadHash = Hmac.getSha256Hash(requestBody);

        OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
        String timestamp = getTimeStamp(now);
        String date = getDate(now);

        // Step 1 is to define the verb (GET, POST, etc.) -- already done by defining
        // constant REQUEST_METHOD

        // Step 2: Create canonical URI--the part of the URI from domain to query
        // string (use '/' if no path)
        String canonical_uri = "/connect";

        // Step 3: Create the canonical query string. In this example, request
        // parameters are passed in the body of the request and the query string
        // is blank.
        String canonical_querystring = buildCanonicalQueryString();

        // Step 4: Create the canonical headers. Header names must be trimmed
        // and lowercase, and sorted in code point order from low to high.
        // Note that there is a trailing \n.
        String canonical_headers = "content-type:" + REQUEST_CONTENT_TYPE + "\n"
                                 + "host:" + awsAuthConfig.getServiceHost() + "\n"
                                 + "x-amz-date:" + timestamp + "\n";
        
        String signed_headers = "content-type;host;x-amz-date";

        log.debug("canonical_headers : {}", canonical_headers);

        String canonical_request = REQUEST_METHOD + "\n" + canonical_uri + "\n" + canonical_querystring + "\n"
                + canonical_headers + "\n" + signed_headers;

        log.debug("canonical_request : {}", canonical_request);

        String credential_scope = date + "/" + awsAuthConfig.getServiceRegion() + "/" + awsAuthConfig.getServiceName()
                + "/" + "aws4_request";
        String canonical_request_hash = Hmac.getSha256Hash(canonical_request);

        log.debug("canonical_request_hash : {}", canonical_request_hash);

        String string_to_sign = AUTH_ALGORITHM + "\n" + timestamp + "\n" + credential_scope + "\n"
                + canonical_request_hash;

        log.debug("string_to_sign : {}", string_to_sign);
        byte[] sigKey = generateAws4SigningKey(date);

        String signature = Hmac.calculateHMAC(string_to_sign, sigKey, Hmac.HMAC_SHA256);
        String authorization_header = AUTH_ALGORITHM + " " + "Credential=" + awsAuthConfig.getAccessKey() + "/"
                + credential_scope + ", " + "SignedHeaders=" + signed_headers + ", " + "Signature=" + signature;

        log.debug("authorization_header : {}", authorization_header);

        return new AuthenticationData(timestamp, date, authorization_header);
    }

    private String buildCanonicalQueryString() {
        String canonicalRequest = REQUEST_METHOD + "\n" +
                                 "/connect" + "\n" +
                "X-Amz-Algorithm=AWS4-HMAC-SHA256\n" +
                "&X-Amz-Credential=MYACCESKEY%2F"+ getDate(OffsetDateTime.now()) + "%2Fus-east-1%2Fchime%2Faws4_request\n" +
                "&X-Amz-Date=" + getTimeStamp(OffsetDateTime.now()) +"\n" +
                "&X-Amz-Expires=10\n" +
                "&X-Amz-SignedHeaders=host\n" +
                "&sessionId=" + UUID.randomUUID() +"\n" +
                "&userArn=" + "MYUSERARN";

        return canonicalRequest;
    }

}

Provided information

  • host: node001.ue1.ws-messaging.chime.aws
  • service name: chime
  • region: us-east-1

It makes the signature, and I'm trying use it via postman, but postman can't connect to the endpoint node001.ue1.ws-messaging.chime.aws/connect, just saying 'connect ETIMEDOUT 54.162.103.101:80'.

I'm new to Amazon, so it's kinda hard to me. Can you say where am I wrong?

Any help appreciated!


Solution

  • Wrote fully working code for signing URL for connecting to chime websocket. Hope this will helps somebody!