diff --git a/app/code/Magento/LoginAsCustomerApi/Api/GenerateLoginCustomerTokenInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/GenerateLoginCustomerTokenInterface.php
new file mode 100644
index 0000000000000..98e3b3c719b99
--- /dev/null
+++ b/app/code/Magento/LoginAsCustomerApi/Api/GenerateLoginCustomerTokenInterface.php
@@ -0,0 +1,26 @@
+loginAsCustomerConfig->isEnabled()) {
+ throw new LocalizedException(__('Login As Customer module is disabled.'));
+ }
+ try {
+ $authenticationData = $this->getAuthenticationDataBySecret->execute($secret);
+ $customerId = $authenticationData->getCustomerId();
+ } catch (\Exception $e) {
+ throw new AuthenticationException(__('Invalid or expired secret.'));
+ }
+ $context = new CustomUserContext(
+ (int)$customerId,
+ CustomUserContext::USER_TYPE_CUSTOMER
+ );
+ $params = $this->tokenManager->createUserTokenParameters();
+ return $this->tokenManager->create($context, $params);
+ }
+}
diff --git a/app/code/Magento/LoginAsCustomerApi/README.md b/app/code/Magento/LoginAsCustomerApi/README.md
index fddc5d97d4696..cbf1ca9e39b83 100644
--- a/app/code/Magento/LoginAsCustomerApi/README.md
+++ b/app/code/Magento/LoginAsCustomerApi/README.md
@@ -48,6 +48,9 @@ This module provides API for ability to login into customer account for an admin
- `\Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerCustomerIdInterface`:
- set id of customer admin is logged as
+- `\Magento\LoginAsCustomerApi\Api\GenerateLoginCustomerTokenInterface`:
+ - generate an integration access token from a valid Login As Customer secret
+
For information about a public API, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/).
## Additional information
diff --git a/app/code/Magento/LoginAsCustomerApi/Test/Unit/Model/GenerateLoginCustomerTokenTest.php b/app/code/Magento/LoginAsCustomerApi/Test/Unit/Model/GenerateLoginCustomerTokenTest.php
new file mode 100644
index 0000000000000..038b1851fc4b8
--- /dev/null
+++ b/app/code/Magento/LoginAsCustomerApi/Test/Unit/Model/GenerateLoginCustomerTokenTest.php
@@ -0,0 +1,125 @@
+getAuthenticationDataBySecret = $this->createMock(GetAuthenticationDataBySecretInterface::class);
+ $this->loginAsCustomerConfig = $this->createMock(LoginAsCustomerConfig::class);
+ $this->tokenManager = $this->createMock(TokenManager::class);
+
+ $this->service = new GenerateLoginCustomerToken(
+ $this->getAuthenticationDataBySecret,
+ $this->loginAsCustomerConfig,
+ $this->tokenManager
+ );
+ }
+
+ public function testModuleDisabledThrowsException(): void
+ {
+ $this->loginAsCustomerConfig
+ ->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(false);
+
+ $this->expectException(LocalizedException::class);
+ $this->expectExceptionMessage('Login As Customer module is disabled.');
+
+ $this->service->createCustomerAccessToken('secret123');
+ }
+
+ public function testInvalidSecretThrowsAuthenticationException(): void
+ {
+ $this->loginAsCustomerConfig
+ ->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(true);
+
+ $this->getAuthenticationDataBySecret
+ ->expects($this->once())
+ ->method('execute')
+ ->willThrowException(new \Exception('Secret invalid.'));
+
+ $this->expectException(AuthenticationException::class);
+ $this->expectExceptionMessage('Invalid or expired secret.');
+
+ $this->service->createCustomerAccessToken('invalid-secret');
+ }
+
+ public function testValidSecretReturnsToken(): void
+ {
+ $secret = 'valid-secret';
+ $customerId = 42;
+ $expectedToken = 'generated-token-xyz';
+
+ $this->loginAsCustomerConfig
+ ->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(true);
+
+ $authenticationData = $this->createMock(AuthenticationDataInterface::class);
+ $authenticationData
+ ->expects($this->once())
+ ->method('getCustomerId')
+ ->willReturn($customerId);
+
+ $this->getAuthenticationDataBySecret
+ ->expects($this->once())
+ ->method('execute')
+ ->with($secret)
+ ->willReturn($authenticationData);
+
+ $tokenParams = $this->createMock(UserTokenParameters::class);
+ $this->tokenManager
+ ->expects($this->once())
+ ->method('createUserTokenParameters')
+ ->willReturn($tokenParams);
+
+ $this->tokenManager
+ ->expects($this->once())
+ ->method('create')
+ ->with(
+ $this->callback(function ($context) use ($customerId) {
+ return $context instanceof CustomUserContext
+ && $context->getUserId() === $customerId
+ && $context->getUserType() === CustomUserContext::USER_TYPE_CUSTOMER;
+ }),
+ $tokenParams
+ )
+ ->willReturn($expectedToken);
+
+ $result = $this->service->createCustomerAccessToken($secret);
+
+ $this->assertSame($expectedToken, $result);
+ }
+}
diff --git a/app/code/Magento/LoginAsCustomerApi/etc/acl.xml b/app/code/Magento/LoginAsCustomerApi/etc/acl.xml
new file mode 100644
index 0000000000000..45312d7476d10
--- /dev/null
+++ b/app/code/Magento/LoginAsCustomerApi/etc/acl.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/LoginAsCustomerApi/etc/di.xml b/app/code/Magento/LoginAsCustomerApi/etc/di.xml
index d92f8e06febc2..59f03c92c6dc5 100644
--- a/app/code/Magento/LoginAsCustomerApi/etc/di.xml
+++ b/app/code/Magento/LoginAsCustomerApi/etc/di.xml
@@ -9,4 +9,6 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
+
diff --git a/app/code/Magento/LoginAsCustomerApi/etc/webapi.xml b/app/code/Magento/LoginAsCustomerApi/etc/webapi.xml
new file mode 100644
index 0000000000000..938d295a4275a
--- /dev/null
+++ b/app/code/Magento/LoginAsCustomerApi/etc/webapi.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php
index 9fa9d5bb460f7..cdc91d8e8fbce 100644
--- a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php
@@ -419,7 +419,7 @@ public function testDelete(): void
* @dataProvider byStoresProvider
* @magentoApiDataFixture Magento/Cms/_files/pages.php
* @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php
- * @param string $requestStore
+ * @param string $requestStore$rules
* @return void
*/
public function testDeleteByStores(string $requestStore): void
diff --git a/dev/tests/api-functional/testsuite/Magento/LoginAsCustomerApi/Api/GenerateLoginCustomerTokenTest.php b/dev/tests/api-functional/testsuite/Magento/LoginAsCustomerApi/Api/GenerateLoginCustomerTokenTest.php
new file mode 100644
index 0000000000000..8f62969336f1f
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/LoginAsCustomerApi/Api/GenerateLoginCustomerTokenTest.php
@@ -0,0 +1,158 @@
+objectManager = Bootstrap::getObjectManager();
+ $this->adminTokens = $this->objectManager->get(AdminTokenServiceInterface::class);
+ }
+
+ /**
+ * @magentoApiDataFixture Magento/LoginAsCustomer/_files/customer.php
+ * @magentoApiDataFixture Magento/LoginAsCustomerApi/_files/admin_login_as_customer.php
+ * @magentoConfigFixture default/login_as_customer/general/enabled 1
+ */
+ public function testValidSecretGeneratesToken(): void
+ {
+ $user = $this->loadAdminUser();
+ $secret = $this->generateSecret((int)$user->getId());
+ $requestData = ['secret' => $secret];
+
+ $token = $this->_webApiCall($this->getServiceInfo(), $requestData);
+
+ $this->assertIsString($token);
+ $this->assertNotEmpty($token);
+ }
+
+ /**
+ * @magentoApiDataFixture Magento/LoginAsCustomerApi/_files/admin_login_as_customer.php
+ * @magentoConfigFixture default/login_as_customer/general/enabled 1
+ */
+ public function testInvalidSecretThrowsException(): void
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('{"message":"Invalid or expired secret."}');
+
+ $this->_webApiCall($this->getServiceInfo(), ['secret' => 'invalid-secret']);
+ }
+
+ /**
+ * @magentoApiDataFixture Magento/LoginAsCustomerApi/_files/admin_login_as_customer.php
+ * @magentoConfigFixture default/login_as_customer/general/enabled 0
+ */
+ public function testModuleDisabledThrowsException(): void
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('{"message":"Login As Customer module is disabled."}');
+
+ $this->_webApiCall($this->getServiceInfo(), ['secret' => 'test']);
+ }
+
+ /**
+ * @magentoApiDataFixture Magento/LoginAsCustomer/_files/customer.php
+ * @magentoApiDataFixture Magento/LoginAsCustomerApi/_files/admin_login_as_customer.php
+ * @magentoConfigFixture default/login_as_customer/general/enabled 1
+ */
+ public function testAdminDeletedAfterSecretGeneration(): void
+ {
+ $user = $this->loadAdminUser();
+ $secret = $this->generateSecret((int)$user->getId());
+
+ // Delete admin user
+ $userResource = $this->objectManager->get(\Magento\User\Model\ResourceModel\User::class);
+ $userResource->delete($user);
+
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later.');
+
+ $this->_webApiCall($this->getServiceInfo(), ['secret' => $secret]);
+ }
+
+ /**
+ * @magentoApiDataFixture Magento/LoginAsCustomer/_files/customer.php
+ * @magentoApiDataFixture Magento/LoginAsCustomerApi/_files/admin_login_as_customer.php
+ * @magentoConfigFixture default/login_as_customer/general/enabled 1
+ */
+ public function testTamperedSecretThrowsException(): void
+ {
+ $user = $this->loadAdminUser();
+ $secret = $this->generateSecret((int)$user->getId());
+ $tampered = $secret . 'AA';
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('{"message":"Invalid or expired secret."}');
+ $this->_webApiCall($this->getServiceInfo(), ['secret' => $tampered]);
+ }
+
+ /**
+ * Generate REST service info with token.
+ */
+ private function getServiceInfo(): array
+ {
+ $token = $this->adminTokens->createAdminAccessToken(
+ self::ADMIN_USERNAME,
+ \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD
+ );
+
+ return [
+ 'rest' => [
+ 'resourcePath' => self::RESOURCE_PATH,
+ 'httpMethod' => Request::HTTP_METHOD_POST,
+ 'token' => $token,
+ ],
+ ];
+ }
+
+ /**
+ * Loads the admin user created by fixture.
+ */
+ private function loadAdminUser(): User
+ {
+ $user = $this->objectManager->create(User::class);
+ $user->load(self::ADMIN_USERNAME, 'username');
+
+ if (!$user->getId()) {
+ $this->fail('Admin user not found. Fixture admin_login_as_customer.php failed.');
+ }
+
+ return $user;
+ }
+
+ /**
+ * Generates secret for given admin.
+ */
+ private function generateSecret(int $adminId): string
+ {
+ $authData = $this->objectManager
+ ->get(AuthenticationDataInterfaceFactory::class)
+ ->create([
+ 'customerId' => 1,
+ 'adminId' => $adminId,
+ ]);
+
+ return $this->objectManager->get(GenerateAuthenticationSecret::class)
+ ->execute($authData);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/LoginAsCustomerApi/_files/admin_login_as_customer.php b/dev/tests/integration/testsuite/Magento/LoginAsCustomerApi/_files/admin_login_as_customer.php
new file mode 100644
index 0000000000000..0b28ca0ecc2db
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/LoginAsCustomerApi/_files/admin_login_as_customer.php
@@ -0,0 +1,56 @@
+get(RoleFactory::class)->create();
+$role->setName('login_as_customer_api_role');
+$role->setData('role_name', $role->getName());
+$role->setRoleType(Group::ROLE_TYPE);
+$role->setUserType((string)UserContextInterface::USER_TYPE_ADMIN);
+
+/** @var RoleResource $roleResource */
+$roleResource = Bootstrap::getObjectManager()->get(RoleResource::class);
+$roleResource->save($role);
+
+/**
+ * ACL rule: allow access to API token generation
+ */
+$rules = Bootstrap::getObjectManager()->get(RulesFactory::class)->create();
+$rules->setRoleId($role->getId());
+$rules->setResources(['Magento_LoginAsCustomerApi::token']);
+
+/** @var RulesResource $rulesResource */
+$rulesResource = Bootstrap::getObjectManager()->get(RulesResource::class);
+$rulesResource->saveRel($rules);
+
+/**
+ * Create admin user
+ */
+$user = Bootstrap::getObjectManager()->create(User::class);
+$user->setUsername('TestAdminLoginAsCustomer')
+ ->setFirstname('John')
+ ->setLastname('Doe')
+ ->setEmail('testAdminLoginAsCustomer@example.com')
+ ->setPassword(\Magento\TestFramework\Bootstrap::ADMIN_PASSWORD)
+ ->setIsActive(1)
+ ->setRoleId($role->getId());
+
+/** @var UserResource $userResource */
+$userResource = Bootstrap::getObjectManager()->get(UserResource::class);
+$userResource->save($user);
diff --git a/dev/tests/integration/testsuite/Magento/LoginAsCustomerApi/_files/admin_login_as_customer_rollback.php b/dev/tests/integration/testsuite/Magento/LoginAsCustomerApi/_files/admin_login_as_customer_rollback.php
new file mode 100644
index 0000000000000..2bda6a7fb9239
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/LoginAsCustomerApi/_files/admin_login_as_customer_rollback.php
@@ -0,0 +1,42 @@
+create(User::class);
+$user->load('TestAdminLoginAsCustomer', 'username');
+
+/** @var UserResource $userResource */
+$userResource = Bootstrap::getObjectManager()->get(UserResource::class);
+$userResource->delete($user);
+
+/** @var Role $role */
+$role = Bootstrap::getObjectManager()->get(RoleFactory::class)->create();
+$role->load('login_as_customer_api_role', 'role_name');
+
+
+/** @var Rules $rules */
+$rules = Bootstrap::getObjectManager()->get(RulesFactory::class)->create();
+$rules->load($role->getId(), 'role_id');
+
+/** @var RulesResource $rulesResource */
+$rulesResource = Bootstrap::getObjectManager()->get(RulesResource::class);
+$rulesResource->delete($rules);
+
+/** @var RoleResource $roleResource */
+$roleResource = Bootstrap::getObjectManager()->get(RoleResource::class);
+$roleResource->delete($role);