What is the Shopify state param? (And how to use it)

If you're building a Shopify app (and following their Authenticate with OAuth tutorial), you might be wondering: what exactly is the state param?

Shopify's documentation doesn't go into much detail on it, so in this article I'm going to break down what it is, what it's for, and how to handle it in your app.

Where does the state param come from?

You probably already know from the documentation that Shopify apps are authenticated with OAuth:

Public apps and custom apps must authenticate using the OAuth 2.0 specification in order to use Shopify’s API resources.

In simple terms, OAuth is a process that allows a third-party app to request access to data on a user's behalf. The user must approve this request, which usually includes a specific set of permissions that the app needs. After approval by the user, the app is then able to access the user's data in line with the permissions granted.

So, the first thing to know is that the state param is an OAuth concept, and is not unique to Shopify.

What is the state param for?

The state param is a security measure to prevent Cross-Site Request Forgery (CSRF) attacks.

What does this mean in the context of a Shopify app?

In a normal flow, the install process looks like this:

  1. A merchant makes a request to install your app.
  2. Your app (the client) redirects the merchant to the Shopify authorization prompt (the page at https://{shop}.myshopify.com/admin/oauth/authorize).
  3. If the merchant grants access, Shopify (the auth server) generates an authorization_code linked to that merchant’s account.
  4. The merchant is redirected back to your app using the redirect_uri, and your app exchanges the authorization_code for an access_token.
  5. Your app can then use this access_token to make requests to Shopify (the resource server) on behalf of the user.

In a compromised flow, it would instead look like this:

  1. An attacker begins the same install process (they make a request to install your app, and are redirected to the authorization prompt).
  2. Shopify generates an authorization_code as expected, which is of course linked to the attacker’s account this time.
  3. Instead of being redirected back to your app, the attacker interrupts this request, and does not complete the authorization process.
  4. The attacker then tricks the merchant into going to the redirect_uri instead (perhaps with a link in an enticing phishing email).
  5. When the merchant follows this link, the authorization_code (which is linked to the attacker's account) will be exchanged for an access_token (also linked to the attacker’s account)—all while the merchant is logged into Shopify.
  6. Now, your app is operating from the merchant's Shopify account session, but with the attacker's access_token. If your app writes any data to Shopify on behalf of the merchant, it will actually be writing it to the attacker’s Shopify account (which could allow them to steal information).

In Shopify's diagram of the OAuth flow, this is where the CSRF attack occurs:
Flowchart of Shopify's OAuth credential granting process, with CSRF attack

How does a state param prevent a CSRF attack?

The state parameter provides a way for your app to confirm that the outgoing request (to the authorization prompt) and incoming request (to the redirect_uri) are part of the same session. It is a token that your app creates, and only your app should be able to verify its integrity.

The state param is:

  • A string that uniquely identifies the session
  • Generated by your app (the client)
  • Passed with each step of the flow

These are the two requests that you need to confirm are part of the same session:
Flowchart of Shopify's OAuth credential granting process, highlighting requests apps need to verify using the state param

Let's think back to what would happen in the CSRF attack version of the flow above, but this time with a state param present.

This time, when the attacker tricks the merchant into going to the redirect_uri, the request will have the state parameter for the attacker’s session; not the merchant’s.

Your app will try to verify that this state param is linked to the merchant. However, it will see that the session making the request (the merchant’s) and the session linked to the state param (the attacker’s) don’t match, and so it can reject the request.

In a normal flow, the state parameter would match the one your app created, so you would know the flow hadn’t been intercepted. This is how your app can make sure the request is coming from the right session.

Aren't the other parameters already linking the request to the merchant's session?

If you're following Shopify's documentation, you've probably noticed the various other parameters in the incoming request, which might look something like this:

code=0907a61c0c8d55e99db179b68161bc00&hmac=700e2dadb827fcc8609e9d5ce208b2e9cdaab9df07390d2cbca10d7c328fc4bf&shop=some-shop.myshopify.com&timestamp=1337178173

What about shop, or hmac?

The presence of these could certainly make a CSRF attack harder, but not necessarily.

Shop

Apps only have to verify that the shop parameter is a valid hostname, which they can do by matching against a regex like this:

/(https|http)\:\/\/[a-zA-Z0-9][a-zA-Z0-9\-]*\.myshopify\.com[\/]?/

So this parameter could be the domain of the attacker's shop, and still pass validation.

Of course, the merchant could spot it in the malicious link and realize something is wrong, but that's far from guaranteed.

HMAC

The hmac param is just a hash of all the other params. Exactly what these params are could change over time, and this is determined by Shopify. So, the hmac is useful for verifying that the request came from Shopify.

But with no state param—and if all the other parameters remained unchanged—the hmac validation in your app would still pass. So this check alone wouldn't tell you whether the flow had been intercepted as part of a CSRF attack.

How should you generate the state param?

There are several ways to create the state param from your app code.

We know that it needs to:

  • Only be valid for a single auth/redirect cycle (hence it being a nonce)
  • Have a way for your app—and only your app—to verify its integrity

Let's take a look at three options.

1. Generate a random value and store it in your database

You can use a package like nonce to generate the value. Your app's backend should store it securely, linked somehow to the session that generated it.

You then compare that value with the one that comes back after authorization to check that they match, and then delete it so it can't be reused for any future sessions.

Once you've generated the value, you should store it in a cookie and attach it to the outgoing request using the Set-Cookie header. This is actually what Shopify’s own koa-shopify-auth package does.

The cookie should have the Secure (not transmitted via HTTP, only via HTTPS) and HttpOnly (can't be read by JavaScript) attributes. Cookies are sent with requests automatically, and as (with these attributes) they cannot be read/modified by JavaScript, it’s a secure way to store the value.

This method has the benefit that you don't need to store these temporary values in a database.

3. Generate a signed JWT

JSON Web Tokens are encoded tokens of arbitrary data that provide another stateless way to store the value. They can be signed and verified using a private key that only your app has access to.

I like this method because it doesn’t require a database or a cookie. You can also store other information in the token (e.g. you could encode the shop name instead of a generated nonce), but be careful not to store any sensitive data—if the payload is unencrypted, it can be read by anyone who gains access to the JWT.

So how is this secure?

If a token is signed, but not encrypted, anyone can read its contents. But if they don't have the private key used to sign it, they can't change the contents, or your app's attempt to verify it will fail because the signature will no longer match.

How to generate a signed JWT

To create the token, you first need a private key (this can be any string, but ideally it should be made up of at least 63 random generated alpha-numeric characters) that you store securely, so it's accessible only to your app through an environment variable, e.g. APP_SECRET.

Then, your app (using Node.js and jsonwebtoken in this example, but similar libraries exist for other languages) would create and verify the token like this:

const jwt = require('jsonwebtoken');

// Generate JWT
const createToken = (data, options = null) => {
try {
return jwt.sign(data, process.env.APP_SECRET, options);
} catch (e) {
throw new Error('Token creation failed');
}
};

// Verify JWT
const validateToken = (token) => {
if (!token) {
throw new Error('Token required');
}
try {
return jwt.verify(token, process.env.APP_SECRET);
} catch (e) {
throw new Error('Token validation failed');
}
};

Another nice feature of JWTs is that you can set them to expire after a specified amount of time. This adds an extra layer of security because, in a normal OAuth flow, you only need the token to be valid for a few seconds. An expired token is much less useful to anyone trying to use it for nefarious purposes.

Succeed in tech

Get actionable tips on coding, getting hired and working as a developer.

I won't spam you. Unsubscribe any time.