Shopify HMAC Verification with Rust

Every request or redirect from Shopify to your app’s server includes an hmac parameter that can be used to verify the authenticity of the request from Shopify.

https://shopify.dev/apps/auth/oauth#verification

I want to build a Shopify application, and I want to use it as an excuse to do some (likely painful) web development with Rust. I would like to learn Rust, but haven’t had many interesting projects to do it with. One of the things I tripped over when building a Shopify application was the very explicit guidance to verify the HMAC value.

Going into this, I had no idea what the HMAC value was, nor what I was supposed to do with it. The Shopify documentation is pretty explicit, but I was going to need even more of a hand holding, because I really didn’t have a clue. According to the docs:

  1. Get to a point where Shopify is sending a request to your application, e.g by installing your app in a Shopify store
  2. remove the hmac query string field from the query string
    • this includes the key, the value, the equal sign, and the delimiting & if present
  3. convert the hmac value from a string into a
  4. Compute the value of the resultant query string (with hmac removed) with a HMAC-SHA256 hash function, where the key is your Shopify Application’s API secret key
  5. compare (securely) the computed value with the provided value, discarding or otherwise not trusting the request if the comparison fails

The code

Disclaimers:

  • I have only used this for HTTP GET requests.
  • I used Rocket, having not used it before today
  • I’m new to Rust, so keep that in mind
// Cargo.toml
[package]
name = "ready-for-production-now"
version = "0.1.0"
edition = "2018"

[dependencies]
rocket = "0.5.0-rc.1"
regex = "1"
hmac = "0.11"
sha2 = "0.9"
hex = "0.4"
lazy_static = "1.4"
// main.rs
use hmac::{Hmac, Mac, NewMac};
use lazy_static::lazy_static;
use regex::Regex;
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome, Request};
use sha2::Sha256;
use std::env;

type HmacSha256 = Hmac<Sha256>;

/// Type to integrate with Rocket's request guards
struct HmacParam();

/// The failure modes of HMAC verification, mostly for troubleshooting
#[derive(Debug)]
enum HmacParamError {
    NoQueryString,
    QueryStringButNoHmac,
    VerificationFailed,
}

lazy_static! {
    /// Note, this is not the best possible implementation. If the hmac value is the last in the query string, this
    /// will break due to a trailing '&' character. Maybe I'll fix that some day...
    static ref HMAC_REGEX: Regex = Regex::new(r"hmac=[^&]+&?").unwrap();
    /// I like to load in secrets with dotenv or similar, then consume them as environment variables
    static ref SHOPIFY_API_SECRET: String = env::var("SHOPIFY_API_SECRET").unwrap();
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for HmacParam {
    type Error = HmacParamError;

    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        // 1. Get the entire query string in one shot
        let query_string = match request.uri().query() {
            None => return Outcome::Failure((Status::Unauthorized, HmacParamError::NoQueryString)),
            Some(q) => q,
        };

        // 2. Grab the hmac query parameter (both key and value)
        let hmac = match query_string.segments().find(|&param| param.0 == "hmac") {
            None => {
                return Outcome::Failure((Status::Unauthorized, HmacParamError::QueryStringButNoHmac))
            }
            Some(h) => h,
        };

        // 3. Remove the hmac key and value from the query string, as they're excluded from the hash
        let query_string_without_hmac = HMAC_REGEX.replace(query_string.as_str(), "").into_owned();

        // 4. Create a HMAC SHA256 hash function using my app's API secret key as the hash key
        let mut hasher = HmacSha256::new_from_slice(&*SHOPIFY_API_SECRET.clone().into_bytes())
            .expect("HMAC key (Shopify API secret) was not appropriately set");

        // 5. Hash the remnants of the query string (with hmac removed
        hasher.update(&query_string_without_hmac.into_bytes());

        // 6. The value in the query string is the result of the hash function represented as hexadecimal, represented
        // as a string. I say that slighly weirdly because you can't just compare the value to the computed hash, the
        // string needs to be decoded as hexadecimal (e.g. the characters '02' are the numerical value 2). At that
        // point the resultant "number" can be compared with the output of the hash function
        let hmac = hex::decode(hmac.1).unwrap();

        // 7. Compare the Shopify-provided value to the freshly computed value
        match hasher.verify(&hmac) {
            Ok(_) => Outcome::Success(HmacParam {}),
            Err(_) => Outcome::Failure((Status::Unauthorized, HmacParamError::VerificationFailed)),
        }
    }
}

fn main() {
    // your code here
}
// example-usage.rs

// Notice hmac_guard: HmacParam! That's the request guard written above
#[allow(unused_variables)]
#[rocket::get("/install?<hmac>&<shop>&<timestamp>")]
fn auth_install(hmac: &str, shop: &str, timestamp: &str, _hmac_guard: HmacParam) -> Redirect {
	// this nonce should be kept to verify the next step
	let nonce = Uuid::new_v4(); 

	// I'm not being coy using constants
, you'll need to actually fill these out with values that make sense for your environment
	let redirect =format!("https://{shop}/admin/oauth/authorize?client_id={client_id}&scope={scopes}&redirect_uri={redirect_uri}&state={nonce}&grant_options[]={access_mode}", 
		shop = shop,
		client_id=*SHOPIFY_API_KEY,
		scopes=SCOPES,
		redirect_uri = *SHOPIFY_REDIRECT_ADDRESS,
		nonce=nonce,
		access_mode=ACCESS_MODE);

	Redirect::to(redirect)
}

What HMAC verification and why bother?

So, why even bother with this? I have not published an application to the Shopify marketplace, so what follows is a bit of speculation. From what I can tell with an in-development app, Shopify does not “test” to ensure you’re validating the HMAC. The Shopify app review guidelines also do not explicitly call out a requirement to do so lest you fail review, but that may be a thing once I get there.

From what I can tell, the intention of the HMAC verification is to ensure the integrity of the request both in its original authorship and in transit between Shopify and your application. As your API’s secret key is used as input to the hashing function, this guarantees that it is Shopify (or someone with access to your API’s secret key, at least) that has authored the request.

At the same time, any request body and all the headers (save the hmac value itself) are inputs to the hash, which guarantees that the content has not been modified/appended to in transit.

As someone with very little experience in this area, it’s unclear how frequently an HMAC verification check fails, and how often that would be because of foul play vs. software bugs, so I have no sense of how useful it is overall.

Edit: Originally I had this code returning Status::BadRequest, Shopify validates that the code returns a 401 Unauthorized (rather than a 400 Bad Request).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s