Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion packages/cli-kit/src/private/node/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import {ApplicationToken, IdentityToken, Sessions} from './session/schema.js'
import {validateSession} from './session/validate.js'
import {applicationId} from './session/identity.js'
import {pollForDeviceAuthorization, requestDeviceAuthorization} from './session/device-authorization.js'
import {automationTokenVariable, automationTokenVariablesProblem} from './session/automation-token.js'
import {
automationTokenAuthMethod,
automationTokenVariable,
automationTokenVariablesProblem,
} from './session/automation-token.js'
import {getCurrentSessionId, setCurrentSessionId} from './conf-store.js'
import * as fqdnModule from '../../public/node/context/fqdn.js'
import {themeToken} from '../../public/node/context/local.js'
Expand Down Expand Up @@ -660,6 +664,7 @@ describe('getLastSeenAuthMethod', () => {
test('returns partners_token if there is a partners token in the environment', async () => {
// Given
vi.mocked(getAppAutomationToken).mockReturnValue('partners-token-456')
vi.mocked(automationTokenAuthMethod).mockReturnValue('partners_token')

// When
const method = await getLastSeenAuthMethod()
Expand All @@ -669,6 +674,18 @@ describe('getLastSeenAuthMethod', () => {
expect(getCurrentSessionId).toHaveBeenCalledOnce()
})

test('returns service_account_token if the service account token is in the environment', async () => {
// Given
vi.mocked(getAppAutomationToken).mockReturnValue('service-account-token-789')
vi.mocked(automationTokenAuthMethod).mockReturnValue('service_account_token')

// When
const method = await getLastSeenAuthMethod()

// Then
expect(method).toBe('service_account_token')
})

test('returns custom_app_token if there is a theme token in the environment and doesnt start with shptka_', async () => {
// Given
vi.mocked(themeToken).mockReturnValue('theme-token-123')
Expand Down
15 changes: 11 additions & 4 deletions packages/cli-kit/src/private/node/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import {IdentityToken, Session, Sessions} from './session/schema.js'
import * as sessionStore from './session/store.js'
import {pollForDeviceAuthorization, requestDeviceAuthorization} from './session/device-authorization.js'
import {automationTokenVariablesProblem} from './session/automation-token.js'
import {automationTokenAuthMethod, automationTokenVariablesProblem} from './session/automation-token.js'
import {isThemeAccessSession} from './api/rest.js'
import {getCurrentSessionId, setCurrentSessionId} from './conf-store.js'
import {UserEmailQueryString, UserEmailQuery} from './api/graphql/business-platform-destinations/user-email.js'
Expand Down Expand Up @@ -115,7 +115,13 @@ export interface OAuthSession {
userId: string
}

type AuthMethod = 'partners_token' | 'device_auth' | 'theme_access_token' | 'custom_app_token' | 'none'
type AuthMethod =
| 'partners_token'
| 'service_account_token'
| 'device_auth'
| 'theme_access_token'
| 'custom_app_token'
| 'none'

let userId: undefined | string
let authMethod: AuthMethod = 'none'
Expand Down Expand Up @@ -155,7 +161,8 @@ export function setLastSeenUserIdAfterAuth(id: string) {
* This function checks for the authentication method in the following order:
* 1. Returns the cached auth method if it's not 'none'.
* 2. Checks for a cached session, which implies 'device_auth' was used.
* 3. Checks for a partners token in the environment.
* 3. Checks for an automation token in the environment: `service_account_token` for SHOPIFY_SERVICE_ACCOUNT_TOKEN,
* `partners_token` for the app and Partner variables.
* 4. Checks for a theme password in the environment.
* 5. If none of the above are true, returns 'none'.
*
Expand All @@ -167,7 +174,7 @@ export async function getLastSeenAuthMethod(): Promise<AuthMethod> {
if (getCurrentSessionId()) return 'device_auth'

const appAutomationToken = getAppAutomationToken()
if (appAutomationToken) return 'partners_token'
if (appAutomationToken) return automationTokenAuthMethod()

const themePassword = themeToken()
if (themePassword) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import {automationTokenVariable, automationTokenVariablesProblem} from './automation-token.js'
import {
automationTokenAuthMethod,
automationTokenVariable,
automationTokenVariablesProblem,
} from './automation-token.js'
import {environmentVariables} from '../constants.js'
import {describe, expect, test} from 'vitest'

Expand Down Expand Up @@ -28,6 +32,21 @@ describe('automationTokenVariable', () => {
})
})

describe('automationTokenAuthMethod', () => {
test.each([
{name: 'the service account variable', env: {[SERVICE_ACCOUNT]: 's'}, expected: 'service_account_token'},
{name: 'the app variable', env: {[APP]: 'a'}, expected: 'partners_token'},
{name: 'the Partners variable', env: {[PARTNERS]: 'p'}, expected: 'partners_token'},
{
name: 'the service account variable over another one',
env: {[SERVICE_ACCOUNT]: 's', [APP]: 'a'},
expected: 'service_account_token',
},
])('reports $expected for $name', ({env, expected}) => {
expect(automationTokenAuthMethod(env)).toBe(expected)
})
})

describe('automationTokenVariablesProblem', () => {
test.each([
{name: 'no automation token variable', env: {}},
Expand Down
17 changes: 17 additions & 0 deletions packages/cli-kit/src/private/node/session/automation-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ export function automationTokenVariable(env: NodeJS.ProcessEnv = process.env): s
return [SERVICE_ACCOUNT_TOKEN, APP_TOKEN, PARTNERS_TOKEN].find((name) => env[name] !== undefined)
}

/**
* Returns the `env_auth_method` analytics value for the automation token variable the CLI authenticates with:
* `service_account_token` for SHOPIFY_SERVICE_ACCOUNT_TOKEN, and `partners_token` for SHOPIFY_APP_AUTOMATION_TOKEN and
* SHOPIFY_CLI_PARTNERS_TOKEN, which existing dashboards already expect.
*
* The value tells which variable held the token, not which kind of token it is: the CLI can't tell tokens apart, and
* any of them works in any of the variables. It shows how much usage has moved to SHOPIFY_SERVICE_ACCOUNT_TOKEN.
*
* @param env - Environment variables to read.
* @returns The auth method to report.
*/
export function automationTokenAuthMethod(
env: NodeJS.ProcessEnv = process.env,
): 'service_account_token' | 'partners_token' {
return automationTokenVariable(env) === SERVICE_ACCOUNT_TOKEN ? 'service_account_token' : 'partners_token'
}
Comment thread
egaodd marked this conversation as resolved.

/**
* Explains why the automation token variables can't be used, so a misconfigured environment fails instead of
* falling back to the logged-in user.
Expand Down
20 changes: 19 additions & 1 deletion packages/cli-kit/src/private/node/session/exchange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {AbortError} from '../../../public/node/error.js'
import {outputDebug} from '../../../public/node/output.js'
import {err, ok} from '../../../public/node/result.js'

import {describe, test, expect, vi, afterAll, beforeEach} from 'vitest'
import {describe, test, expect, vi, afterAll, afterEach, beforeEach} from 'vitest'
import {Response} from 'node-fetch'

const currentDate = new Date(2022, 1, 1, 10)
Expand Down Expand Up @@ -275,6 +275,10 @@ describe.each(tokenExchangeMethods)(
const grantType = 'urn:ietf:params:oauth:grant-type:token-exchange'
const accessTokenType = 'urn:ietf:params:oauth:token-type:access_token'

afterEach(() => {
vi.unstubAllEnvs()
})

test(`Executing ${tokenExchangeMethod.name} returns access token and user ID for a valid CLI token`, async () => {
// Given
let capturedUrl = ''
Expand Down Expand Up @@ -325,6 +329,20 @@ describe.each(tokenExchangeMethods)(
expect(params.get('subject_token')).toBe(automationToken)
})

test(`Executing ${tokenExchangeMethod.name} reports service_account_token when SHOPIFY_SERVICE_ACCOUNT_TOKEN is used`, async () => {
// Given
vi.stubEnv('SHOPIFY_SERVICE_ACCOUNT_TOKEN', automationToken)
vi.mocked(shopifyFetch).mockResolvedValue(
new Response(JSON.stringify({access_token: 'expected_access_token', expires_in: 300, scope: 'scope'})),
)

// When
await tokenExchangeMethod(automationToken)

// Then
await expect(getLastSeenAuthMethod()).resolves.toBe('service_account_token')
})

test(`Executing ${tokenExchangeMethod.name} throws AbortError and logs the caught error if an error is caught`, async () => {
const expectedErrorMessage = `The custom token provided can't be used for the ${expectedErrorName} API.`
vi.mocked(shopifyFetch).mockImplementation(async () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/cli-kit/src/private/node/session/exchange.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {ApplicationToken, IdentityToken} from './schema.js'
import {applicationId, clientId as getIdentityClientId} from './identity.js'
import {tokenExchangeScopes} from './scopes.js'
import {automationTokenAuthMethod} from './automation-token.js'
import {API} from '../api.js'
import {identityFqdn} from '../../../public/node/context/fqdn.js'
import {shopifyFetch} from '../../../public/node/http.js'
Expand Down Expand Up @@ -89,7 +90,7 @@ async function exchangeAppAutomationTokenForAccessToken(
const accessToken = newToken[appId]!.accessToken
const userId = nonRandomUUID(token)
setLastSeenUserIdAfterAuth(userId)
setLastSeenAuthMethod('partners_token')
setLastSeenAuthMethod(automationTokenAuthMethod())
return {accessToken, userId}
} catch (error) {
const prettyName = apiName.replace(/-/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase())
Expand Down
Loading