How-To: Authenticate your application with Shopfront

We would highly suggest reading through the general authentication information before reading this document.

Shopfront uses OAuth 2.0 for authentication, this guide will walk you through how to authenticate your application.

Getting Started

Before you start developing your integration with Shopfront, you'll have to create an "application" on Shopfront. You can do this from your user account (don't have a user account, sign up now - it's free!), you can have an unlimited amount of applications created in your account.

Once you've opened your user account, simply create a new application by pressing the + NEW button.

+ NEW Button

You'll then be presented with a dialog box that requests a Name and a Redirect URI. The Name field is simply the name of your application, this is visible to user's so make sure they know it's your application. The Redirect URI is the address to redirect users to once they've approved the application on Shopfront, typically this is just a normal web address that you control.

Once you've filled in the details, press the Confirm button.

Congratulations! You've now created your application. You'll now need to get two pieces of information, the client_id and client_secret. These can be retrieved by pressing the Details link next to the application.

Once the details are obtained, you're ready to start writing code.

Authorizing Your Application

Authorization is a multi-step process, in this tutorial, we're going to presume you have a website that people visit which they can then click on a link / button which will then perform the authorization process.

So, let's pretend that the user whose account you want to authorise has clicked the button, you then need to redirect them to https://onshopfront.com/oauth/authorize or https://[vendor].onshopfront.com/oauth/authorize (preferred). When redirecting you need to pass the following as GET parameters:

  • client_id: Your client_id from before,
  • redirect_uri: The URL that you provided when creating the application, the user will be redirected back here
  • response_type: This must equal code,
  • state: This is a unique "state" for the request that will be passed back to you (useful for preventing a number of different attacks),
  • scope: The required scopes (permissions) for future calls (space separated).
Authorizing Your Application
// This is a server side example, but performing the redirect on the client side is similar
// We presume you have sent a request which contains the Vendor's subdomain

// Setup our state tracking
// Normally you would not store this in a global variable and instead
// store either in persistent storage (with an expiry) or in something like
// Redis which supports expiry naturally
globalThis.shopfrontStates = {};

// Create a state tracking function
const getStateForVendor = (vendor, generateWhenMissing = false) => {
    if(typeof globalThis.shopfrontStates[vendor] === "undefined") {
        if(generateWhenMissing) {
            // We would suggest using something like a UUID which is not guessable
            // as your state (currently this state is guessable, so it is not secure)
            const state = atob(`${vendor}${Date.now()}`);
            globalThis.shopfrontStates[vendor] = {
                state,
                timeout: setTimeout(() => {
                    delete globalThis.shopfrontStates[vendor];
                }, 5 * 1000 * 60),
            };
        } else {
            return false;
        }
    }

    return globalThis.shopfrontStates[vendor].state;
};

// Handle a request, you'll have to change this to how your requests are handled
// We're making up a request and response object
routes.get("/shopfront-redirector", (request) => {
    // Get the Vendor's subdomain
    const vendor = request.queryString.get("vendor");

    // Assemble the URL
    const url = new URL(`https://${vendor}.onshopfront.com/oauth/authorize`);
    url.searchParams.append("client_id", process.env.SHOPFRONT_CLIENT_ID); // Your Client ID
    url.searchParams.append("redirect_uri", encodeURIComponent(process.env.SHOPFRONT_REDIRECT_URI)); // Your Redirect URI
    url.searchParams.append("response_type", "code");
    url.searchParams.append("state", getStateForVendor(vendor, true));
    url.searchParams.append("scopes", "modify_integrations create_webhooks sell see_products");

    // Redirect the user
    return response.redirect(url);
});
<?php

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

// Setup our state tracking
// We would suggest storing the state in either some sort of persistent
// storage (with an expiry) or something like Redis which supports
// expiry naturally
// We're going to store the state on the filesystem but this can be quite slow
// depending upon how many requests you're serving
function getStateForVendor(string $vendor, bool $generateWhenMissing = false): ?string {
    $storagePath = "/tmp/shopfrontStates/" . $vendor;

    if(file_exists($storagePath)) {
        $data = json_decode(file_get_contents($storagePath));

        if($data->expiry > time()) {
            return $data->state;
        }
    }

    if($generateWhenMissing) {
        // We would suggest using something like a UUID which is not guessable
        // as your state (currently this state is guessable, so it is not secure)
        $state = base64_encode($vendor . time());

        file_put_contents($storagePath, json_encode([
            "state" => $state,
            "expiry" => time() + (5 * 60),
        ]));

        return $state;
    }

    return null;
}

// Handle a request, you'll have to change this to how your requests are handled
// We're using a PSR-7 request and response object
Route::get("/shopfront-redirector", function(ServerRequestInterface $request): ResponseInterface {
    // Get the Vendor's subdomain
    $parameters = $request->getQueryParams();
    $vendor     = $parameters["vendor"];

    // Assemble the URL
    $urlParameters = http_build_query([
        "client_id"     => env("SHOPFRONT_CLIENT_ID"),
        "redirect_uri"  => env("SHOPFRONT_REDIRECT_URI"),
        "response_type" => "code",
        "state"         => getStateForVendor($vendor, true),
    ]);

    $url = "https://" . $vendor . ".onshopfront.com/oauth/authorize?" . $urlParameters;

    // Redirect the user
    return Response::redirect($url); // Made-up response object that would implement ResponseInterface
});

The user may be asked to log in (depending upon how recently they last logged in):

Login Screen

Once the user has logged in they will be presented with the application approval screen and the choice to either approve your application or decline the application:

Approval Screen

Once the user has made a decision, they will be redirected to your redirect_uri with the result of their decision.

The request will always contain both a state parameter which matches the original state provided and a vendor parameter which can be used to determine which store the user was redirect from.

If the user declined the application, you'll receive an error parameter with the reason the integration was declined.

If the user approves the integration, you'll receive a code parameter which can be used to obtain an access token.

Authorization Result
// Continues from previous code

// We're creating a way to delete the state before it has timed out
// to prevent the same URL from being used twice
const deleteState = (vendor) => {
    if(typeof globalThis.shopfrontStates[vendor] === "undefined") {
        return;
    }

    clearTimeout(globalThis.shopfrontStates[vendor].timeout);
    delete globalThis.shopfrontStates[vendor];
};

// Create a way to store the access token that we retrieve later.
// We're just going to put it into the global namespace, but
// you should store it persistently in a database (or something
// similar)
globalThis.shopfrontTokens = {};
const storeAccessToken = (vendor, tokens) => {
    globalThis.shopfrontTokens[vendor] = tokens;
};

// Handle the redirect request
routes.get("/shopfront-redirect", async (request) => {
    const vendor       = request.queryString.get("vendor");
    const state        = request.queryString.get("state");
    const currentState = getStateForVendor(vendor);

    // We're going to delete the state as we're not going to use it again
    deleteState(vendor);

    // Validate the state
    if(currentState !== state) {
        throw new Error("Invalid state provided");
    }

    // Check if there was an error
    if(request.queryString.has("error")) {
        const error = request.queryString.get("error");

        throw new Error(`Received error from Shopfront: ${error}`);
    }

    // We've been approved for integration, obtain an access token
    const code = request.queryString.get("code");

    // We're just going to use fetch here to simplify the code
    const response = await fetch("https://onshopfront.com/oauth/token", {
        method : "POST",
        headers: {
            "Content-Type": "application/json",
            "Accept"      : "application/json",
        },
        body: JSON.stringify({
            client_id    : process.env.SHOPFRONT_CLIENT_ID,
            client_secret: process.env.SHOPFRONT_CLIENT_SECRET,
            redirect_uri : process.env.SHOPFRONT_REDIRECT_URI,
            grant_type   : "authorization_code",
            code,
        }),
    });

    if(response.status !== 200) {
        // We received an error response from Shopfront
        throw new Error("Could not swap authorization code for an access token");
    }

    // We've successfully obtained an access token
    const body = await response.json();

    // Store the body
    storeAccessToken(vendor, body);

    // You can now query Shopfront!
    queryShopfront(vendor); // This is specified in the next part
});
// Continues from previous code

// We're creating a way to delete the state before it has timed out
// to prevent the same URL from being used twice
function deleteState(string $vendor) {
    $storagePath = "/tmp/shopfrontStates/" . $vendor;

    if(!file_exists($storagePath)) {
        return;
    }

    unlink($storagePath);
}

// Create a way to store the access token that we retrieve later.
// We're just going to put it onto the filesystem, but you should
// store it persistently in a database (or something similar)
function storeAccessToken(string $vendor, object $tokens) {
    $storagePath = "/tmp/shopfrontTokens/" . $vendor;

    file_put_contents($storagePath, json_encode($tokens));
}

// Handle the redirect request
Route::get("/shopfront-redirect", function(ServerRequestInterface $request): ResponseInterface {
    // Get the current vendor and state
    $parameters   = $request->getQueryParams();
    $vendor       = $parameters["vendor"];
    $state        = $parameters["state"];
    $currentState = getStateForVendor($vendor);

    // We're going to delete the state as we're not going to use it again
    deleteState($vendor);

    // Ensure the state is valid
    if($currentState !== $state) {
        throw new Exception("Invalid state provided");
    }

    // Check if there was an error
    if(isset($parameters["error"])) {
        $error = $parameters["error"];
        throw new Exception("Received error from Shopfront: " . $error);
    }

    // We've been approved for integration, obtain an access token
    if(!isset($parameters["code"])) {
        // There is an issue with some PHP configurations that have the
        // subhosin patch not being able to read the code, so we'll manually
        // parse it from the URL
        $url       = parse_url($_SERVER["REQUEST_URI"]);
        $urlParams = explode("&", $url["query"]);

        for($i = 0, $c = count($urlParams); $i < $c; $i++) {
            if(substr($urlParams[$i], 0, 5) === "code=") {
                $parameters["code"] = substr($urlParams[$i], 5);
                break;
            }
        }
    }

    $code = $parameters["code"];

    // Swap the code for an access token, we're going to use cURL
    $ch = curl_init("https://onshopfront.com/oauth/token");

    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Content-Type" => "application/json",
        "Accept"       => "application/json",
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
        "client_id"     => env("SHOPFRONT_CLIENT_ID"),
        "client_secret" => env("SHOPFRONT_CLIENT_SECRET"),
        "redirect_uri"  => env("SHOPFRONT_REDIRECT_URI"),
        "grant_type"    => "authorization_code",
        "code"          => $code,
    ]));

    $response    = curl_exec($ch);
    $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    curl_close($ch);

    if($status_code !== 200) {
        throw new Exception("Could not swap authorization code for an access token");
    }

    // Store the access token
    storeAccessToken($vendor, json_decode($response));

    // You can now query Shopfront!
    queryShopfront($vendor); // This is specified in the next part
});

Presuming the user has approved your integration you can then request an access token from Shopfront. You can do this by sending a POST request to https://onshopfront.com/oauth/token with the following fields in a JSON body (don't redirect the user):

  • client_id: Your client_id from earlier when you created your application,
  • client_secret: Your client_secret from earlier when you created your application,
  • redirect_uri: The URL that you provided when creating the application,
  • code: The code that was provided in the previous step,
  • grant_type: This must equal authorization_code

If all of the information is correct and not too much time has elapsed from the code being generated (they expire), Shopfront will return a JSON response (with a 200 status code) that contains the following information:

  • token_type: The type of token (this will be set to Bearer),
  • access_token: The access token you can use to retrieve data in Shopfront with,
  • refresh_token: The token to use to refresh the access token with (this should be stored securely, more later),
  • expires_in: The number of seconds until the access_token expires

Once you've received this response you're ready to start querying Shopfront.

Querying Shopfront

Now that you have your bearer token you can start sending requests to any of Shopfront's APIs by supplying the access_token in the Authorization header, you can see a basic example that retrieves the current user's information on the right hand side (if you're on a desktop).

Querying Shopfront
// Continues from previous
const getTokens = (vendor) => {
    if(typeof globalThis.shopfrontTokens[vendor] === "undefined") {
        throw new Error(`Tokens not found for ${vendor}`);
    }

    return globalThis.shopfrontTokens[vendor];
};

const queryShopfront = (vendor) => {
    const tokens = getTokens(vendor);

    return fetch(`https://${vendor}.onshopfront.com/api/v2/graphql`, {
        method : "POST",
        headers: {
            "Content-Type" : "application/json",
            "Accept"       : "application/json",
            "Authorization": `Bearer ${tokens.access_token}`,
        },
        body: JSON.stringify({
            query: `{
                viewer {
                    name
                }
            }`
        }),
    })
        .then(async response => {
            if(response.status !== 200) {
                if(response.status === 429) {
                    // The rate limiter has been hit,
                    // you'll probably want to wait and retry
                    throw new Error("Hit rate limiter");
                } else if(response.status === 401) {
                    let error  = "";
                    const body = await response.json();
                    if(typeof body.data === "string") {
                        error = body.data;
                    } else if(typeof body.data === "object") {
                        error = body.data.error;
                    }

                    if(error === "invalid_token" || error === "Unauthenticated.") {
                        // Refresh the token (next section)
                        return refreshToken(vendor);
                    }
                }

                throw await response.json();
            }

            return response.json();
        })
        .then(body => {
            if(typeof body.errors !== "undefined") {
                throw body.errors;
            }

            console.log(`Successfully integrated as ${body.data.viewer.name}`);
        });
};
// Continues from previous
function getTokens(string $vendor): object {
    $storagePath = "/tmp/shopfrontTokens/" . $vendor;

    if(!file_exists($storagePath)) {
        throw new Exception("Tokens not found for " . $vendor);
    }

    return json_decode(file_get_contents($storagePath));
}

function queryShopfront(string $vendor) {
    $tokens = getTokens($vendor);

    $ch = curl_init("https://" . $vendor . ".onshopfront.com/api/v2/graphql");

    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Content-Type"  => "application/json",
        "Accept"        => "application/json",
        "Authorization" => "Bearer " . $tokens->access_token,
    ]);

    $query = <<<GRAPHQL
    {
        viewer {
            name
        }
    }
GRAPHQL;

    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
        "query" => $query,
    ]));

    $response    = curl_exec($ch);
    $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    curl_close($ch);

    try {
        $response = json_decode($response);
    } catch(\Exception $e) {
        // Couldn't decode, this may not be an actual error
    }

    if($status_code !== 200 || isset($response->errors)) {
        if($status_code === 429) {
            // The rate limiter has been hit,
            // you'll probably want to wait and retry
            throw new Exception("Hit rate limiter");
        } else if($status_code === 401 && is_object($response)) {
            $error = "";
            if(is_string($response->data)) {
                $error = $response->data;
            } else if(is_object($response->data)) {
                $error = $response->data->error;
            }

            if($error === "invalid_token" || $error === "Unauthenticated.") {
                // Refresh the token (next section)
                return refreshToken($vendor);
            }
        }

        throw new Exception("Invalid response returned from Shopfront");
    }

    // Query success! Can access the name through $response->data->viewer->name
}

Refreshing Tokens

After an amount of time has elapsed, your access_token will expire (normally about two weeks after being issued), and you'll need to request a new one (other events can cause your access_token to expire as well, not just time).

Refresh tokens also expire (normally in about 30 days after being issued)! Make sure you've refreshed your token before this time elapses otherwise you'll need to reintegrate your application again.

Refreshing an access token is a fairly simple process, we'd suggest you build a way of refreshing tokens into your queries rather than waiting for the time to elapse as it's much more future-proof and allows you to account for other ways the token could expire.

When you've detected that your access_token needs to be refreshed, send a POST request to https://onshopfront.com/oauth/token with the following fields in a JSON body:

  • client_id: Your client_id from earlier when you created your application,
  • client_secret: Your client_secret from earlier when you created your application,
  • refresh_token: The current refresh_token,
  • grant_type: This must equal refresh_token

Shopfront wil then respond with the following JSON response (status code 200):

  • access_token: The access token you can use to retrieve data in Shopfront with,
  • refresh_token: The token to use to refresh the access token with (this should replace the current refresh_token),
  • expires_in: The number of seconds until the access_token expires
Refreshing Tokens
// Continues from previous
const refreshToken = (vendor) => {
    // Get the tokens
    const oldTokens = getTokens(vendor);

    // Send the request to Shopfront to refresh our access token
    return fetch("https://onshopfront.com/oauth/token", {
        method : "POST",
        headers: {
            "Content-Type": "application/json",
            "Accept"      : "application/json",
        },
        body: JSON.stringify({
            client_id    : process.env.SHOPFRONT_CLIENT_ID,
            client_secret: process.env.SHOPFRONT_CLIENT_SECRET,
            refresh_token: oldTokens.refresh_token,
            grant_type   : "refresh_token",
        }),
    })
        .then(response => {
            if(response.status !== 200) {
                // Invalid refresh token, you'll likely have to reintegrate
                throw new Error("Unable to refresh the access token");
            }

            return response.json();
        })
        .then(body => {
            // Refresh token has been updated, store all of the details
            storeAccessToken(vendor, body);
        });
};
// Continues from previous
function refreshToken(string $vendor) {
    // Get the tokens
    $oldTokens = getTokens($vendor);

    // Send the request to Shopfront and refresh our access token
    $ch = curl_init("https://onshopfront.com/oauth/token");

    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Content-Type" => "application/json",
        "Accept"       => "application/json",
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
        "client_id"     => env("SHOPFRONT_CLIENT_ID"),
        "client_secret" => env("SHOPFRONT_CLIENT_SECRET"),
        "grant_type"    => "refresh_token",
        "refresh_token" => $oldTokens->refresh_token,
    ]));

    $response    = curl_exec($ch);
    $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    curl_close($ch);

    if($status_code !== 200) {
        // Invalid refresh token, you'll likely have to reintegrate
        throw new Exception("Unable to refresh the access token");
    }

    // Refresh token has been updated, store all of the details
    storeAccessToken($vendor, json_decode($response));
}

Revoking Your Application

If for some reason you need to revoke your application, you can do so through a GraphQL query (see the Getting Started with GraphQL information to learn about GraphQL).

Send a GraphQL request to the revokeIntegration mutation and all future requests will be prevented to Shopfront causing the Vendor to have to reintegrate your application (it will also prevent any Webhooks from sending and any embedded applications from loading).

Revoking Your Application
// Continues from previous
const revokeShopfront = (vendor) => {
    const tokens = getTokens(vendor);
    return fetch(`https://${vendor}.onshopfront.com/api/v2/graphql`, {
        method : "POST",
        headers: {
            "Content-Type" : "application/json",
            "Accept"       : "application/json",
            "Authorization": `Bearer ${tokens.access_token}`,
        },
        body: JSON.stringify({
            query: `mutation revokeIntegration()`
        }),
    })
        .then(response => {
            if(response.status !== 200) {
                // Handle error
                throw new Error("Invalid response from Shopfront");
            }

            return response.json();
        })
        .then(body => {
            if(typeof body.errors !== "undefined") {
                throw body.errors;
            }

            console.log("Integration successfully revoked");
        });
};
// Continues from previous
function revokeShopfront(string $vendor) {
    $tokens = getTokens($vendor);

    $ch = curl_init("https://" . $vendor . ".onshopfront.com/api/v2/graphql");

    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Content-Type"  => "application/json",
        "Accept"        => "application/json",
        "Authorization" => "Bearer " . $tokens->access_token,
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
        "query" => "mutation revokeIntegration()",
    ]));

    $response    = curl_exec($ch);
    $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    curl_close($ch);

    $response = json_decode($response);

    if($status_code !== 200 || isset($response->errors)) {
        // Handle error
        throw new Exception("Invalid response from Shopfront");
    }

    // Integration has been successfully removed
}