Skip to content
This repository has been archived by the owner on Jan 30, 2023. It is now read-only.

Add option to return redirect URL as a header in verifyRequest #78

Merged
merged 1 commit into from
Mar 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
### Fixed
### Added

- Add `returnHeader` option to `verifyRequest`, which allows using the middleware on XHR requests. [78](https://github.com/Shopify/koa-shopify-auth/pull/78)

## [4.0.3] - 2021-03-16

### Fixed
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ app.use(
// which access mode is being used
// defaults to 'online'
accessMode: 'offline',
// if false, redirect the user to OAuth. If true, send back a 403 with the following headers:
// - X-Shopify-API-Request-Failure-Reauthorize: '1'
// - X-Shopify-API-Request-Failure-Reauthorize-Url: '<auth_url_path>'
// defaults to false
returnHeader: true,
}),
);
```
Expand Down
56 changes: 56 additions & 0 deletions src/verify-request/tests/verify-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import jwt from 'jsonwebtoken';
import verifyRequest from '../verify-request';
import {clearSession} from '../utilities';
import {TEST_COOKIE_NAME, TOP_LEVEL_OAUTH_COOKIE_NAME} from '../../index';
import {REAUTH_HEADER, REAUTH_URL_HEADER} from '../verify-token';
import { clear } from 'console';

const TEST_SHOP = 'testshop.myshopify.io';
Expand Down Expand Up @@ -168,6 +169,29 @@ describe('verifyRequest', () => {
`${authRoute}?shop=${TEST_SHOP}`,
);
});

it('returns a header if setting is active', async () => {
// Session exists but has already expired
const session = await Shopify.Context.SESSION_STORAGE.loadSession(jwtSessionId);
session.expires = new Date(Date.now() - 10);
await Shopify.Utils.storeSession(session);

const verifyRequestMiddleware = verifyRequest({returnHeader: true});
const ctx = createMockContext({
redirect: jest.fn(),
headers: { authorization: `Bearer ${jwtToken}` }
});
const next = jest.fn();

await verifyRequestMiddleware(ctx, next);

expect(ctx.redirect).not.toHaveBeenCalled();
expect(ctx.response.status).toBe(403);
expect(ctx.response.headers).toEqual(expect.objectContaining({
[REAUTH_HEADER.toLowerCase()]: '1',
[REAUTH_URL_HEADER.toLowerCase()]: `/auth?shop=${TEST_SHOP}`,
}));
});
});

describe('when there is no session', () => {
Expand Down Expand Up @@ -262,6 +286,38 @@ describe('verifyRequest', () => {

expect(await clearSession(ctx)).toBeUndefined();
});

it('returns a header if setting is active', async () => {
const jwtPayload = {
iss: `https://${TEST_SHOP}/admin`,
dest: `https://${TEST_SHOP}`,
aud: Shopify.Context.API_KEY,
sub: TEST_USER,
exp: (Date.now() + 3600000) / 1000,
nbf: 1234,
iat: 1234,
jti: '4321',
sid: 'abc123',
};

const jwtToken = jwt.sign(jwtPayload, Shopify.Context.API_SECRET_KEY, { algorithm: 'HS256' });

const verifyRequestMiddleware = verifyRequest({returnHeader: true});
const ctx = createMockContext({
redirect: jest.fn(),
headers: { authorization: `Bearer ${jwtToken}` }
});
const next = jest.fn();

await verifyRequestMiddleware(ctx, next);

expect(ctx.redirect).not.toHaveBeenCalled();
expect(ctx.response.status).toBe(403);
expect(ctx.response.headers).toEqual(expect.objectContaining({
[REAUTH_HEADER.toLowerCase()]: '1',
[REAUTH_URL_HEADER.toLowerCase()]: `/auth?shop=${TEST_SHOP}`,
}));
});
});
});

Expand Down
1 change: 1 addition & 0 deletions src/verify-request/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface Routes {

type VerifyRequestOptions = {
accessMode: AccessMode;
returnHeader: boolean;
}

export type Options = Partial<VerifyRequestOptions> & Partial<Routes>;
5 changes: 3 additions & 2 deletions src/verify-request/verify-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {Options, Routes} from './types';
import {DEFAULT_ACCESS_MODE} from '../auth';

export default function verifyRequest(givenOptions: Options = {}) {
const {accessMode} = {
const {accessMode, returnHeader} = {
accessMode: DEFAULT_ACCESS_MODE,
returnHeader: false,
...givenOptions
};
const routes: Routes = {
Expand All @@ -18,6 +19,6 @@ export default function verifyRequest(givenOptions: Options = {}) {

return compose([
loginAgainIfDifferentShop(routes, accessMode),
verifyToken(routes, accessMode)
verifyToken(routes, accessMode, returnHeader)
]);
}
29 changes: 27 additions & 2 deletions src/verify-request/verify-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {Routes} from './types';
import {redirectToAuth} from './utilities';
import {DEFAULT_ACCESS_MODE} from '../auth';

export function verifyToken(routes: Routes, accessMode: AccessMode = DEFAULT_ACCESS_MODE) {
export const REAUTH_HEADER = 'X-Shopify-API-Request-Failure-Reauthorize';
export const REAUTH_URL_HEADER = 'X-Shopify-API-Request-Failure-Reauthorize-Url';

export function verifyToken(routes: Routes, accessMode: AccessMode = DEFAULT_ACCESS_MODE, returnHeader = false) {
return async function verifyTokenMiddleware(
ctx: Context,
next: NextFunction,
Expand All @@ -30,6 +33,28 @@ export function verifyToken(routes: Routes, accessMode: AccessMode = DEFAULT_ACC

ctx.cookies.set(TEST_COOKIE_NAME, '1');

redirectToAuth(routes, ctx);
if (returnHeader) {
ctx.response.status = 403;
ctx.response.set(REAUTH_HEADER, '1');

let shop: string;
if (session) {
shop = session.shop;
} else if (Shopify.Context.IS_EMBEDDED_APP) {
const authHeader: string = ctx.req.headers.authorization;
const matches = authHeader?.match(/Bearer (.*)/);
if (matches) {
const payload = Shopify.Utils.decodeSessionToken(matches[1]);
shop = payload.dest.replace('https://', '');
}
}

if (shop) {
ctx.response.set(REAUTH_URL_HEADER, `${routes.authRoute}?shop=${shop}`);
}
return;
} else {
redirectToAuth(routes, ctx);
}
};
}