Skip to content
This repository has been archived by the owner on Apr 11, 2024. It is now read-only.

Commit

Permalink
Add adminApiAccessToken property to config
Browse files Browse the repository at this point in the history
Add `adminApiAccessToken` parameter to `config` for when
`isCustomStoreApp` is `true`.  This is then used for authenticating API
requests, instead of using `apiSecretKey` which was previously set to
the Admin API access token for custom store apps.

`apiSecretKey` must now be set to the custom store app's API secret key,
which is used to validate the HMAC of webhook events received from
Shopify for a custom store app.

Fixes #772, #800
  • Loading branch information
mkevinosullivan committed Apr 14, 2023
1 parent 284c185 commit 7dcecb6
Show file tree
Hide file tree
Showing 9 changed files with 57 additions and 15 deletions.
7 changes: 7 additions & 0 deletions .changeset/green-terms-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@shopify/shopify-api': major
---

⚠️ [Breaking] Add `adminApiAccessToken` parameter to `config` for when `isCustomStoreApp` is `true`, which is then used for API access. `apiSecretKey` must now be set to the custom store app's API secret key, which is used to validate the HMAC of webhook events received from Shopify for a custom store app. Fixes #772, #800

See [setting up a custom store app](https://github.com/shopify/shopify-api-js/blob/main/docs/guides/custom-store-app.md) for more details.
19 changes: 12 additions & 7 deletions docs/guides/custom-store-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,27 @@ A custom app is an app that you or a developer builds exclusively for your Shopi

A store-specific custom app does not use the OAuth process to authenticate - it uses the secrets established during the app creation and install process to access the API. As a result, there are no sessions to be retrieved from incoming requests and stored in a database, etc.

When initializing `shopifyApi` in a custom app, set the `isCustomStoreApp` configuration property to `true`, and set the `apiSecretKey` to the **Admin API access token** obtained during the installation process (step 2 in the [prerequisites](#prerequisites)).
When initializing `shopifyApi` in a custom app

- set the `isCustomStoreApp` configuration property to `true`
- set the `apiSecretKey` configuration property to the **API secret key** obtained during the installation process (step 2 in the [prerequisites](#prerequisites)).
- set the `adminApiAccessToken` configuration property to the **Admin API access token** obtained during the installation process (step 2 in the [prerequisites](#prerequisites)).

## Example

### Initialization

```js
import "@shopify/shopify-api/adapters/node";
import { shopifyApi, LATEST_API_VERSION, Session } from "@shopify/shopify-api";
import { restResources } from "@shopify/shopify-api/rest/admin/2023-01";
import { shopifyApi, ApiVersion, Session } from "@shopify/shopify-api";
import { restResources } from "@shopify/shopify-api/rest/admin/2023-04";

const shopify = shopifyApi({
apiKey: "App_API_key",
apiSecretKey: "Admin_API_Access_Token", // Note: this is the API access token, NOT the API Secret Key
apiVersion: LATEST_API_VERSION,
isCustomStoreApp: true, // this MUST be set to true (default is false)
apiSecretKey: "App_API_secret_key", // Note: this is the API Secret Key, NOT the API access token
apiVersion: ApiVersion.April23,
isCustomStoreApp: true, // this MUST be set to true (default is false)
adminApiAccessToken: "Admin_API_Access_Token", // Note: this is the API access token, NOT the API Secret Key
scopes: [],
isEmbeddedApp: false,
hostName: "my-shop.myshopify.com",
Expand All @@ -36,7 +41,7 @@ const shopify = shopifyApi({
});
```

> **Note** The `apiSecretKey` is **NOT** set to the API secret key but to the **Admin API access token**.
> **Note** In version 7 and earlier, the `apiSecretKey` was set to Admin API access token, but this prevented webhook hmac validation from working. To migrate from an app built with an API library version `7.0.0` or earlier, make sure to set the `apiSecretKey` to the API secret key (needed for webhook hmac validation) and set the `adminApiAccessToken` to the Admin API access token, required for client authentication.
### Making requests

Expand Down
18 changes: 18 additions & 0 deletions lib/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,31 @@ describe('Config object', () => {

it("ignores an empty 'scopes' when isCustomStoreApp is true", () => {
validParams.isCustomStoreApp = true;
validParams.adminApiAccessToken = 'token';
delete (validParams as any).scopes;

expect(() => validateConfig(validParams)).not.toThrow(
ShopifyErrors.ShopifyError,
);
});

it('requires adminApiAccessToken when isCustomStoreApp is true', () => {
const invalid: ConfigParams = {...validParams};
invalid.isCustomStoreApp = true;

try {
validateConfig(invalid);
fail(
'Initializing with isCustomStoreApp=true without adminApiAccessToken did not throw an exception',
);
} catch (error) {
expect(error).toBeInstanceOf(ShopifyErrors.ShopifyError);
expect(error.message).toContain(
'Missing values for: adminApiAccessToken',
);
}
});

it('can partially override logger settings', () => {
const configWithLogger = {...validParams};
configWithLogger.logger = {
Expand Down
1 change: 1 addition & 0 deletions lib/base-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ConfigParams<T extends ShopifyRestResources = any> {
apiVersion: ApiVersion;
isEmbeddedApp: boolean;
isCustomStoreApp?: boolean;
adminApiAccessToken?: string;
userAgentPrefix?: string;
privateAppStorefrontAccessToken?: string;
customShopDomains?: (RegExp | string)[];
Expand Down
4 changes: 3 additions & 1 deletion lib/clients/graphql/__tests__/graphql_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ describe('GraphQL client', () => {

it('adapts to private app requests', async () => {
shopify.config.isCustomStoreApp = true;
shopify.config.adminApiAccessToken = 'dangit-another-access-token';

const client = new shopify.clients.Graphql({session});
queueMockResponse(JSON.stringify(successResponse));
Expand All @@ -101,7 +102,8 @@ describe('GraphQL client', () => {
);

const customHeaders: {[key: string]: string} = {};
customHeaders[ShopifyHeader.AccessToken] = shopify.config.apiSecretKey;
customHeaders[ShopifyHeader.AccessToken] =
shopify.config.adminApiAccessToken;

expect({
method: 'POST',
Expand Down
2 changes: 1 addition & 1 deletion lib/clients/graphql/graphql_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class GraphqlClient {
protected getApiHeaders(): HeaderParams {
return {
[ShopifyHeader.AccessToken]: this.graphqlClass().config.isCustomStoreApp
? this.graphqlClass().config.apiSecretKey
? (this.graphqlClass().config.adminApiAccessToken as string)
: (this.session.accessToken as string),
};
}
Expand Down
4 changes: 3 additions & 1 deletion lib/clients/rest/__tests__/rest_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ describe('REST client', () => {

it('adapts to private app requests', async () => {
shopify.config.isCustomStoreApp = true;
shopify.config.adminApiAccessToken = 'test-admin-api-access-token';

const client = new shopify.clients.Rest({session});

Expand All @@ -358,7 +359,8 @@ describe('REST client', () => {
);

const customHeaders: {[key: string]: string} = {};
customHeaders[ShopifyHeader.AccessToken] = shopify.config.apiSecretKey;
customHeaders[ShopifyHeader.AccessToken] =
shopify.config.adminApiAccessToken;

expect({
method: 'GET',
Expand Down
2 changes: 1 addition & 1 deletion lib/clients/rest/rest_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class RestClient extends HttpClient {
): Promise<RestRequestReturn<T>> {
params.extraHeaders = {
[ShopifyHeader.AccessToken]: this.restClass().config.isCustomStoreApp
? this.restClass().config.apiSecretKey
? (this.restClass().config.adminApiAccessToken as string)
: (this.session.accessToken as string),
...params.extraHeaders,
};
Expand Down
15 changes: 11 additions & 4 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export function validateConfig(params: ConfigParams<any>): ConfigInterface {
if (!('isCustomStoreApp' in params) || !params.isCustomStoreApp) {
mandatory.push('scopes');
}
if ('isCustomStoreApp' in params && params.isCustomStoreApp) {
if (
!('adminApiAccessToken' in params) ||
params.adminApiAccessToken?.length === 0
) {
mandatory.push('adminApiAccessToken');
}
}
const missing: (keyof ConfigParams)[] = [];
mandatory.forEach((key) => {
if (!notEmpty(params[key])) {
Expand All @@ -48,6 +56,7 @@ export function validateConfig(params: ConfigParams<any>): ConfigInterface {
const {
hostScheme,
isCustomStoreApp,
adminApiAccessToken,
userAgentPrefix,
logger,
privateAppStorefrontAccessToken,
Expand All @@ -63,10 +72,8 @@ export function validateConfig(params: ConfigParams<any>): ConfigInterface {
? params.scopes
: new AuthScopes(params.scopes),
hostScheme: hostScheme ?? config.hostScheme,
isCustomStoreApp:
isCustomStoreApp === undefined
? config.isCustomStoreApp
: isCustomStoreApp,
isCustomStoreApp: isCustomStoreApp ?? config.isCustomStoreApp,
adminApiAccessToken: adminApiAccessToken ?? config.adminApiAccessToken,
userAgentPrefix: userAgentPrefix ?? config.userAgentPrefix,
logger: {...config.logger, ...(logger || {})},
privateAppStorefrontAccessToken:
Expand Down

0 comments on commit 7dcecb6

Please sign in to comment.