Shopify app with Rust: Part 5 – get OAuth access token

The last post (starting OAuth) tackled the first pass at the app installation API that serves Shopify’s OAuth flow. It also covered declaring the redirect URI, so Shopify sends the application server a web request, the server responds with a redirect to Shopify to authorize the installation, and then Shopify sends the user back to the application server.

A quick refresher on Shopify’s described OAuth flow:

1. The merchant makes a request to install the app.
2. The app redirects to Shopify to load the OAuth grant screen and requests the required scopes.
3. Shopify displays a prompt to the merchant to give authorization to the app, and prompts the merchant to log in if required.
4. The merchant consents to the scopes and is redirected to the redirect_uri.
5. The app makes an access token request to Shopify including the client_id, client_secret, and code.
6. Shopify returns the access token and requested scopes.
7. The app uses the access token to make requests to the Shopify API.
8. Shopify returns the requested data.

https://shopify.dev/apps/auth/oauth#the-oauth-flow

We have an authorized request knocking at the door of the application server (/callback API) but we’re not doing anything with it. In this post we’re going to tackle the body of:

// main.rs

#[rocket::get("/callback")]
pub fn callback() -> &'static str {
	"Now what..."
}

The goal here is to address: “The app makes an access token request to Shopify including the client_id, client_secret, and code.” If you take a close look at the output of the previous attempt to install an application, it seems we’ve missed quite a few parameters on the callback API:

Callback API invocation

Specifically on:

GET /callback?code=8a99f98c7deb52660f76219dee5ada50&hmac=6cb4c00bf66110d41cb4d7e26c59ade80d3b322de7ca0f28e54ac9b8dace4f4b&host=dG9ieS10ZXN0LXN0b3JlMi5teXNob3BpZnkuY29tL2FkbWlu&shop=toby-test-store2.myshopify.com&state=random-value&timestamp=1639244833 text/html

There are 6 query parameters!

  • code: The authorization code provided in the redirect, this is what we were after in the previous step and will use in the next step to get a token!
    • looks like: 8a99f98c7deb52660f76219dee5ada50
  • hmac: This is a signature we’ll use later to verify the request is unaltered from Shopify, it’s a bit ol’ TO-DO right now though
    • looks like: 6cb4c00bf66110d41cb4d7e26c59ade80d3b322de7ca0f28e54ac9b8dace4f4b
  • host: base64 encoded host name appended with the /admin URL segment
    • looks like dG9ieS10ZXN0LXN0b3JlMi5teXNob3BpZnkuY29tL2FkbWlu
    • decoded, looks like: toby-test-store2.myshopify.com/admin
  • shop: The plaintext name of the merchant’s shop (without trailing /admin)
    • looks like: toby-test-store2.myshopify.com
  • state: The nonce we provided in the installation redirect URL
    • looks like: random-value right now, but we’ll fix that eventually…
  • timestamp: seconds since epoch of the request, I think?
    • looks like: 1639244833

The timestamp, state, and hmac we can ignore entirely for the time being (this is a development app, we can’t ignore these in production). The host is useful, but not immediately, so let’s ignore it for the time being as well. The code and shop are exactly what we need for the next step. First, let’s make them accessible in our request handler:

// main.rs
#[rocket::get("/callback?<code>&<shop>")]
pub fn callback(code: &str, shop: &str) -> &'static str {
    println!("The code is: {}", code);
    println!("The shop is: {}", shop);
    "Now what..."
}

Now, what are we ultimately doing with these values? We have a code that represents the Shopify store’s agreement to install the application with the requested permissions. Now that we have that code, we’re going back to Shopify to say “Look, the shop agreed – can we access their data?”. This happens by exchanging the authorization code for an access token. The access token will let us make requests to Shopify that are authorized to access the granting store’s data. Before we dive into that too much, lets actually get an access token.

We need the application server to make a request. To do so, we’ll need an HTTP client. Looking at lib.rs, reqsounds godwest seems like it may be overkill but will likely do the job. We’re using virtually no features of the HTTP client library, and in this case it has to be synchronous regardless of whether the underlying code is sync or async.

Add reqwest to your Cargo.toml:

[package]
name = "template"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = "0.5.0-rc.1"
reqwest = { version = "0.11" }

Now let’s send an HTTP POST to https://{shop}/admin/oauth/access_token. In the spirit of just doing stuff, let’s give it a shot and see what happens. We’ll ignore Reqwest’s advice to share a client for the time being (another TO-DO?) and go for it. Lets also take the opportunity to change the return type to a Result just because it makes the code a bit easier to work with.

// main.rs
#[rocket::get("/callback?<code>&<shop>")]
pub async fn callback(code: &str, shop: &str) -> Result<&'static str, &'static str> {
    println!("The code is: {}", code);
    println!("The shop is: {}", shop);

    let access_token_uri = format!("https://{}/admin/oauth/access_token", shop);

    let client = reqwest::Client::new();
    let response = client
        .post(access_token_uri)
        .send()
        .await
        .map_err(|_| "Error sending request")?;

    println!("Response status: {}", response.status());

    let response_body = response
        .text()
        .await
        .map_err(|_| "Error getting response body as text")?;

    println!("{}", response_body);

    Ok("Now what...")
}

Some good things: the request itself succeeds in sending and a response comes back. Areas for improvement, the request yields an HTTP 400 Bad Request and the body is HTML. If you render the HTML, it looks something like this:

HTTP 400 Bad Request

As it says on Shopify, this is a JSON body with 3 properties:

  • client_id: this is our application’s API key, coming from the Shopify partner dashboard for the app
  • client_secret: this is our applications API secret key, also coming from the Shopify partner dashboard for the app
  • code: this is the code query parameter from the callback URL we have is populated

So, lets grab the values and try again! client_id and client_secret come from the Shopify partner dashboard:

This is where API key and API Secret key are coming from

The code is a query parameter in the original request. We’re crafting a JSON request, and basically as soon as anyone mentions JSON it’s time to add the Serde crate. While we’re there, we’ll enable the json feature of Reqwest so we can use Serde:

[package]
name = "template"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = "0.5.0-rc.1"
reqwest = { version = "0.11", features = ["json"] }
serde = "1"

One of the easiest ways I’ve found to craft a JSON request is to define a type for the request body and then let Serde serialize it to JSON. Adding the appropriate import and a stuct definition:

// main.rs
use rocket::{response::Redirect, routes, serde::Serialize};

#[derive(Serialize)]
struct AccessTokenRequest<'a> {
    client_id: &'a str,
    client_secret: &'a str,
    code: &'a str,
}

Now we can use that and amend our request so it contains the correct body:

#[rocket::get("/callback?<code>&<shop>")]
pub async fn callback(code: &str, shop: &str) -> Result<&'static str, &'static str> {
    let access_token_uri = format!("https://{}/admin/oauth/access_token", shop);

    let body = AccessTokenRequest {
        client_id: "801f137828580f71e33ad6b14f75e533",
        client_secret: "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",
        code,
    };

    let client = reqwest::Client::new();
    let response = client
        .post(access_token_uri)
        .json(&body)
        .send()
        .await
        .map_err(|_| "Error sending request")?;

    println!("Response status: {}", response.status());

    let response_body = response
        .text()
        .await
        .map_err(|_| "Error getting response body as text")?;

    println!("{}", response_body);

    Ok("Now what...")
}

Start up the application server, try installing or opening the app and:

Voilà! An access token!

We have an access token for the shop! As a side note, this is effectively a password that can be used to access a store’s data. Never reveal the value, and be diligent to not let it show up in screenshots in a blog post or similar.

As a quality of life improvement, let’s also deserialize the response body so it’s easier to use. As a quirk of the deserialization, we can only use owned types, so we need String instead of slices:

// main.rs
use rocket::{
    response::Redirect,
    routes,
    serde::{Deserialize, Serialize},
};

#[derive(Deserialize, Debug)]
struct AccessTokenResponse {
    access_token: String,
    scope: String,
}

#[rocket::get("/callback?<code>&<shop>")]
pub async fn callback(code: &str, shop: &str) -> Result<&'static str, &'static str> {
    ...
    let response_body: AccessTokenResponse = response
        .json()
        .await
        .map_err(|_| "Error getting response body as JSON")?;

    println!("{:?}", response_body);
    ...

Once again, referring back to the Shopify OAuth flow we’ve now tackled two more steps:

1. The merchant makes a request to install the app.
2. The app redirects to Shopify to load the OAuth grant screen and requests the required scopes.
3. Shopify displays a prompt to the merchant to give authorization to the app, and prompts the merchant to log in if required.
4. The merchant consents to the scopes and is redirected to the redirect_uri.
5. The app makes an access token request to Shopify including the client_id, client_secret, and code.
6. Shopify returns the access token and requested scopes.
7. The app uses the access token to make requests to the Shopify API.
8. Shopify returns the requested data.

https://shopify.dev/apps/auth/oauth#the-oauth-flow

Status

Where we started in this post: We had an app that “installable” and we could both request and be granted permissions for a store installing the app. Unfortunately we could not leverage those permissions to do anything

Where we ended with this post: We’ve exchanged the ephemeral authorization a store grants us for a permanent authorization. In practice this looks like using the provided code value for an OAuth access token. With this token, we can (theoretically at this point) make requests to Shopify for data that belongs to the associated store

Next post: Make an authorized request and obtain a shop’s data!

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 )

Facebook photo

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

Connecting to %s