# How To / Authenticate

# How-To: Authenticate your application with Shopfront

> We would highly suggest reading through the [general authentication information](/documentation/General/Authentication) 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](/applications) (don't have a user account, [sign up now](/register) - 
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](/imgs/developer/howto/authenticate/new-button.JPG)

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**

```javascript
// 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);
});
```

**Authorizing Your Application**

```php
<?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](/imgs/developer/howto/authenticate/login-screen.png)

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](/imgs/developer/howto/authenticate/approval-screen.png)

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**

```javascript
// 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
});
```

**Authorization Result**

```php
// 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**

```javascript
// 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}`);
        });
};
```

**Querying Shopfront**

```php
// 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**

```javascript
// 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);
        });
};
```

**Refreshing Tokens**

```php
// 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](/documentation/General/GraphQL%20Getting%20Started)).

Send a GraphQL request to the [`revokeIntegration`](https://developer.onshopfront.com/documentation/Mutations/Integrations#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**

```javascript
// 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");
        });
};
```

**Revoking Your Application**

```php
// 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
}
```