diff --git a/packages/shopify-api/lib/__tests__/config.test.ts b/packages/shopify-api/lib/__tests__/config.test.ts index 93a48536f..04eb00ad2 100644 --- a/packages/shopify-api/lib/__tests__/config.test.ts +++ b/packages/shopify-api/lib/__tests__/config.test.ts @@ -29,7 +29,6 @@ describe('Config object', () => { expect(config.apiKey).toEqual(validParams.apiKey); expect(config.apiSecretKey).toEqual(validParams.apiSecretKey); - expect(config.scopes.equals(validParams.scopes)).toBeTruthy(); expect(config.hostName).toEqual(validParams.hostName); }); @@ -54,16 +53,6 @@ describe('Config object', () => { expect(error.message).toContain('Missing values for: apiSecretKey'); } - invalid = {...validParams}; - invalid.scopes = []; - try { - validateConfig(invalid); - fail('Initializing without scopes did not throw an exception'); - } catch (error) { - expect(error).toBeInstanceOf(ShopifyErrors.ShopifyError); - expect(error.message).toContain('Missing values for: scopes'); - } - invalid = {...validParams}; invalid.hostName = ''; try { @@ -97,6 +86,14 @@ describe('Config object', () => { validParams.isCustomStoreApp = false; }); + it('scopes can be not defined', () => { + delete (validParams as any).scopes; + + expect(() => validateConfig(validParams)).not.toThrow( + ShopifyErrors.ShopifyError, + ); + }); + it("ignores a missing 'apiKey' when isCustomStoreApp is true", () => { validParams.isCustomStoreApp = true; validParams.adminApiAccessToken = 'token'; diff --git a/packages/shopify-api/lib/auth/oauth/oauth.ts b/packages/shopify-api/lib/auth/oauth/oauth.ts index 5bcc4f5b1..857c16a42 100644 --- a/packages/shopify-api/lib/auth/oauth/oauth.ts +++ b/packages/shopify-api/lib/auth/oauth/oauth.ts @@ -21,6 +21,7 @@ import { import {logger, ShopifyLogger} from '../../logger'; import {DataType} from '../../clients/types'; import {fetchRequestFactory} from '../../utils/fetch-request'; +import {AuthScopes} from '../scopes'; import { SESSION_COOKIE_NAME, @@ -68,6 +69,10 @@ export function begin(config: ConfigInterface): OAuthBegin { config.isCustomStoreApp, 'Cannot perform OAuth for private apps', ); + throwIfScopesUndefined( + config.scopes, + 'Apps that use OAuth must define the required scopes in the config', + ); const log = logger(config); log.info('Beginning OAuth', {shop, isOnline, callbackPath}); @@ -101,7 +106,8 @@ export function begin(config: ConfigInterface): OAuthBegin { const query = { client_id: config.apiKey, - scope: config.scopes.toString(), + // If scopes is undefined, threw an error + scope: config.scopes!.toString(), redirect_uri: `${config.hostScheme}://${config.hostName}${callbackPath}`, state, 'grant_options[]': isOnline ? 'per-user' : '', @@ -255,3 +261,12 @@ function throwIfCustomStoreApp( throw new ShopifyErrors.PrivateAppError(message); } } + +function throwIfScopesUndefined( + scopes: string | AuthScopes | undefined, + message: string, +): void { + if (!scopes) { + throw new ShopifyErrors.MissingRequiredArgument(message); + } +} diff --git a/packages/shopify-api/lib/base-types.ts b/packages/shopify-api/lib/base-types.ts index 2a41e139c..327bffcb2 100644 --- a/packages/shopify-api/lib/base-types.ts +++ b/packages/shopify-api/lib/base-types.ts @@ -27,7 +27,7 @@ export interface ConfigParams< */ apiSecretKey: string; /** - * The scopes your app needs to access the API. + * The scopes your app needs to access the API. Not required if using Shopify managed installation. */ scopes?: string[] | AuthScopes; /** @@ -121,7 +121,7 @@ export type ConfigInterface = Omit< > & { apiKey: string; hostScheme: 'http' | 'https'; - scopes: AuthScopes; + scopes?: AuthScopes; isCustomStoreApp: boolean; billing?: BillingConfig; logger: { diff --git a/packages/shopify-api/lib/config.ts b/packages/shopify-api/lib/config.ts index d06fa8fe4..24aecd9fd 100644 --- a/packages/shopify-api/lib/config.ts +++ b/packages/shopify-api/lib/config.ts @@ -10,7 +10,6 @@ export function validateConfig( const config = { apiKey: '', apiSecretKey: '', - scopes: new AuthScopes([]), hostName: '', hostScheme: 'https', apiVersion: LATEST_API_VERSION, @@ -30,7 +29,6 @@ export function validateConfig( const mandatory: (keyof Params)[] = ['apiSecretKey', 'hostName']; if (!('isCustomStoreApp' in params) || !params.isCustomStoreApp) { mandatory.push('apiKey'); - mandatory.push('scopes'); } if ('isCustomStoreApp' in params && params.isCustomStoreApp) { if ( diff --git a/packages/shopify-api/lib/session/__tests__/session.test.ts b/packages/shopify-api/lib/session/__tests__/session.test.ts index 8ad036eec..152f4d8ff 100644 --- a/packages/shopify-api/lib/session/__tests__/session.test.ts +++ b/packages/shopify-api/lib/session/__tests__/session.test.ts @@ -58,20 +58,46 @@ describe('isActive', () => { expect(session.isActive(shopify.config.scopes)).toBeTruthy(); }); +}); - it('returns false if session is not active', () => { - const shopify = shopifyApi(testConfig()); +it('returns true when scopes that passed in empty and scopes are not equal', () => { + const session = new Session({ + id: 'active', + shop: 'active-shop', + state: 'test_state', + isOnline: true, + scope: 'test_scope', + accessToken: 'indeed', + expires: new Date(Date.now() + 86400), + }); - const session = new Session({ - id: 'not_active', - shop: 'inactive-shop', - state: 'not_same', - isOnline: true, - scope: 'test_scope', - expires: new Date(Date.now() - 1), - }); - expect(session.isActive(shopify.config.scopes)).toBeFalsy(); + expect(session.isActive('')).toBeTruthy(); +}); + +it('returns false if session is not active', () => { + const shopify = shopifyApi(testConfig()); + + const session = new Session({ + id: 'not_active', + shop: 'inactive-shop', + state: 'not_same', + isOnline: true, + scope: 'test_scope', + expires: new Date(Date.now() - 1), + }); + expect(session.isActive(shopify.config.scopes)).toBeFalsy(); +}); + +it('returns false if checking scopes and scopes are not equal', () => { + const session = new Session({ + id: 'not_active', + shop: 'inactive-shop', + state: 'not_same', + isOnline: true, + scope: 'test_scope', + expires: new Date(Date.now() - 1), }); + expect(session.isActive('fake_scope')).toBeFalsy(); }); describe('isExpired', () => { diff --git a/packages/shopify-api/lib/session/session.ts b/packages/shopify-api/lib/session/session.ts index 465217116..36b43d2c5 100644 --- a/packages/shopify-api/lib/session/session.ts +++ b/packages/shopify-api/lib/session/session.ts @@ -172,13 +172,18 @@ export class Session { } /** - * Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes. + * Whether the session is active. Active sessions have an access token that is not expired, and has has the given + * scopes if checkScopes is true. */ public isActive(scopes: AuthScopes | string | string[]): boolean { + const usingScopes = + scopes !== '' && typeof scopes !== 'undefined' && scopes !== null; + const isScopeChanged = this.isScopeChanged(scopes); + const hasAccessToken = Boolean(this.accessToken); + const isTokenNotExpired = !this.isExpired(); + return ( - !this.isScopeChanged(scopes) && - Boolean(this.accessToken) && - !this.isExpired() + (!usingScopes || !isScopeChanged) && hasAccessToken && isTokenNotExpired ); }