How-To: Direct Authentication

We would suggest reading the general authentication information and the standard OAuth guide before reading this document.

In order to become a member of Shopfront's Partner Program, you'll need to support a concept we call "direct authentication", which is used when your integration appears in the Shopfront marketplace and allows users to be redirected to your site for them to sign up or to quickly complete authorisation.

Getting Started

When a user clicks the "Integrate" button for your app inside Shopfront (Menu > Setup > Integrations), Shopfront performs a GET redirect to a URL you provide during the partner program sign-up process.

This allows you to:

  1. Identify which Shopfront vendor initiated the flow.
  2. Run your signup or authorisation process.
  3. (optionally) Redirect the user back to Shopfront when complete.

Handling the Marketplace Redirect

Shopfront will redirect the user to your integration URL using the following format:

https://[your-url]?vendor=[subdomain]&setup=shopfront&redirect=[url-to-redirect-once-done]

The redirect parameter will be URL-encoded and should be treated as an opaque URL. You can store it and use it later to return the user to Shopfront.

You should expect these query parameters:

  • vendor: The vendor subdomain initiating the integration.
  • setup: Will equal shopfront for this flow.
  • redirect: The Shopfront URL to send the user back to after your flow is complete.

In the future, additional query parameters may be added.

Handling the Marketplace Redirect
// This is a server-side example for handling the marketplace redirect.

globalThis.shopfrontDirectAuth = {
    states: {},
    redirects: {},
};

const createState = (vendor, redirectUrl) => {
    const state = btoa(`${vendor}:${Date.now()}`);
    globalThis.shopfrontDirectAuth.states[state] = vendor;
    globalThis.shopfrontDirectAuth.redirects[state] = redirectUrl;

    // Clean up after 10 minutes.
    setTimeout(() => {
        delete globalThis.shopfrontDirectAuth.states[state];
        delete globalThis.shopfrontDirectAuth.redirects[state];
    }, 10 * 60 * 1000);

    return state;
};

routes.get("/shopfront/direct-auth", (request, response) => {
    const vendor = request.queryString.get("vendor");
    const redirectUrl = request.queryString.get("redirect");

    const state = createState(vendor, redirectUrl);

    // Instead of redirecting straight back to Shopfront here, you may want to show a marketing page or have them
    // authenticate with your application first
    const url = new URL(`https://${vendor}.onshopfront.com/oauth/authorize`);
    url.searchParams.append("client_id", process.env.SHOPFRONT_CLIENT_ID);
    url.searchParams.append("redirect_uri", process.env.SHOPFRONT_REDIRECT_URI);
    url.searchParams.append("response_type", "code");
    url.searchParams.append("state", state);
    url.searchParams.append("scope", "modify_integrations create_webhooks");

    return response.redirect(url);
});
<?php

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

function storeDirectAuthState(string $state, string $vendor, string $redirectUrl): void {
    $storagePath = "/tmp/shopfrontDirectAuth/" . $state;
    if(!is_dir(dirname($storagePath))) {
        mkdir(dirname($storagePath), 0777, true);
    }

    file_put_contents($storagePath, json_encode([
        "vendor" => $vendor,
        "redirect" => $redirectUrl,
        "expiry" => time() + (10 * 60),
    ]));
}

function createDirectAuthState(string $vendor, string $redirectUrl): string {
    $state = base64_encode($vendor . time());
    storeDirectAuthState($state, $vendor, $redirectUrl);

    return $state;
}

Route::get("/shopfront/direct-auth", function(ServerRequestInterface $request): ResponseInterface {
    $parameters = $request->getQueryParams();
    $vendor = $parameters["vendor"];
    $redirectUrl = $parameters["redirect"];

    $state = createDirectAuthState($vendor, $redirectUrl);

    // Instead of redirecting straight back to Shopfront here, you may want to show a marketing page or have them
    // authenticate with your application first
    $urlParameters = http_build_query([
        "client_id" => env("SHOPFRONT_CLIENT_ID"),
        "redirect_uri" => env("SHOPFRONT_REDIRECT_URI"),
        "response_type" => "code",
        "state" => $state,
        "scope" => "modify_integrations create_webhooks",
    ]);

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

    return Response::redirect($url);
});

Authorising the User

Once the user lands on your site, you can prompt them to sign up or log in and then begin the standard OAuth flow with Shopfront (see the Authenticate guide).

We recommend using the vendor parameter to direct the user to the vendor-specific OAuth endpoint:

https://[vendor].onshopfront.com/oauth/authorize

When going through the partner program onboarding process, you'll need to demo to us how this interaction works and how a user flows from Shopfront through to your app and back to Shopfront for authentication. Care should be taken with this flow as it's the most likely way users will first interact with your application from Shopfront.

Returning to Shopfront

After you exchange the OAuth code for an access token, you may wish to redirect the user to the redirect URL provided by Shopfront at the start of the flow. This will allow the user to continue where they left off before leaving Shopfront to integrate with your application.

If the user cancels or fails authorisation on your side, you can still return them to the redirect URL so they can resume where they left off.

Returning to Shopfront
// This is a server-side example for handling the OAuth callback.

routes.get("/shopfront/oauth/callback", async (request, response) => {
    const state = request.queryString.get("state");
    const code = request.queryString.get("code");

    const redirectUrl = globalThis.shopfrontDirectAuth.redirects[state];
    const vendor = globalThis.shopfrontDirectAuth.states[state];

    const tokenResponse = await fetch("https://onshopfront.com/oauth/token", {
        method: "POST",
        headers: {
            "Content-Type": "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,
            code,
            grant_type: "authorization_code",
        }),
    });

    const tokenBody = await tokenResponse.json();
    await storeShopfrontTokens(vendor, tokenBody);

    return response.redirect(redirectUrl);
});
<?php

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

function readDirectAuthState(string $state): ?object {
    $storagePath = "/tmp/shopfrontDirectAuth/" . $state;
    if(!file_exists($storagePath)) {
        return null;
    }

    $data = json_decode(file_get_contents($storagePath));
    if($data->expiry <= time()) {
        return null;
    }

    return $data;
}

Route::get("/shopfront/oauth/callback", function(ServerRequestInterface $request): ResponseInterface {
    $parameters = $request->getQueryParams();
    $state = $parameters["state"];
    $code = $parameters["code"];

    $stored = readDirectAuthState($state);
    if(!$stored) {
        return Response::json(["error" => "invalid_state"], 400);
    }

    $tokenBody = httpPostJson("https://onshopfront.com/oauth/token", [
        "client_id" => env("SHOPFRONT_CLIENT_ID"),
        "client_secret" => env("SHOPFRONT_CLIENT_SECRET"),
        "redirect_uri" => env("SHOPFRONT_REDIRECT_URI"),
        "code" => $code,
        "grant_type" => "authorization_code",
    ]);

    storeShopfrontTokens($stored->vendor, $tokenBody);

    return Response::redirect($stored->redirect);
});