diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index 8f4373657d..0d7828f29d 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `AuthenticationController.getCustomerServiceToken` method and messenger action to retrieve a Customer Service specific access token ([#9442](https://github.com/MetaMask/core/pull/9442)) + - Exchanges the OIDC access token for a short-lived token scoped to the customer-service audience via `POST /api/v2/customer-service/token`, which Customer Service tooling consumes to identify and authenticate the user. + ### Changed - Bump `@metamask/keyring-controller` from `^27.0.0` to `^27.1.0` ([#9129](https://github.com/MetaMask/core/pull/9129)) diff --git a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController-method-action-types.ts b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController-method-action-types.ts index aa72ad6fe8..221d5f67f8 100644 --- a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController-method-action-types.ts +++ b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController-method-action-types.ts @@ -85,6 +85,22 @@ export type AuthenticationControllerGetUserProfileLineageAction = { handler: AuthenticationController['getUserProfileLineage']; }; +/** + * Returns a Customer Service specific access token for the specified SRP, + * logging in if needed. + * + * Exchanges the OIDC access token for a short-lived token scoped to the + * customer-service audience. Customer Service tooling consumes this token to + * identify and authenticate the user. + * + * @param entropySourceId - The entropy source ID. Omit for the primary SRP. + * @returns The customer-service access token. + */ +export type AuthenticationControllerGetCustomerServiceTokenAction = { + type: `AuthenticationController:getCustomerServiceToken`; + handler: AuthenticationController['getCustomerServiceToken']; +}; + export type AuthenticationControllerIsSignedInAction = { type: `AuthenticationController:isSignedIn`; handler: AuthenticationController['isSignedIn']; @@ -101,4 +117,5 @@ export type AuthenticationControllerMethodActions = | AuthenticationControllerGetSessionProfileAction | AuthenticationControllerRefreshCanonicalProfileIdAction | AuthenticationControllerGetUserProfileLineageAction + | AuthenticationControllerGetCustomerServiceTokenAction | AuthenticationControllerIsSignedInAction; diff --git a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts index 5469c04228..0351e8f10b 100644 --- a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts +++ b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts @@ -9,7 +9,10 @@ import type { import type { LoginResponse } from '../../sdk'; import { Platform } from '../../sdk'; import { arrangeAuthAPIs } from '../../sdk/__fixtures__/auth'; -import { MOCK_USER_PROFILE_LINEAGE_RESPONSE } from '../../sdk/mocks/auth'; +import { + MOCK_ACCESS_JWT, + MOCK_USER_PROFILE_LINEAGE_RESPONSE, +} from '../../sdk/mocks/auth'; import { AuthenticationController } from './AuthenticationController'; import type { AuthenticationControllerMessenger, @@ -1069,6 +1072,75 @@ describe('AuthenticationController', () => { }); }); + describe('getCustomerServiceToken', () => { + it('should throw error if not logged in', async () => { + const metametrics = createMockAuthMetaMetrics(); + const { messenger } = createMockAuthenticationMessenger(); + const controller = new AuthenticationController({ + messenger, + state: { isSignedIn: false, needsProfilePairing: true }, + metametrics, + }); + + await expect(controller.getCustomerServiceToken()).rejects.toThrow( + expect.any(Error), + ); + }); + + it('should return the customer service token', async () => { + const metametrics = createMockAuthMetaMetrics(); + mockAuthenticationFlowEndpoints(); + + const { messenger } = createMockAuthenticationMessenger(); + const originalState = mockSignedInState(); + const controller = new AuthenticationController({ + messenger, + state: originalState, + metametrics, + }); + + const result = await controller.getCustomerServiceToken(); + expect(result).toBe(MOCK_ACCESS_JWT); + }); + + it('should throw if the customer service token request fails', async () => { + const metametrics = createMockAuthMetaMetrics(); + mockAuthenticationFlowEndpoints({ endpointFail: 'customerService' }); + + const { messenger } = createMockAuthenticationMessenger(); + const originalState = mockSignedInState(); + const controller = new AuthenticationController({ + messenger, + state: originalState, + metametrics, + }); + + await expect(controller.getCustomerServiceToken()).rejects.toThrow( + expect.any(Error), + ); + }); + + it('should throw error if wallet is locked', async () => { + const metametrics = createMockAuthMetaMetrics(); + const { messenger, mockKeyringControllerGetState } = + createMockAuthenticationMessenger(); + + const originalState = mockSignedInState(); + + mockKeyringControllerGetState.mockReturnValue({ isUnlocked: false }); + + const controller = new AuthenticationController({ + messenger, + state: originalState, + metametrics, + }); + + await expect(controller.getCustomerServiceToken()).rejects.toThrow( + expect.any(Error), + ); + }); + }); + describe('isSignedIn', () => { it('should return false if not logged in', () => { const metametrics = createMockAuthMetaMetrics(); @@ -1412,13 +1484,14 @@ function createMockAuthenticationMessenger() { * @returns mock auth endpoints */ function mockAuthenticationFlowEndpoints(params?: { - endpointFail: 'nonce' | 'login' | 'token' | 'lineage'; + endpointFail: 'nonce' | 'login' | 'token' | 'lineage' | 'customerService'; }) { const { mockNonceUrl, mockOAuth2TokenUrl, mockSrpLoginUrl, mockUserProfileLineageUrl, + mockCustomerServiceTokenUrl, } = arrangeAuthAPIs({ mockNonceUrl: params?.endpointFail === 'nonce' ? { status: 500 } : undefined, @@ -1428,6 +1501,8 @@ function mockAuthenticationFlowEndpoints(params?: { params?.endpointFail === 'token' ? { status: 500 } : undefined, mockUserProfileLineageUrl: params?.endpointFail === 'lineage' ? { status: 500 } : undefined, + mockCustomerServiceTokenUrl: + params?.endpointFail === 'customerService' ? { status: 500 } : undefined, }); return { @@ -1435,6 +1510,7 @@ function mockAuthenticationFlowEndpoints(params?: { mockOAuth2TokenUrl, mockSrpLoginUrl, mockUserProfileLineageUrl, + mockCustomerServiceTokenUrl, }; } diff --git a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts index 2cf8b92cad..1bc6c14ce5 100644 --- a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts +++ b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts @@ -111,6 +111,7 @@ const MESSENGER_EXPOSED_METHODS = [ 'getSessionProfile', 'refreshCanonicalProfileId', 'getUserProfileLineage', + 'getCustomerServiceToken', 'isSignedIn', 'requestProfilePairing', ] as const; @@ -569,6 +570,26 @@ export class AuthenticationController extends BaseController< return await this.#auth.getUserProfileLineage(resolvedId); } + /** + * Returns a Customer Service specific access token for the specified SRP, + * logging in if needed. + * + * Exchanges the OIDC access token for a short-lived token scoped to the + * customer-service audience. Customer Service tooling consumes this token to + * identify and authenticate the user. + * + * @param entropySourceId - The entropy source ID. Omit for the primary SRP. + * @returns The customer-service access token. + */ + public async getCustomerServiceToken( + entropySourceId?: string, + ): Promise { + this.#assertIsUnlocked('getCustomerServiceToken'); + const resolvedId = + entropySourceId ?? (await this.#getPrimaryEntropySourceId()); + return await this.#auth.getCustomerServiceToken(resolvedId); + } + public isSignedIn(): boolean { return this.state.isSignedIn; } diff --git a/packages/profile-sync-controller/src/controllers/authentication/mocks/mockResponses.test.ts b/packages/profile-sync-controller/src/controllers/authentication/mocks/mockResponses.test.ts index a0f78412f7..8ae5e21883 100644 --- a/packages/profile-sync-controller/src/controllers/authentication/mocks/mockResponses.test.ts +++ b/packages/profile-sync-controller/src/controllers/authentication/mocks/mockResponses.test.ts @@ -1,6 +1,8 @@ import { getMockAuthAccessTokenResponse, + getMockCustomerServiceTokenResponse, getE2EIdentifierFromJwt, + MOCK_CUSTOMER_SERVICE_TOKEN_RESPONSE, MOCK_OATH_TOKEN_RESPONSE, } from './mockResponses'; @@ -75,3 +77,13 @@ describe('getMockAuthAccessTokenResponse()', () => { expect(payload.exp).toBe(4102444800); }); }); + +describe('getMockCustomerServiceTokenResponse()', () => { + it('returns a POST mock for the customer service token endpoint', () => { + const mock = getMockCustomerServiceTokenResponse(); + + expect(mock.requestMethod).toBe('POST'); + expect(mock.url).toContain('/customer-service/token'); + expect(mock.response).toStrictEqual(MOCK_CUSTOMER_SERVICE_TOKEN_RESPONSE); + }); +}); diff --git a/packages/profile-sync-controller/src/controllers/authentication/mocks/mockResponses.ts b/packages/profile-sync-controller/src/controllers/authentication/mocks/mockResponses.ts index 080cc97584..985d4f05fd 100644 --- a/packages/profile-sync-controller/src/controllers/authentication/mocks/mockResponses.ts +++ b/packages/profile-sync-controller/src/controllers/authentication/mocks/mockResponses.ts @@ -4,10 +4,12 @@ import { MOCK_SRP_LOGIN_RESPONSE as SDK_MOCK_SRP_LOGIN_RESPONSE, MOCK_OIDC_TOKEN_RESPONSE as SDK_MOCK_OIDC_TOKEN_RESPONSE, MOCK_PAIR_PROFILES_RESPONSE as SDK_MOCK_PAIR_PROFILES_RESPONSE, + MOCK_CUSTOMER_SERVICE_TOKEN_RESPONSE as SDK_MOCK_CUSTOMER_SERVICE_TOKEN_RESPONSE, MOCK_NONCE_URL, MOCK_SRP_LOGIN_URL, MOCK_OIDC_TOKEN_URL, MOCK_PAIR_PROFILES_URL, + MOCK_CUSTOMER_SERVICE_TOKEN_URL, } from '../../../sdk/mocks/auth'; type MockResponse = { @@ -144,3 +146,14 @@ export const getMockAuthAccessTokenResponse = (): MockResponse => { }, } satisfies MockResponse; }; + +export const MOCK_CUSTOMER_SERVICE_TOKEN_RESPONSE = + SDK_MOCK_CUSTOMER_SERVICE_TOKEN_RESPONSE; + +export const getMockCustomerServiceTokenResponse = (): MockResponse => { + return { + url: MOCK_CUSTOMER_SERVICE_TOKEN_URL, + requestMethod: 'POST', + response: MOCK_CUSTOMER_SERVICE_TOKEN_RESPONSE, + } satisfies MockResponse; +}; diff --git a/packages/profile-sync-controller/src/sdk/__fixtures__/auth.ts b/packages/profile-sync-controller/src/sdk/__fixtures__/auth.ts index 25e1db75d3..791d665834 100644 --- a/packages/profile-sync-controller/src/sdk/__fixtures__/auth.ts +++ b/packages/profile-sync-controller/src/sdk/__fixtures__/auth.ts @@ -14,6 +14,8 @@ import { MOCK_SRP_LOGIN_RESPONSE, MOCK_SRP_LOGIN_URL, MOCK_USER_PROFILE_LINEAGE_RESPONSE, + MOCK_CUSTOMER_SERVICE_TOKEN_URL, + MOCK_CUSTOMER_SERVICE_TOKEN_RESPONSE, } from '../mocks/auth'; type MockReply = { @@ -104,6 +106,21 @@ export const handleMockUserProfileLineage = ( return mockUserProfileLineageEndpoint; }; +export const handleMockCustomerServiceToken = ( + mockReply?: MockReply, +): nock.Scope => { + const reply = mockReply ?? { + status: 200, + body: MOCK_CUSTOMER_SERVICE_TOKEN_RESPONSE, + }; + const mockCustomerServiceTokenEndpoint = nock(MOCK_CUSTOMER_SERVICE_TOKEN_URL) + .persist() + .post('') + .reply(reply.status, reply.body); + + return mockCustomerServiceTokenEndpoint; +}; + export const arrangeAuthAPIs = (options?: { mockNonceUrl?: MockReply; mockOAuth2TokenUrl?: MockReply; @@ -112,6 +129,7 @@ export const arrangeAuthAPIs = (options?: { mockPairIdentifiers?: MockReply; mockPairProfiles?: MockReply; mockUserProfileLineageUrl?: MockReply; + mockCustomerServiceTokenUrl?: MockReply; }): { mockNonceUrl: nock.Scope; mockOAuth2TokenUrl: nock.Scope; @@ -120,6 +138,7 @@ export const arrangeAuthAPIs = (options?: { mockPairIdentifiersUrl: nock.Scope; mockPairProfilesUrl: nock.Scope; mockUserProfileLineageUrl: nock.Scope; + mockCustomerServiceTokenUrl: nock.Scope; } => { const mockNonceUrl = handleMockNonce(options?.mockNonceUrl); const mockOAuth2TokenUrl = handleMockOAuth2Token(options?.mockOAuth2TokenUrl); @@ -132,6 +151,9 @@ export const arrangeAuthAPIs = (options?: { const mockUserProfileLineageUrl = handleMockUserProfileLineage( options?.mockUserProfileLineageUrl, ); + const mockCustomerServiceTokenUrl = handleMockCustomerServiceToken( + options?.mockCustomerServiceTokenUrl, + ); return { mockNonceUrl, @@ -141,5 +163,6 @@ export const arrangeAuthAPIs = (options?: { mockPairIdentifiersUrl, mockPairProfilesUrl, mockUserProfileLineageUrl, + mockCustomerServiceTokenUrl, }; }; diff --git a/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-siwe.ts b/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-siwe.ts index 3d23e1756b..487e4f019b 100644 --- a/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-siwe.ts +++ b/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-siwe.ts @@ -6,6 +6,7 @@ import { SIWE_LOGIN_URL, authenticate, authorizeOIDC, + getCustomerServiceToken, getNonce, getUserProfileLineage, } from './services'; @@ -75,6 +76,11 @@ export class SIWEJwtBearerAuth implements IBaseAuth { return await getUserProfileLineage(this.#config.env, accessToken); } + async getCustomerServiceToken(): Promise { + const accessToken = await this.getAccessToken(); + return await getCustomerServiceToken(this.#config.env, accessToken); + } + async signMessage(message: string): Promise { this.#assertSigner(this.#signer); return await this.#signer.signMessage(message); diff --git a/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.test.ts b/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.test.ts index 93ff7ae37f..47149e3d78 100644 --- a/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.test.ts +++ b/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.test.ts @@ -21,12 +21,15 @@ const mockGetNonce = jest.fn(); const mockAuthenticate = jest.fn(); const mockAuthorizeOIDC = jest.fn(); const mockPairProfiles = jest.fn(); +const mockGetCustomerServiceToken = jest.fn(); jest.mock('./services', () => ({ authenticate: (...args: unknown[]): unknown => mockAuthenticate(...args), authorizeOIDC: (...args: unknown[]): unknown => mockAuthorizeOIDC(...args), getNonce: (...args: unknown[]): unknown => mockGetNonce(...args), getUserProfileLineage: jest.fn(), + getCustomerServiceToken: (...args: unknown[]): unknown => + mockGetCustomerServiceToken(...args), pairProfiles: (...args: unknown[]): unknown => mockPairProfiles(...args), })); @@ -247,6 +250,21 @@ describe('SRPJwtBearerAuth rate limit handling', () => { expect(token).toBe('access'); expect(mockGetNonce).toHaveBeenCalledTimes(1); }); + + it('getCustomerServiceToken exchanges the access token via the service', async () => { + const { auth } = createAuth(); + mockGetCustomerServiceToken.mockResolvedValue('cs-access-token'); + + const result = await auth.getCustomerServiceToken(); + + expect(result).toBe('cs-access-token'); + // Logs in to obtain the OIDC access token, then exchanges it. + expect(mockAuthorizeOIDC).toHaveBeenCalledTimes(1); + expect(mockGetCustomerServiceToken).toHaveBeenCalledWith( + config.env, + 'access', + ); + }); }); describe('SRPJwtBearerAuth profileId resolution', () => { diff --git a/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.ts b/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.ts index 71c31c2efd..5cd06ded82 100644 --- a/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.ts +++ b/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/flow-srp.ts @@ -13,6 +13,7 @@ import { validateLoginResponse } from '../utils/validate-login-response'; import { authenticate, authorizeOIDC, + getCustomerServiceToken, getNonce, getUserProfileLineage, pairProfiles, @@ -165,6 +166,11 @@ export class SRPJwtBearerAuth implements IBaseAuth { return await getUserProfileLineage(this.#config.env, accessToken); } + async getCustomerServiceToken(entropySourceId?: string): Promise { + const accessToken = await this.getAccessToken(entropySourceId); + return await getCustomerServiceToken(this.#config.env, accessToken); + } + async pairSrpProfiles( accessTokens: string[], authAccessToken: string, diff --git a/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/services.test.ts b/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/services.test.ts index 63ac534088..d027faf50d 100644 --- a/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/services.test.ts +++ b/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/services.test.ts @@ -12,6 +12,7 @@ import { pairIdentifiers, pairProfiles, getUserProfileLineage, + getCustomerServiceToken, NONCE_URL, OIDC_TOKEN_URL, SRP_LOGIN_URL, @@ -19,6 +20,7 @@ import { PAIR_IDENTIFIERS, PAIR_PROFILES_URL, PROFILE_LINEAGE_URL, + CUSTOMER_SERVICE_TOKEN_URL, } from './services'; import { AuthType } from './types'; @@ -134,6 +136,12 @@ describe('services', () => { 'https://authentication.dev-api.cx.metamask.io/api/v2/profile/pair', ); }); + + it('should build correct CUSTOMER_SERVICE_TOKEN_URL', () => { + expect(CUSTOMER_SERVICE_TOKEN_URL(Env.DEV)).toBe( + 'https://authentication.dev-api.cx.metamask.io/api/v2/customer-service/token', + ); + }); }); describe('getNonce', () => { @@ -1015,6 +1023,84 @@ describe('services', () => { }); }); + describe('getCustomerServiceToken', () => { + it('should return the customer service token on success', async () => { + const mockResponse = createMockResponse({ + access_token: 'cs-access-token', + }); + mockFetch.mockResolvedValue(mockResponse); + + const result = await getCustomerServiceToken(Env.DEV, 'access-token'); + + expect(result).toBe('cs-access-token'); + expect(mockFetch).toHaveBeenCalledWith( + expect.any(URL), + expect.objectContaining({ + method: 'POST', + headers: { + Authorization: 'Bearer access-token', + }, + }), + ); + }); + + it('should throw SignInError when access_token is missing', async () => { + const mockResponse = createMockResponse({}); + mockFetch.mockResolvedValue(mockResponse); + + await expect( + getCustomerServiceToken(Env.DEV, 'access-token'), + ).rejects.toThrow(SignInError); + await expect( + getCustomerServiceToken(Env.DEV, 'access-token'), + ).rejects.toThrow( + 'Failed to get customer service token: missing access_token', + ); + }); + + it('should throw SignInError on network failure', async () => { + mockFetch.mockRejectedValue(new Error('DNS resolution failed')); + + await expect( + getCustomerServiceToken(Env.DEV, 'access-token'), + ).rejects.toThrow(SignInError); + await expect( + getCustomerServiceToken(Env.DEV, 'access-token'), + ).rejects.toThrow( + 'Failed to get customer service token: DNS resolution failed', + ); + }); + + it('should throw SignInError on error response', async () => { + const mockResponse = createMockResponse( + { message: 'Unauthorized', error: 'invalid_token' }, + { ok: false, status: 401 }, + ); + mockFetch.mockResolvedValue(mockResponse); + + await expect( + getCustomerServiceToken(Env.DEV, 'access-token'), + ).rejects.toThrow(SignInError); + await expect( + getCustomerServiceToken(Env.DEV, 'access-token'), + ).rejects.toThrow( + 'Failed to get customer service token: HTTP 401 - Unauthorized (error: invalid_token)', + ); + }); + + it('should throw RateLimitedError on 429 response', async () => { + const mockResponse = createMockResponse( + { message: 'Rate limited', error: 'too_many_requests' }, + { ok: false, status: 429 }, + ); + mockFetch.mockResolvedValue(mockResponse); + + await expect( + getCustomerServiceToken(Env.DEV, 'access-token'), + ).rejects.toThrow(RateLimitedError); + }); + }); + describe('parseRetryAfter edge cases', () => { it('should handle past HTTP-date by returning null (no delay)', async () => { const pastDate = new Date(Date.now() - 10000).toUTCString(); diff --git a/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/services.ts b/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/services.ts index f6d01cbc50..5c3a5be224 100644 --- a/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/services.ts +++ b/packages/profile-sync-controller/src/sdk/authentication-jwt-bearer/services.ts @@ -158,6 +158,9 @@ export const PAIR_PROFILES_URL = (env: Env): string => export const PROFILE_LINEAGE_URL = (env: Env): string => `${getEnvUrls(env).authApiUrl}/api/v2/profile/lineage`; +export const CUSTOMER_SERVICE_TOKEN_URL = (env: Env): string => + `${getEnvUrls(env).authApiUrl}/api/v2/customer-service/token`; + const getAuthenticationUrl = (authType: AuthType, env: Env): string => { switch (authType) { case AuthType.SRP: @@ -517,3 +520,52 @@ export async function getUserProfileLineage( ); } } + +/** + * Service to get a Customer Service specific access token. + * + * Exchanges a valid OIDC access token for a short-lived token scoped to the + * customer-service audience, which Customer Service tooling consumes to + * identify and authenticate the user. + * + * @param env - server environment + * @param accessToken - JWT access token used to access protected resources + * @returns The customer-service access token. + */ +export async function getCustomerServiceToken( + env: Env, + accessToken: string, +): Promise { + const customerServiceTokenUrl = new URL(CUSTOMER_SERVICE_TOKEN_URL(env)); + + try { + const response = await fetch(customerServiceTokenUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (!response.ok) { + return await throwServiceError( + response, + 'Failed to get customer service token', + SignInError, + ); + } + + const tokenResponse = await response.json(); + if (typeof tokenResponse?.access_token !== 'string') { + throw new SignInError( + 'Failed to get customer service token: missing access_token', + ); + } + return tokenResponse.access_token; + } catch (error) { + return await throwServiceError( + error, + 'Failed to get customer service token', + SignInError, + ); + } +} diff --git a/packages/profile-sync-controller/src/sdk/authentication.ts b/packages/profile-sync-controller/src/sdk/authentication.ts index 5eb1e2d3bd..fdff9253a0 100644 --- a/packages/profile-sync-controller/src/sdk/authentication.ts +++ b/packages/profile-sync-controller/src/sdk/authentication.ts @@ -83,6 +83,10 @@ export class JwtBearerAuth implements SIWEInterface, SRPInterface { return await this.#sdk.getUserProfileLineage(entropySourceId); } + async getCustomerServiceToken(entropySourceId?: string): Promise { + return await this.#sdk.getCustomerServiceToken(entropySourceId); + } + async pairSrpProfiles( accessTokens: string[], authAccessToken: string, diff --git a/packages/profile-sync-controller/src/sdk/mocks/auth.ts b/packages/profile-sync-controller/src/sdk/mocks/auth.ts index 52e2ce367c..8d50ff88af 100644 --- a/packages/profile-sync-controller/src/sdk/mocks/auth.ts +++ b/packages/profile-sync-controller/src/sdk/mocks/auth.ts @@ -7,6 +7,7 @@ import { PAIR_IDENTIFIERS, PAIR_PROFILES_URL, PROFILE_LINEAGE_URL, + CUSTOMER_SERVICE_TOKEN_URL, } from '../authentication-jwt-bearer/services'; export const MOCK_NONCE_URL = NONCE_URL(Env.PRD); @@ -16,6 +17,9 @@ export const MOCK_SIWE_LOGIN_URL = SIWE_LOGIN_URL(Env.PRD); export const MOCK_PAIR_IDENTIFIERS_URL = PAIR_IDENTIFIERS(Env.PRD); export const MOCK_PAIR_PROFILES_URL = PAIR_PROFILES_URL(Env.PRD); export const MOCK_PROFILE_LINEAGE_URL = PROFILE_LINEAGE_URL(Env.PRD); +export const MOCK_CUSTOMER_SERVICE_TOKEN_URL = CUSTOMER_SERVICE_TOKEN_URL( + Env.PRD, +); export const MOCK_JWT = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImIwNzE2N2U2LWJjNWUtNDgyZC1hNjRhLWU1MjQ0MjY2MGU3NyJ9.eyJzdWIiOiI1MzE0ODc5YWM2NDU1OGI3OTQ5ZmI4NWIzMjg2ZjZjNjUwODAzYmFiMTY0Y2QyOWNmMmM3YzdmMjMzMWMwZTRlIiwiaWF0IjoxNzA2MTEzMDYyLCJleHAiOjE3NjkxODUwNjMsImlzcyI6ImF1dGgubWV0YW1hc2suaW8iLCJhdWQiOiJwb3J0Zm9saW8ubWV0YW1hc2suaW8ifQ.E5UL6oABNweS8t5a6IBTqTf7NLOJbrhJSmEcsr7kwLp4bGvcENJzACwnsHDkA6PlzfDV09ZhAGU_F3hlS0j-erbY0k0AFR-GAtyS7E9N02D8RgUDz5oDR65CKmzM8JilgFA8UvruJ6OJGogroaOSOqzRES_s8MjHpP47RJ9lXrUesajsbOudXbuksXWg5QmWip6LLvjwr8UUzcJzNQilyIhiEpo4WdzWM4R3VtTwr4rHnWEvtYnYCov1jmI2w3YQ48y0M-3Y9IOO0ov_vlITRrOnR7Y7fRUGLUFmU5msD8mNWRywjQFLHfJJ1yNP5aJ8TkuCK3sC6kcUH335IVvukQ'; @@ -61,6 +65,13 @@ export const MOCK_OIDC_TOKEN_RESPONSE = { expires_in: 3600, }; +export const MOCK_CUSTOMER_SERVICE_TOKEN_RESPONSE = { + access_token: MOCK_ACCESS_JWT, + refresh_token: 'ory_rt_mock_refresh_token', + expires_in: 3600, + token_type: 'bearer', +}; + export const MOCK_PAIR_PROFILES_RESPONSE = { profile: { identifier_id: MOCK_SRP_LOGIN_RESPONSE.profile.identifier_id,