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.
state
param come from?
Where does the 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.
state
param for?
What is the 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:
- A merchant makes a request to install your app.
- Your app (the client) redirects the merchant to the Shopify authorization prompt (the page at
https://{shop}.myshopify.com/admin/oauth/authorize
). - If the merchant grants access, Shopify (the auth server) generates an
authorization_code
linked to that merchant’s account. - The merchant is redirected back to your app using the
redirect_uri
, and your app exchanges theauthorization_code
for anaccess_token
. - 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:
- An attacker begins the same install process (they make a request to install your app, and are redirected to the authorization prompt).
- Shopify generates an
authorization_code
as expected, which is of course linked to the attacker’s account this time. - Instead of being redirected back to your app, the attacker interrupts this request, and does not complete the authorization process.
- The attacker then tricks the merchant into going to the
redirect_uri
instead (perhaps with a link in an enticing phishing email). - When the merchant follows this link, the
authorization_code
(which is linked to the attacker's account) will be exchanged for anaccess_token
(also linked to the attacker’s account)—all while the merchant is logged into Shopify. - 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:
state
param prevent a CSRF attack?
How does a 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:
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×tamp=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.
state
param?
How should you generate the 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.
2. Generate a random value and set it as a cookie
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.