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

Commit

Permalink
Merge pull request #78 from Shopify/return_header_on_verify_request
Browse files Browse the repository at this point in the history
Add option to return redirect URL as a header in verifyRequest
  • Loading branch information
paulomarg committed Mar 23, 2021
2 parents d50cdc1 + f2ec2d2 commit 964efee
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 4 deletions.
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);
}
};
}

0 comments on commit 964efee

Please sign in to comment.