@@ -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<Http::STATUS_OK, array{lastLogin: int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array<empty>, 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); | |||
} | |||
} |
@@ -43,6 +43,7 @@ use OC\User\Session; | |||
use OC_App; | |||
use OCP\AppFramework\Controller; | |||
use OCP\AppFramework\Http; | |||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired; | |||
use OCP\AppFramework\Http\Attribute\OpenAPI; | |||
use OCP\AppFramework\Http\Attribute\UseSession; | |||
use OCP\AppFramework\Http\DataResponse; | |||
@@ -61,7 +62,6 @@ use OCP\Notification\IManager; | |||
use OCP\Security\Bruteforce\IThrottler; | |||
use OCP\Util; | |||
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] | |||
class LoginController extends Controller { | |||
public const LOGIN_MSG_INVALIDPASSWORD = 'invalidpassword'; | |||
public const LOGIN_MSG_USERDISABLED = 'userdisabled'; | |||
@@ -126,6 +126,7 @@ class LoginController extends Controller { | |||
* @return TemplateResponse|RedirectResponse | |||
*/ | |||
#[UseSession] | |||
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] | |||
public function showLoginForm(string $user = null, string $redirect_url = null): Http\Response { | |||
if ($this->userSession->isLoggedIn()) { | |||
return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl()); | |||
@@ -274,6 +275,7 @@ class LoginController extends Controller { | |||
* @return RedirectResponse | |||
*/ | |||
#[UseSession] | |||
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] | |||
public function tryLogin(Chain $loginChain, | |||
string $user = '', | |||
string $password = '', | |||
@@ -352,13 +354,22 @@ class LoginController extends Controller { | |||
} | |||
/** | |||
* Confirm the user password | |||
* | |||
* @NoAdminRequired | |||
* @BruteForceProtection(action=sudo) | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* @param string $password The password of the user | |||
* | |||
* @return DataResponse<Http::STATUS_OK, array{lastLogin: int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array<empty>, array{}> | |||
* | |||
* 200: Password confirmation succeeded | |||
* 403: Password confirmation failed | |||
*/ | |||
#[UseSession] | |||
#[NoCSRFRequired] | |||
public function confirmPassword(string $password): DataResponse { | |||
$loginName = $this->userSession->getLoginName(); | |||
$loginResult = $this->userManager->checkPassword($loginName, $password); |
@@ -919,6 +919,63 @@ | |||
} | |||
} | |||
}, | |||
"/index.php/login/confirm": { | |||
"post": { | |||
"operationId": "login-confirm-password", | |||
"summary": "Confirm the user password", | |||
"tags": [ | |||
"login" | |||
], | |||
"security": [ | |||
{ | |||
"bearer_auth": [] | |||
}, | |||
{ | |||
"basic_auth": [] | |||
} | |||
], | |||
"parameters": [ | |||
{ | |||
"name": "password", | |||
"in": "query", | |||
"description": "The password of the user", | |||
"required": true, | |||
"schema": { | |||
"type": "string" | |||
} | |||
} | |||
], | |||
"responses": { | |||
"200": { | |||
"description": "Password confirmation succeeded", | |||
"content": { | |||
"application/json": { | |||
"schema": { | |||
"type": "object", | |||
"required": [ | |||
"lastLogin" | |||
], | |||
"properties": { | |||
"lastLogin": { | |||
"type": "integer", | |||
"format": "int64" | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}, | |||
"403": { | |||
"description": "Password confirmation failed", | |||
"content": { | |||
"application/json": { | |||
"schema": {} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}, | |||
"/index.php/login/v2/poll": { | |||
"post": { | |||
"operationId": "client_flow_login_v2-poll", | |||
@@ -2418,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", |
@@ -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'], | |||
@@ -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 | |||
); | |||
} | |||