From f823c6b68e82f90b65e4159685383197172686af Mon Sep 17 00:00:00 2001 From: Victor Moura Cortez Date: Wed, 13 May 2026 18:18:07 -0300 Subject: [PATCH 1/2] fix: pass account to VTEX ID calls Co-authored-by: Cursor --- src/clients/external/ID.test.ts | 85 +++++++++++++++++++ src/clients/external/ID.ts | 18 ++-- .../schema/schemaDirectives/Auth.test.ts | 67 +++++++++++++++ .../graphql/schema/schemaDirectives/Auth.ts | 10 +-- 4 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 src/clients/external/ID.test.ts create mode 100644 src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.test.ts diff --git a/src/clients/external/ID.test.ts b/src/clients/external/ID.test.ts new file mode 100644 index 000000000..6d3b2da71 --- /dev/null +++ b/src/clients/external/ID.test.ts @@ -0,0 +1,85 @@ +import { ID } from './ID' + +jest.mock('./ExternalClient', () => ({ + ExternalClient: class { + protected context: any + protected http: any + + constructor (baseURL: string, context: any) { + this.context = context + this.http = { get: jest.fn() } + } + }, +})) + +describe('ID client', () => { + const context: any = { + account: 'storecomponents', + authToken: 'auth-token', + } + + const createClientWithMockedGet = () => { + const client = new ID(context) + const get = jest.fn().mockResolvedValue({ authenticationToken: 'temporary-token' }) + + ;(client as any).http = { get } + + return { client, get } + } + + test('sends account on temporary token requests', async () => { + const { client, get } = createClientWithMockedGet() + + await client.getTemporaryToken() + + expect(get).toHaveBeenCalledWith('/start', expect.objectContaining({ + params: { + an: context.account, + }, + })) + }) + + test('sends account on email code requests', async () => { + const { client, get } = createClientWithMockedGet() + + await client.sendCodeToEmail('authentication-token', 'user@example.com') + + expect(get).toHaveBeenCalledWith('/accesskey/send', expect.objectContaining({ + params: { + an: context.account, + authenticationToken: 'authentication-token', + email: 'user@example.com', + }, + })) + }) + + test('sends account on access key validation requests', async () => { + const { client, get } = createClientWithMockedGet() + + await client.getEmailCodeAuthenticationToken('authentication-token', 'user@example.com', '123456') + + expect(get).toHaveBeenCalledWith('/accesskey/validate', expect.objectContaining({ + params: { + accesskey: '123456', + an: context.account, + authenticationToken: 'authentication-token', + login: 'user@example.com', + }, + })) + }) + + test('sends account on password validation requests', async () => { + const { client, get } = createClientWithMockedGet() + + await client.getPasswordAuthenticationToken('authentication-token', 'user@example.com', 'password') + + expect(get).toHaveBeenCalledWith('/classic/validate', expect.objectContaining({ + params: { + an: context.account, + authenticationToken: 'authentication-token', + login: 'user@example.com', + password: 'password', + }, + })) + }) +}) diff --git a/src/clients/external/ID.ts b/src/clients/external/ID.ts index 2ffb22f52..a1b0dbb1e 100644 --- a/src/clients/external/ID.ts +++ b/src/clients/external/ID.ts @@ -24,14 +24,15 @@ export class ID extends ExternalClient { public getTemporaryToken = (tracingConfig?: RequestTracingConfig) => { const metric = 'vtexid-temp-token' - return this.http.get(routes.START, {metric, tracing: { + const params = this.accountParams() + return this.http.get(routes.START, {metric, params, tracing: { requestSpanNameSuffix: metric, ...tracingConfig?.tracing, }}).then(({authenticationToken}) => authenticationToken) } public sendCodeToEmail = (token: string, email: string, tracingConfig?: RequestTracingConfig) => { - const params = {authenticationToken: token, email} + const params = this.accountParams({authenticationToken: token, email}) const metric = 'vtexid-send-code' return this.http.get(routes.SEND, {metric, params, tracing: { requestSpanNameSuffix: metric, @@ -40,11 +41,11 @@ export class ID extends ExternalClient { } public getEmailCodeAuthenticationToken = (token: string, email: string, code: string, tracingConfig?: RequestTracingConfig) => { - const params = { + const params = this.accountParams({ accesskey: code, authenticationToken: token, login: email, - } + }) const metric = 'vtexid-email-token' return this.http.get(routes.VALIDATE, {metric, params, tracing: { requestSpanNameSuffix: metric, @@ -53,17 +54,22 @@ export class ID extends ExternalClient { } public getPasswordAuthenticationToken = (token: string, email: string, password: string, tracingConfig?: RequestTracingConfig) => { - const params = { + const params = this.accountParams({ authenticationToken: token, login: email, password, - } + }) const metric = 'vtexid-pass-token' return this.http.get(routes.VALIDATE_CLASSIC, {metric, params, tracing: { requestSpanNameSuffix: metric, ...tracingConfig?.tracing, }}) } + + private accountParams = (params: Record = {}) => ({ + ...params, + an: this.context.account, + }) } interface TemporaryToken { diff --git a/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.test.ts b/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.test.ts new file mode 100644 index 000000000..fd8465dca --- /dev/null +++ b/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.test.ts @@ -0,0 +1,67 @@ +import axios from 'axios' + +import { Auth } from './Auth' + +jest.mock('axios', () => ({ + __esModule: true, + default: { + request: jest.fn(), + }, +})) + +describe('Auth directive', () => { + class TestAuth extends Auth { + constructor (args: any) { + super({ + args, + context: {}, + name: 'auth', + schema: {} as any, + visitedType: {} as any, + }) + } + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + test('sends account when validating the VTEX ID token', async () => { + const request = axios.request as jest.Mock + request + .mockResolvedValueOnce({ data: { account: 'storecomponents', user: 'user@example.com' } }) + .mockResolvedValueOnce({ data: true }) + + const field = { + resolve: jest.fn().mockResolvedValue('resolved'), + } as any + const directive = new TestAuth({ + productCode: 'product-code', + resourceCode: 'resource-code', + scope: 'PRIVATE', + }) + const ctx = { + cookies: { + get: jest.fn().mockReturnValue('vtex-id-token'), + }, + get: jest.fn(), + vtex: { + account: 'storecomponents', + authToken: 'auth-token', + }, + } as any + + directive.visitFieldDefinition(field) + + await field.resolve(null, {}, ctx, {}) + + const expectedUrl = 'vtexid.vtex.com.br/api/vtexid/pub/authenticated/user?authToken=vtex-id-token&an=storecomponents' + + expect(request).toHaveBeenNthCalledWith(1, expect.objectContaining({ + headers: expect.objectContaining({ + 'X-VTEX-Proxy-To': `https://${expectedUrl}`, + }), + url: `http://${expectedUrl}`, + })) + }) +}) diff --git a/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.ts b/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.ts index 6dd8dd862..4b5f27cdf 100644 --- a/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.ts +++ b/src/service/worker/runtime/graphql/schema/schemaDirectives/Auth.ts @@ -16,8 +16,8 @@ interface VtexIdParsedToken { account: string } -async function parseIdToken(authToken: string, vtexIdToken: string): Promise { - const url = `vtexid.vtex.com.br/api/vtexid/pub/authenticated/user?authToken=${vtexIdToken}` +async function parseIdToken(authToken: string, vtexIdToken: string, account: string): Promise { + const url = `vtexid.vtex.com.br/api/vtexid/pub/authenticated/user?authToken=${encodeURIComponent(vtexIdToken)}&an=${encodeURIComponent(account)}` const req = await axios.request({ headers: { 'Accept': 'application/json', @@ -51,7 +51,7 @@ async function auth (ctx: ServiceContext, authArgs: AuthDirectiveArgs): Promise< throw new AuthenticationError('VtexIdclientAutCookie not found.') } - const parsedToken = await parseIdToken(ctx.vtex.authToken, vtexIdToken) + const parsedToken = await parseIdToken(ctx.vtex.authToken, vtexIdToken, ctx.vtex.account) if (!parsedToken || parsedToken.account !== ctx.vtex.account) { throw new AuthenticationError('Could not find user specified by VtexIdclientAutCookie.') } @@ -69,7 +69,7 @@ async function auth (ctx: ServiceContext, authArgs: AuthDirectiveArgs): Promise< } function parseArgs (authArgs: AuthDirectiveArgs): AuthDirectiveArgs { - if (authArgs.scope == 'PUBLIC') { + if (authArgs.scope === 'PUBLIC') { return authArgs } @@ -84,7 +84,7 @@ export class Auth extends SchemaDirectiveVisitor { const {resolve = defaultFieldResolver} = field field.resolve = async (root, args, ctx, info) => { const authArgs = parseArgs(this.args as AuthDirectiveArgs) - if (!authArgs.scope || authArgs.scope == 'PRIVATE') { + if (!authArgs.scope || authArgs.scope === 'PRIVATE') { await auth(ctx, authArgs) } return resolve(root, args, ctx, info) From 01980d8f0342808cf70aa024cb5fc124f50060b2 Mon Sep 17 00:00:00 2001 From: Victor Moura Cortez Date: Thu, 14 May 2026 13:36:05 -0300 Subject: [PATCH 2/2] fix: set VTEX ID account param in client defaults Co-authored-by: Cursor --- src/clients/external/ID.test.ts | 36 ++++++++++++++++++++++++--------- src/clients/external/ID.ts | 25 ++++++++++++----------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/clients/external/ID.test.ts b/src/clients/external/ID.test.ts index 6d3b2da71..5aaf5e2f4 100644 --- a/src/clients/external/ID.test.ts +++ b/src/clients/external/ID.test.ts @@ -4,10 +4,12 @@ jest.mock('./ExternalClient', () => ({ ExternalClient: class { protected context: any protected http: any + protected options: any - constructor (baseURL: string, context: any) { + constructor (baseURL: string, context: any, options: any) { this.context = context this.http = { get: jest.fn() } + this.options = options } }, })) @@ -27,15 +29,34 @@ describe('ID client', () => { return { client, get } } - test('sends account on temporary token requests', async () => { - const { client, get } = createClientWithMockedGet() + test('sets the account as a default query parameter', () => { + const client = new ID(context) - await client.getTemporaryToken() + expect((client as any).options.params).toEqual({ + an: context.account, + }) + }) - expect(get).toHaveBeenCalledWith('/start', expect.objectContaining({ + test('preserves custom default query parameters', () => { + const client = new ID(context, { params: { - an: context.account, + locale: 'en-US', }, + }) + + expect((client as any).options.params).toEqual({ + an: context.account, + locale: 'en-US', + }) + }) + + test('requests temporary tokens without overriding default account params', async () => { + const { client, get } = createClientWithMockedGet() + + await client.getTemporaryToken() + + expect(get).toHaveBeenCalledWith('/start', expect.not.objectContaining({ + params: expect.anything(), })) }) @@ -46,7 +67,6 @@ describe('ID client', () => { expect(get).toHaveBeenCalledWith('/accesskey/send', expect.objectContaining({ params: { - an: context.account, authenticationToken: 'authentication-token', email: 'user@example.com', }, @@ -61,7 +81,6 @@ describe('ID client', () => { expect(get).toHaveBeenCalledWith('/accesskey/validate', expect.objectContaining({ params: { accesskey: '123456', - an: context.account, authenticationToken: 'authentication-token', login: 'user@example.com', }, @@ -75,7 +94,6 @@ describe('ID client', () => { expect(get).toHaveBeenCalledWith('/classic/validate', expect.objectContaining({ params: { - an: context.account, authenticationToken: 'authentication-token', login: 'user@example.com', password: 'password', diff --git a/src/clients/external/ID.ts b/src/clients/external/ID.ts index a1b0dbb1e..569534def 100644 --- a/src/clients/external/ID.ts +++ b/src/clients/external/ID.ts @@ -19,20 +19,25 @@ const endpoint = (env: string) => { export class ID extends ExternalClient { constructor (context: IOContext, opts?: InstanceOptions) { - super(endpoint(VTEXID_ENDPOINTS.STABLE), context, opts) + super(endpoint(VTEXID_ENDPOINTS.STABLE), context, { + ...opts, + params: { + an: context.account, + ...opts?.params, + }, + }) } public getTemporaryToken = (tracingConfig?: RequestTracingConfig) => { const metric = 'vtexid-temp-token' - const params = this.accountParams() - return this.http.get(routes.START, {metric, params, tracing: { + return this.http.get(routes.START, {metric, tracing: { requestSpanNameSuffix: metric, ...tracingConfig?.tracing, }}).then(({authenticationToken}) => authenticationToken) } public sendCodeToEmail = (token: string, email: string, tracingConfig?: RequestTracingConfig) => { - const params = this.accountParams({authenticationToken: token, email}) + const params = {authenticationToken: token, email} const metric = 'vtexid-send-code' return this.http.get(routes.SEND, {metric, params, tracing: { requestSpanNameSuffix: metric, @@ -41,11 +46,11 @@ export class ID extends ExternalClient { } public getEmailCodeAuthenticationToken = (token: string, email: string, code: string, tracingConfig?: RequestTracingConfig) => { - const params = this.accountParams({ + const params = { accesskey: code, authenticationToken: token, login: email, - }) + } const metric = 'vtexid-email-token' return this.http.get(routes.VALIDATE, {metric, params, tracing: { requestSpanNameSuffix: metric, @@ -54,11 +59,11 @@ export class ID extends ExternalClient { } public getPasswordAuthenticationToken = (token: string, email: string, password: string, tracingConfig?: RequestTracingConfig) => { - const params = this.accountParams({ + const params = { authenticationToken: token, login: email, password, - }) + } const metric = 'vtexid-pass-token' return this.http.get(routes.VALIDATE_CLASSIC, {metric, params, tracing: { requestSpanNameSuffix: metric, @@ -66,10 +71,6 @@ export class ID extends ExternalClient { }}) } - private accountParams = (params: Record = {}) => ({ - ...params, - an: this.context.account, - }) } interface TemporaryToken {