From 6243a9471d6db3a5d8f13395a89575d4038e266e Mon Sep 17 00:00:00 2001 From: provokateurin Date: Tue, 20 Feb 2024 09:32:33 +0100 Subject: [PATCH] feat(core): Add OCS endpoint for confirming the user password Signed-off-by: provokateurin --- core/Controller/AppPasswordController.php | 36 ++++++ core/openapi.json | 107 ++++++++++++++++++ core/routes.php | 1 + .../Controller/AppPasswordControllerTest.php | 20 +++- 4 files changed, 163 insertions(+), 1 deletion(-) diff --git a/core/Controller/AppPasswordController.php b/core/Controller/AppPasswordController.php index a4b7791997a..2575729fe85 100644 --- a/core/Controller/AppPasswordController.php +++ b/core/Controller/AppPasswordController.php @@ -31,7 +31,9 @@ namespace OC\Core\Controller; use OC\Authentication\Events\AppPasswordCreatedEvent; use OC\Authentication\Token\IProvider; use OC\Authentication\Token\IToken; +use OC\User\Session; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\UseSession; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\Authentication\Exceptions\CredentialsUnavailableException; @@ -41,6 +43,8 @@ use OCP\Authentication\LoginCredentials\IStore; use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; use OCP\ISession; +use OCP\IUserManager; +use OCP\Security\Bruteforce\IThrottler; use OCP\Security\ISecureRandom; class AppPasswordController extends \OCP\AppFramework\OCSController { @@ -52,6 +56,9 @@ class AppPasswordController extends \OCP\AppFramework\OCSController { private IProvider $tokenProvider, private IStore $credentialStore, private IEventDispatcher $eventDispatcher, + private Session $userSession, + private IUserManager $userManager, + private IThrottler $throttler, ) { parent::__construct($appName, $request); } @@ -165,4 +172,33 @@ class AppPasswordController extends \OCP\AppFramework\OCSController { 'apppassword' => $newToken, ]); } + + /** + * Confirm the user password + * + * @NoAdminRequired + * @BruteForceProtection(action=sudo) + * + * @param string $password The password of the user + * + * @return DataResponse|DataResponse, array{}> + * + * 200: Password confirmation succeeded + * 403: Password confirmation failed + */ + #[UseSession] + public function confirmUserPassword(string $password): DataResponse { + $loginName = $this->userSession->getLoginName(); + $loginResult = $this->userManager->checkPassword($loginName, $password); + if ($loginResult === false) { + $response = new DataResponse([], Http::STATUS_FORBIDDEN); + $response->throttle(['loginName' => $loginName]); + return $response; + } + + $confirmTimestamp = time(); + $this->session->set('last-password-confirm', $confirmTimestamp); + $this->throttler->resetDelay($this->request->getRemoteAddress(), 'sudo', ['loginName' => $loginName]); + return new DataResponse(['lastLogin' => $confirmTimestamp], Http::STATUS_OK); + } } diff --git a/core/openapi.json b/core/openapi.json index 9bfee0d40b9..5a33f547a92 100644 --- a/core/openapi.json +++ b/core/openapi.json @@ -2475,6 +2475,113 @@ } } }, + "/ocs/v2.php/core/apppassword/confirm": { + "put": { + "operationId": "app_password-confirm-user-password", + "summary": "Confirm the user password", + "tags": [ + "app_password" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "password", + "in": "query", + "description": "The password of the user", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Password confirmation succeeded", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "lastLogin" + ], + "properties": { + "lastLogin": { + "type": "integer", + "format": "int64" + } + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Password confirmation failed", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/hovercard/v1/{userId}": { "get": { "operationId": "hover_card-get-user", diff --git a/core/routes.php b/core/routes.php index fe1fe6fcd75..9f8bc5c2c1e 100644 --- a/core/routes.php +++ b/core/routes.php @@ -123,6 +123,7 @@ $application->registerRoutes($this, [ ['root' => '/core', 'name' => 'AppPassword#getAppPassword', 'url' => '/getapppassword', 'verb' => 'GET'], ['root' => '/core', 'name' => 'AppPassword#rotateAppPassword', 'url' => '/apppassword/rotate', 'verb' => 'POST'], ['root' => '/core', 'name' => 'AppPassword#deleteAppPassword', 'url' => '/apppassword', 'verb' => 'DELETE'], + ['root' => '/core', 'name' => 'AppPassword#confirmUserPassword', 'url' => '/apppassword/confirm', 'verb' => 'PUT'], ['root' => '/hovercard', 'name' => 'HoverCard#getUser', 'url' => '/v1/{userId}', 'verb' => 'GET'], diff --git a/tests/Core/Controller/AppPasswordControllerTest.php b/tests/Core/Controller/AppPasswordControllerTest.php index 47220fcf5ab..5f257d163ed 100644 --- a/tests/Core/Controller/AppPasswordControllerTest.php +++ b/tests/Core/Controller/AppPasswordControllerTest.php @@ -29,6 +29,7 @@ use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Token\IProvider; use OC\Authentication\Token\IToken; use OC\Core\Controller\AppPasswordController; +use OC\User\Session; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\Authentication\Exceptions\CredentialsUnavailableException; @@ -38,6 +39,8 @@ use OCP\Authentication\LoginCredentials\IStore; use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; use OCP\ISession; +use OCP\IUserManager; +use OCP\Security\Bruteforce\IThrottler; use OCP\Security\ISecureRandom; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -61,6 +64,15 @@ class AppPasswordControllerTest extends TestCase { /** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */ private $eventDispatcher; + /** @var Session|MockObject */ + private $userSession; + + /** @var IUserManager|MockObject */ + private $userManager; + + /** @var IThrottler|MockObject */ + private $throttler; + /** @var AppPasswordController */ private $controller; @@ -73,6 +85,9 @@ class AppPasswordControllerTest extends TestCase { $this->credentialStore = $this->createMock(IStore::class); $this->request = $this->createMock(IRequest::class); $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->userSession = $this->createMock(Session::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->throttler = $this->createMock(IThrottler::class); $this->controller = new AppPasswordController( 'core', @@ -81,7 +96,10 @@ class AppPasswordControllerTest extends TestCase { $this->random, $this->tokenProvider, $this->credentialStore, - $this->eventDispatcher + $this->eventDispatcher, + $this->userSession, + $this->userManager, + $this->throttler ); } -- 2.39.5