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

Commit

Permalink
Adding documentation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
paulomarg committed Jan 30, 2024
1 parent fc67b1b commit c883867
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 3 deletions.
9 changes: 9 additions & 0 deletions packages/shopify-api/docs/reference/flow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# shopify.flow

This object contains functions used to authenticate Flow extension requests coming from Shopify.

| Property | Description |
| ------------------------- | ------------------------------------------------------------------ |
| [validate](./validate.md) | Redirect the user to Shopify to request authorization for the app. |

[Back to shopifyApi](../shopifyApi.md)
64 changes: 64 additions & 0 deletions packages/shopify-api/docs/reference/flow/validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# shopify.flow.validate

Takes in a raw request and the raw body for that request, and validates that it's a legitimate Shopify Flow extension request.

## Example

```ts
app.post('/flow', express.text({type: '*/*'}), async (req, res) => {
const {valid, topic, domain} = await shopify.flow.validate({
rawBody: req.body, // is a string
rawRequest: req,
rawResponse: res,
});

if (!result.valid) {
console.log(`Received invalid Flow extension request: ${result.reason}`);
res.send(400);
}

res.send(200);
});
```

## Parameters

Receives an object containing:

### rawBody

`string` | :exclamation: required

The raw body of the request received by the app.

### rawRequest

`AdapterRequest` | :exclamation: required

The HTTP Request object used by your runtime.

### rawResponse

`AdapterResponse` | :exclamation: required for Node.js

The HTTP Response object used by your runtime. Required for Node.js.

## Return

Returns an object containing:

### valid

`boolean`

Whether the request is a valid Flow extension request from Shopify.

### If valid is `false`:

#### reason

`FlowValidationErrorReason`

The reason why the check was considered invalid.

[Back to shopify.flow](./README.md)
1 change: 1 addition & 0 deletions packages/shopify-api/docs/reference/shopifyApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ This function returns an object containing the following properties:
| [session](./session/README.md) | Object containing functions to manage Shopify sessions. |
| [webhooks](./webhooks/README.md) | Object containing functions to configure and handle Shopify webhooks. |
| [billing](./billing/README.md) | Object containing functions to enable apps to bill merchants. |
| [flow](./flow/README.md) | Object containing functions to authenticate Flow extension requests. |
| [utils](./utils/README.md) | Object containing general functions to help build apps. |
| [rest](../guides/rest-resources.md) | Object containing OO representations of the Admin REST API. See the [API reference documentation](https://shopify.dev/docs/api/admin-rest) for details. |

Expand Down
119 changes: 119 additions & 0 deletions packages/shopify-api/lib/flow/__tests__/flow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {shopifyApi} from '../..';
import {ShopifyHeader} from '../../types';
import {
createSHA256HMAC,
HashFormat,
type NormalizedRequest,
} from '../../../runtime';
import {testConfig} from '../../__tests__/test-config';
import {FlowValidationErrorReason} from '../types';

describe('flow', () => {
describe('validate', () => {
describe('failure cases', () => {
it('fails if the HMAC header is missing', async () => {
// GIVEN
const shopify = shopifyApi(testConfig());

const payload = {field: 'value'};
const req: NormalizedRequest = {
method: 'GET',
url: 'https://my-app.my-domain.io',
headers: {},
};

// WHEN
const result = await shopify.flow.validate({
rawBody: JSON.stringify(payload),
rawRequest: req,
});

// THEN
expect(result).toMatchObject({
valid: false,
reason: FlowValidationErrorReason.MissingHmac,
});
});

it('fails if the HMAC header is invalid', async () => {
// GIVEN
const shopify = shopifyApi(testConfig());

const payload = {field: 'value'};
const req: NormalizedRequest = {
method: 'GET',
url: 'https://my-app.my-domain.io',
headers: {[ShopifyHeader.Hmac]: 'invalid'},
};

// WHEN
const result = await shopify.flow.validate({
rawBody: JSON.stringify(payload),
rawRequest: req,
});

// THEN
expect(result).toMatchObject({
valid: false,
reason: FlowValidationErrorReason.InvalidHmac,
});
});

it('fails if the body is empty', async () => {
// GIVEN
const shopify = shopifyApi(testConfig());

const req: NormalizedRequest = {
method: 'GET',
url: 'https://my-app.my-domain.io',
headers: {
[ShopifyHeader.Hmac]: await createSHA256HMAC(
shopify.config.apiSecretKey,
'',
HashFormat.Base64,
),
},
};

// WHEN
const result = await shopify.flow.validate({
rawBody: '',
rawRequest: req,
});

// THEN
expect(result).toMatchObject({
valid: false,
reason: FlowValidationErrorReason.MissingBody,
});
});
});

it('succeeds if the body and HMAC header are correct', async () => {
// GIVEN
const shopify = shopifyApi(testConfig());

const payload = {field: 'value'};
const req: NormalizedRequest = {
method: 'GET',
url: 'https://my-app.my-domain.io',
headers: {
[ShopifyHeader.Hmac]: await createSHA256HMAC(
shopify.config.apiSecretKey,
JSON.stringify(payload),
HashFormat.Base64,
),
},
};

// WHEN
const result = await shopify.flow.validate({
rawBody: JSON.stringify(payload),
rawRequest: req,
});

// THEN
expect(result).toMatchObject({valid: true});
});
});
});
12 changes: 12 additions & 0 deletions packages/shopify-api/lib/flow/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import {AdapterArgs} from '../../runtime/types';

export interface FlowValidateParams extends AdapterArgs {
/**
* The raw body of the request.
*/
rawBody: string;
}

Expand All @@ -11,10 +14,19 @@ export enum FlowValidationErrorReason {
}

export interface FlowValidationInvalid {
/**
* Whether the request is a valid Flow request from Shopify.
*/
valid: false;
/**
* The reason why the request is not valid.
*/
reason: FlowValidationErrorReason;
}

export interface FlowValidationValid {
/**
* Whether the request is a valid Flow request from Shopify.
*/
valid: true;
}
5 changes: 3 additions & 2 deletions packages/shopify-api/lib/flow/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {HashFormat} from '../../runtime/crypto/types';
import {ConfigInterface} from '../base-types';
import {safeCompare} from '../auth/oauth/safe-compare';
import {logger} from '../logger';
import {ShopifyHeader} from '../types';

import {
FlowValidateParams,
Expand All @@ -20,10 +21,10 @@ export function validateFactory(config: ConfigInterface) {
const request = await abstractConvertRequest(adapterArgs);

if (!rawBody.length) {
fail(FlowValidationErrorReason.MissingBody, config);
return fail(FlowValidationErrorReason.MissingBody, config);
}

const hmac = getHeader(request.headers, 'x-shopify-hmac-sha256');
const hmac = getHeader(request.headers, ShopifyHeader.Hmac);

if (!hmac) {
return fail(FlowValidationErrorReason.MissingHmac, config);
Expand Down
3 changes: 2 additions & 1 deletion packages/shopify-api/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export * from './billing/types';
export * from './clients/types';
export * from './session/types';
export * from './webhooks/types';
export * from './flow/types';

export interface Shopify<
Params extends ConfigParams = ConfigParams,
Expand Down Expand Up @@ -69,9 +70,9 @@ export function shopifyApi<
utils: shopifyUtils(validatedConfig),
webhooks: shopifyWebhooks(validatedConfig),
billing: shopifyBilling(validatedConfig),
flow: shopifyFlow(validatedConfig),
logger: logger(validatedConfig),
rest: {} as Resources,
flow: shopifyFlow(validatedConfig),
};

if (restResources) {
Expand Down
7 changes: 7 additions & 0 deletions packages/shopify-api/runtime/http/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ export type AdapterRequest = any;
export type AdapterResponse = any;
export type AdapterHeaders = any;
export interface AdapterArgs {
/**
* The raw request, from the app's framework.
*/
rawRequest: AdapterRequest;
/**
* The raw response, from the app's framework. Only applies to frameworks that expose an API similar to Node's HTTP
* module.
*/
rawResponse?: AdapterResponse;
}

Expand Down

0 comments on commit c883867

Please sign in to comment.