Skip to content

Commit 19bff05

Browse files
Merge pull request #43668 from nextcloud/feat/core/expose-confirm-password-endpoint
2 parents 7a21c2d + 6243a94 commit 19bff05

5 files changed

Lines changed: 232 additions & 2 deletions

File tree

core/Controller/AppPasswordController.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
use OC\Authentication\Events\AppPasswordCreatedEvent;
3232
use OC\Authentication\Token\IProvider;
3333
use OC\Authentication\Token\IToken;
34+
use OC\User\Session;
3435
use OCP\AppFramework\Http;
36+
use OCP\AppFramework\Http\Attribute\UseSession;
3537
use OCP\AppFramework\Http\DataResponse;
3638
use OCP\AppFramework\OCS\OCSForbiddenException;
3739
use OCP\Authentication\Exceptions\CredentialsUnavailableException;
@@ -41,6 +43,8 @@
4143
use OCP\EventDispatcher\IEventDispatcher;
4244
use OCP\IRequest;
4345
use OCP\ISession;
46+
use OCP\IUserManager;
47+
use OCP\Security\Bruteforce\IThrottler;
4448
use OCP\Security\ISecureRandom;
4549

4650
class AppPasswordController extends \OCP\AppFramework\OCSController {
@@ -52,6 +56,9 @@ public function __construct(
5256
private IProvider $tokenProvider,
5357
private IStore $credentialStore,
5458
private IEventDispatcher $eventDispatcher,
59+
private Session $userSession,
60+
private IUserManager $userManager,
61+
private IThrottler $throttler,
5562
) {
5663
parent::__construct($appName, $request);
5764
}
@@ -165,4 +172,33 @@ public function rotateAppPassword(): DataResponse {
165172
'apppassword' => $newToken,
166173
]);
167174
}
175+
176+
/**
177+
* Confirm the user password
178+
*
179+
* @NoAdminRequired
180+
* @BruteForceProtection(action=sudo)
181+
*
182+
* @param string $password The password of the user
183+
*
184+
* @return DataResponse<Http::STATUS_OK, array{lastLogin: int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array<empty>, array{}>
185+
*
186+
* 200: Password confirmation succeeded
187+
* 403: Password confirmation failed
188+
*/
189+
#[UseSession]
190+
public function confirmUserPassword(string $password): DataResponse {
191+
$loginName = $this->userSession->getLoginName();
192+
$loginResult = $this->userManager->checkPassword($loginName, $password);
193+
if ($loginResult === false) {
194+
$response = new DataResponse([], Http::STATUS_FORBIDDEN);
195+
$response->throttle(['loginName' => $loginName]);
196+
return $response;
197+
}
198+
199+
$confirmTimestamp = time();
200+
$this->session->set('last-password-confirm', $confirmTimestamp);
201+
$this->throttler->resetDelay($this->request->getRemoteAddress(), 'sudo', ['loginName' => $loginName]);
202+
return new DataResponse(['lastLogin' => $confirmTimestamp], Http::STATUS_OK);
203+
}
168204
}

core/Controller/LoginController.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
use OC_App;
4444
use OCP\AppFramework\Controller;
4545
use OCP\AppFramework\Http;
46+
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
4647
use OCP\AppFramework\Http\Attribute\OpenAPI;
4748
use OCP\AppFramework\Http\Attribute\UseSession;
4849
use OCP\AppFramework\Http\DataResponse;
@@ -61,7 +62,6 @@
6162
use OCP\Security\Bruteforce\IThrottler;
6263
use OCP\Util;
6364

64-
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
6565
class LoginController extends Controller {
6666
public const LOGIN_MSG_INVALIDPASSWORD = 'invalidpassword';
6767
public const LOGIN_MSG_USERDISABLED = 'userdisabled';
@@ -126,6 +126,7 @@ public function logout() {
126126
* @return TemplateResponse|RedirectResponse
127127
*/
128128
#[UseSession]
129+
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
129130
public function showLoginForm(string $user = null, string $redirect_url = null): Http\Response {
130131
if ($this->userSession->isLoggedIn()) {
131132
return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl());
@@ -274,6 +275,7 @@ private function generateRedirect(?string $redirectUrl): RedirectResponse {
274275
* @return RedirectResponse
275276
*/
276277
#[UseSession]
278+
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
277279
public function tryLogin(Chain $loginChain,
278280
string $user = '',
279281
string $password = '',
@@ -352,13 +354,22 @@ private function createLoginFailedResponse(
352354
}
353355

354356
/**
357+
* Confirm the user password
358+
*
355359
* @NoAdminRequired
356360
* @BruteForceProtection(action=sudo)
357361
*
358362
* @license GNU AGPL version 3 or any later version
359363
*
364+
* @param string $password The password of the user
365+
*
366+
* @return DataResponse<Http::STATUS_OK, array{lastLogin: int}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array<empty>, array{}>
367+
*
368+
* 200: Password confirmation succeeded
369+
* 403: Password confirmation failed
360370
*/
361371
#[UseSession]
372+
#[NoCSRFRequired]
362373
public function confirmPassword(string $password): DataResponse {
363374
$loginName = $this->userSession->getLoginName();
364375
$loginResult = $this->userManager->checkPassword($loginName, $password);

core/openapi.json

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,63 @@
919919
}
920920
}
921921
},
922+
"/index.php/login/confirm": {
923+
"post": {
924+
"operationId": "login-confirm-password",
925+
"summary": "Confirm the user password",
926+
"tags": [
927+
"login"
928+
],
929+
"security": [
930+
{
931+
"bearer_auth": []
932+
},
933+
{
934+
"basic_auth": []
935+
}
936+
],
937+
"parameters": [
938+
{
939+
"name": "password",
940+
"in": "query",
941+
"description": "The password of the user",
942+
"required": true,
943+
"schema": {
944+
"type": "string"
945+
}
946+
}
947+
],
948+
"responses": {
949+
"200": {
950+
"description": "Password confirmation succeeded",
951+
"content": {
952+
"application/json": {
953+
"schema": {
954+
"type": "object",
955+
"required": [
956+
"lastLogin"
957+
],
958+
"properties": {
959+
"lastLogin": {
960+
"type": "integer",
961+
"format": "int64"
962+
}
963+
}
964+
}
965+
}
966+
}
967+
},
968+
"403": {
969+
"description": "Password confirmation failed",
970+
"content": {
971+
"application/json": {
972+
"schema": {}
973+
}
974+
}
975+
}
976+
}
977+
}
978+
},
922979
"/index.php/login/v2/poll": {
923980
"post": {
924981
"operationId": "client_flow_login_v2-poll",
@@ -2418,6 +2475,113 @@
24182475
}
24192476
}
24202477
},
2478+
"/ocs/v2.php/core/apppassword/confirm": {
2479+
"put": {
2480+
"operationId": "app_password-confirm-user-password",
2481+
"summary": "Confirm the user password",
2482+
"tags": [
2483+
"app_password"
2484+
],
2485+
"security": [
2486+
{
2487+
"bearer_auth": []
2488+
},
2489+
{
2490+
"basic_auth": []
2491+
}
2492+
],
2493+
"parameters": [
2494+
{
2495+
"name": "password",
2496+
"in": "query",
2497+
"description": "The password of the user",
2498+
"required": true,
2499+
"schema": {
2500+
"type": "string"
2501+
}
2502+
},
2503+
{
2504+
"name": "OCS-APIRequest",
2505+
"in": "header",
2506+
"description": "Required to be true for the API request to pass",
2507+
"required": true,
2508+
"schema": {
2509+
"type": "boolean",
2510+
"default": true
2511+
}
2512+
}
2513+
],
2514+
"responses": {
2515+
"200": {
2516+
"description": "Password confirmation succeeded",
2517+
"content": {
2518+
"application/json": {
2519+
"schema": {
2520+
"type": "object",
2521+
"required": [
2522+
"ocs"
2523+
],
2524+
"properties": {
2525+
"ocs": {
2526+
"type": "object",
2527+
"required": [
2528+
"meta",
2529+
"data"
2530+
],
2531+
"properties": {
2532+
"meta": {
2533+
"$ref": "#/components/schemas/OCSMeta"
2534+
},
2535+
"data": {
2536+
"type": "object",
2537+
"required": [
2538+
"lastLogin"
2539+
],
2540+
"properties": {
2541+
"lastLogin": {
2542+
"type": "integer",
2543+
"format": "int64"
2544+
}
2545+
}
2546+
}
2547+
}
2548+
}
2549+
}
2550+
}
2551+
}
2552+
}
2553+
},
2554+
"403": {
2555+
"description": "Password confirmation failed",
2556+
"content": {
2557+
"application/json": {
2558+
"schema": {
2559+
"type": "object",
2560+
"required": [
2561+
"ocs"
2562+
],
2563+
"properties": {
2564+
"ocs": {
2565+
"type": "object",
2566+
"required": [
2567+
"meta",
2568+
"data"
2569+
],
2570+
"properties": {
2571+
"meta": {
2572+
"$ref": "#/components/schemas/OCSMeta"
2573+
},
2574+
"data": {}
2575+
}
2576+
}
2577+
}
2578+
}
2579+
}
2580+
}
2581+
}
2582+
}
2583+
}
2584+
},
24212585
"/ocs/v2.php/hovercard/v1/{userId}": {
24222586
"get": {
24232587
"operationId": "hover_card-get-user",

core/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
['root' => '/core', 'name' => 'AppPassword#getAppPassword', 'url' => '/getapppassword', 'verb' => 'GET'],
124124
['root' => '/core', 'name' => 'AppPassword#rotateAppPassword', 'url' => '/apppassword/rotate', 'verb' => 'POST'],
125125
['root' => '/core', 'name' => 'AppPassword#deleteAppPassword', 'url' => '/apppassword', 'verb' => 'DELETE'],
126+
['root' => '/core', 'name' => 'AppPassword#confirmUserPassword', 'url' => '/apppassword/confirm', 'verb' => 'PUT'],
126127

127128
['root' => '/hovercard', 'name' => 'HoverCard#getUser', 'url' => '/v1/{userId}', 'verb' => 'GET'],
128129

tests/Core/Controller/AppPasswordControllerTest.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use OC\Authentication\Token\IProvider;
3030
use OC\Authentication\Token\IToken;
3131
use OC\Core\Controller\AppPasswordController;
32+
use OC\User\Session;
3233
use OCP\AppFramework\Http\DataResponse;
3334
use OCP\AppFramework\OCS\OCSForbiddenException;
3435
use OCP\Authentication\Exceptions\CredentialsUnavailableException;
@@ -38,6 +39,8 @@
3839
use OCP\EventDispatcher\IEventDispatcher;
3940
use OCP\IRequest;
4041
use OCP\ISession;
42+
use OCP\IUserManager;
43+
use OCP\Security\Bruteforce\IThrottler;
4144
use OCP\Security\ISecureRandom;
4245
use PHPUnit\Framework\MockObject\MockObject;
4346
use Test\TestCase;
@@ -61,6 +64,15 @@ class AppPasswordControllerTest extends TestCase {
6164
/** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */
6265
private $eventDispatcher;
6366

67+
/** @var Session|MockObject */
68+
private $userSession;
69+
70+
/** @var IUserManager|MockObject */
71+
private $userManager;
72+
73+
/** @var IThrottler|MockObject */
74+
private $throttler;
75+
6476
/** @var AppPasswordController */
6577
private $controller;
6678

@@ -73,6 +85,9 @@ protected function setUp(): void {
7385
$this->credentialStore = $this->createMock(IStore::class);
7486
$this->request = $this->createMock(IRequest::class);
7587
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
88+
$this->userSession = $this->createMock(Session::class);
89+
$this->userManager = $this->createMock(IUserManager::class);
90+
$this->throttler = $this->createMock(IThrottler::class);
7691

7792
$this->controller = new AppPasswordController(
7893
'core',
@@ -81,7 +96,10 @@ protected function setUp(): void {
8196
$this->random,
8297
$this->tokenProvider,
8398
$this->credentialStore,
84-
$this->eventDispatcher
99+
$this->eventDispatcher,
100+
$this->userSession,
101+
$this->userManager,
102+
$this->throttler
85103
);
86104
}
87105

0 commit comments

Comments
 (0)