Skip to content

Commit

Permalink
Merge pull request #43668 from nextcloud/feat/core/expose-confirm-pas…
Browse files Browse the repository at this point in the history
…sword-endpoint
  • Loading branch information
provokateurin authored Feb 20, 2024
2 parents 7a21c2d + 6243a94 commit 19bff05
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 2 deletions.
36 changes: 36 additions & 0 deletions core/Controller/AppPasswordController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
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;
Expand All @@ -41,6 +43,8 @@
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 {
Expand All @@ -52,6 +56,9 @@ public function __construct(
private IProvider $tokenProvider,
private IStore $credentialStore,
private IEventDispatcher $eventDispatcher,
private Session $userSession,
private IUserManager $userManager,
private IThrottler $throttler,
) {
parent::__construct($appName, $request);
}
Expand Down Expand Up @@ -165,4 +172,33 @@ public function rotateAppPassword(): DataResponse {
'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);
}
}
13 changes: 12 additions & 1 deletion core/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
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;
Expand All @@ -61,7 +62,6 @@
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';
Expand Down Expand Up @@ -126,6 +126,7 @@ public function logout() {
* @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());
Expand Down Expand Up @@ -274,6 +275,7 @@ private function generateRedirect(?string $redirectUrl): RedirectResponse {
* @return RedirectResponse
*/
#[UseSession]
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
public function tryLogin(Chain $loginChain,
string $user = '',
string $password = '',
Expand Down Expand Up @@ -352,13 +354,22 @@ private function createLoginFailedResponse(
}

/**
* 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);
Expand Down
164 changes: 164 additions & 0 deletions core/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions core/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
['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'],

Expand Down
20 changes: 19 additions & 1 deletion tests/Core/Controller/AppPasswordControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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;
Expand All @@ -38,6 +39,8 @@
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;
Expand All @@ -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;

Expand All @@ -73,6 +85,9 @@ protected function setUp(): void {
$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',
Expand All @@ -81,7 +96,10 @@ protected function setUp(): void {
$this->random,
$this->tokenProvider,
$this->credentialStore,
$this->eventDispatcher
$this->eventDispatcher,
$this->userSession,
$this->userManager,
$this->throttler
);
}

Expand Down

0 comments on commit 19bff05

Please sign in to comment.