diff --git a/.changeset/brown-hounds-suffer.md b/.changeset/brown-hounds-suffer.md new file mode 100644 index 000000000..34eaa87da --- /dev/null +++ b/.changeset/brown-hounds-suffer.md @@ -0,0 +1,5 @@ +--- +'@shopify/shopify-api': patch +--- + +Allow not checking a session token payload's `aud` field to support tokens generated outside of the Shopify Admin. diff --git a/.github/workflows/markdown_link_checker_config.json b/.github/workflows/markdown_link_checker_config.json index 0d2a104f3..fb1c64b0a 100644 --- a/.github/workflows/markdown_link_checker_config.json +++ b/.github/workflows/markdown_link_checker_config.json @@ -1,4 +1,9 @@ { "retryOn429": true, - "fallbackRetryDelay": "1s" + "fallbackRetryDelay": "1s", + "ignorePatterns": [ + { + "pattern": "^https://help.shopify.com" + } + ] } diff --git a/docs/reference/session/decodeSessionToken.md b/docs/reference/session/decodeSessionToken.md index 243ac5105..a4d7dda64 100644 --- a/docs/reference/session/decodeSessionToken.md +++ b/docs/reference/session/decodeSessionToken.md @@ -22,6 +22,19 @@ app.get('/fetch-some-data', async (req, res) => { The token to parse. +### options + +`Object` + +An object that allows the following fields: + +#### checkAudience + +`boolean` | Defaults to `true` + +Whether the method should check the `aud` field in the decoded payload. +This should always be set to `true` if the token is coming from the Shopify Admin. + ## Return `Promise` diff --git a/lib/session/__tests__/decode-session-token.test.ts b/lib/session/__tests__/decode-session-token.test.ts index 13d54a30e..2539ad309 100644 --- a/lib/session/__tests__/decode-session-token.test.ts +++ b/lib/session/__tests__/decode-session-token.test.ts @@ -65,4 +65,29 @@ describe('JWT session token', () => { ShopifyErrors.InvalidJwtError, ); }); + + test("doesn't fail on a mismatching API key when not checking the token's audience", async () => { + shopify.config.apiKey = 'something_else'; + + // The token is signed with a key that is not the current value + const token = await signJWT(shopify.config.apiSecretKey, payload); + + const actualPayload = await shopify.session.decodeSessionToken(token, { + checkAudience: false, + }); + expect(actualPayload).toStrictEqual(payload); + }); + + test("doesn't fail on a missing aud field when not checking the token's audience", async () => { + const payloadWithoutAud = {...payload}; + delete (payloadWithoutAud as any).aud; + + // The token is signed with a key that is not the current value + const token = await signJWT(shopify.config.apiSecretKey, payload); + + const actualPayload = await shopify.session.decodeSessionToken(token, { + checkAudience: false, + }); + expect(actualPayload).toStrictEqual(payload); + }); }); diff --git a/lib/session/decode-session-token.ts b/lib/session/decode-session-token.ts index cdd5e4031..ff314b133 100644 --- a/lib/session/decode-session-token.ts +++ b/lib/session/decode-session-token.ts @@ -8,8 +8,15 @@ import {JwtPayload} from './types'; const JWT_PERMITTED_CLOCK_TOLERANCE = 10; +export interface DecodeSessionTokenOptions { + checkAudience?: boolean; +} + export function decodeSessionToken(config: ConfigInterface) { - return async (token: string): Promise => { + return async ( + token: string, + {checkAudience = true}: DecodeSessionTokenOptions = {}, + ): Promise => { let payload: JwtPayload; try { payload = ( @@ -26,7 +33,7 @@ export function decodeSessionToken(config: ConfigInterface) { // The exp and nbf fields are validated by the JWT library - if (payload.aud !== config.apiKey) { + if (checkAudience && payload.aud !== config.apiKey) { throw new ShopifyErrors.InvalidJwtError( 'Session token had invalid API key', );