2018-01-02 15:13:32 -05:00
< ? php
/**
* @ copyright 2018 , Roeland Jago Douma < roeland @ famdouma . nl >
*
2019-12-03 13:57:53 -05:00
* @ author Bjoern Schiessle < bjoern @ schiessle . org >
2018-01-02 15:13:32 -05:00
* @ author Roeland Jago Douma < roeland @ famdouma . nl >
*
* @ license GNU AGPL version 3 or any later version
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
2021-06-04 15:52:51 -04:00
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
2018-01-02 15:13:32 -05:00
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
2019-12-03 13:57:53 -05:00
* along with this program . If not , see < http :// www . gnu . org / licenses />.
2018-01-02 15:13:32 -05:00
*
*/
namespace OC\AppFramework\Middleware\Security ;
use OC\AppFramework\Middleware\Security\Exceptions\NotConfirmedException ;
use OC\AppFramework\Utility\ControllerMethodReflector ;
2024-03-01 12:37:47 -05:00
use OC\Authentication\Token\IProvider ;
2025-02-11 05:28:31 -05:00
use OC\User\Manager ;
2018-01-02 15:13:32 -05:00
use OCP\AppFramework\Controller ;
2023-04-24 11:13:18 -04:00
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired ;
2018-01-02 15:13:32 -05:00
use OCP\AppFramework\Middleware ;
use OCP\AppFramework\Utility\ITimeFactory ;
2024-03-01 12:37:47 -05:00
use OCP\Authentication\Exceptions\ExpiredTokenException ;
use OCP\Authentication\Exceptions\InvalidTokenException ;
use OCP\Authentication\Exceptions\WipeTokenException ;
2025-02-11 05:28:31 -05:00
use OCP\IRequest ;
2018-01-02 15:13:32 -05:00
use OCP\ISession ;
use OCP\IUserSession ;
2024-03-01 12:37:47 -05:00
use OCP\Session\Exceptions\SessionNotAvailableException ;
2018-10-11 15:56:24 -04:00
use OCP\User\Backend\IPasswordConfirmationBackend ;
2025-02-11 05:28:31 -05:00
use Psr\Log\LoggerInterface ;
2023-04-24 11:13:18 -04:00
use ReflectionMethod ;
2018-01-02 15:13:32 -05:00
class PasswordConfirmationMiddleware extends Middleware {
2025-02-11 05:28:31 -05:00
private array $excludedUserBackEnds = [ 'user_saml' => true , 'user_globalsiteselector' => true ];
2018-01-02 15:13:32 -05:00
2025-02-11 05:28:31 -05:00
public function __construct (
private ControllerMethodReflector $reflector ,
private ISession $session ,
private IUserSession $userSession ,
private ITimeFactory $timeFactory ,
private IProvider $tokenProvider ,
private LoggerInterface $logger ,
private IRequest $request ,
private Manager $userManager ,
2024-03-01 12:37:47 -05:00
) {
2018-01-02 15:13:32 -05:00
}
/**
* @ throws NotConfirmedException
*/
2025-02-11 05:28:31 -05:00
public function beforeController ( Controller $controller , string $methodName ) {
2023-04-24 11:13:18 -04:00
$reflectionMethod = new ReflectionMethod ( $controller , $methodName );
2025-02-11 05:28:31 -05:00
if ( ! $this -> needsPasswordConfirmation ( $reflectionMethod )) {
return ;
}
2018-10-11 15:56:24 -04:00
2025-02-11 05:28:31 -05:00
$user = $this -> userSession -> getUser ();
$backendClassName = '' ;
if ( $user !== null ) {
$backend = $user -> getBackend ();
if ( $backend instanceof IPasswordConfirmationBackend ) {
if ( ! $backend -> canConfirmPassword ( $user -> getUID ())) {
return ;
}
2018-01-02 15:13:32 -05:00
}
2025-02-11 05:28:31 -05:00
$backendClassName = $user -> getBackendClassName ();
}
try {
$sessionId = $this -> session -> getId ();
$token = $this -> tokenProvider -> getToken ( $sessionId );
} catch ( SessionNotAvailableException | InvalidTokenException | WipeTokenException | ExpiredTokenException ) {
// States we do not deal with here.
return ;
}
$scope = $token -> getScopeAsArray ();
if ( isset ( $scope [ 'password-unconfirmable' ]) && $scope [ 'password-unconfirmable' ] === true ) {
// Users logging in from SSO backends cannot confirm their password by design
return ;
}
if ( $this -> isPasswordConfirmationStrict ( $reflectionMethod )) {
$authHeader = $this -> request -> getHeader ( 'Authorization' );
[, $password ] = explode ( ':' , base64_decode ( substr ( $authHeader , 6 )), 2 );
$loginResult = $this -> userManager -> checkPassword ( $user -> getUid (), $password );
if ( $loginResult === false ) {
throw new NotConfirmedException ();
2024-03-01 12:37:47 -05:00
}
2025-02-11 05:28:31 -05:00
$this -> session -> set ( 'last-password-confirm' , $this -> timeFactory -> getTime ());
} else {
2018-01-02 15:13:32 -05:00
$lastConfirm = ( int ) $this -> session -> get ( 'last-password-confirm' );
2024-03-01 12:37:47 -05:00
// TODO: confirm excludedUserBackEnds can go away and remove it
2018-10-27 09:43:51 -04:00
if ( ! isset ( $this -> excludedUserBackEnds [ $backendClassName ]) && $lastConfirm < ( $this -> timeFactory -> getTime () - ( 30 * 60 + 15 ))) { // allow 15 seconds delay
2018-01-02 15:13:32 -05:00
throw new NotConfirmedException ();
}
}
}
2023-04-24 11:13:18 -04:00
2025-02-11 05:28:31 -05:00
private function needsPasswordConfirmation ( ReflectionMethod $reflectionMethod ) : bool {
$attributes = $reflectionMethod -> getAttributes ( PasswordConfirmationRequired :: class );
if ( ! empty ( $attributes )) {
2023-04-24 11:13:18 -04:00
return true ;
}
2025-02-11 05:28:31 -05:00
if ( $this -> reflector -> hasAnnotation ( 'PasswordConfirmationRequired' )) {
$this -> logger -> debug ( $reflectionMethod -> getDeclaringClass () -> getName () . '::' . $reflectionMethod -> getName () . ' uses the @' . 'PasswordConfirmationRequired' . ' annotation and should use the #[PasswordConfirmationRequired] attribute instead' );
2023-04-24 11:13:18 -04:00
return true ;
}
return false ;
}
2025-02-11 05:28:31 -05:00
private function isPasswordConfirmationStrict ( ReflectionMethod $reflectionMethod ) : bool {
$attributes = $reflectionMethod -> getAttributes ( PasswordConfirmationRequired :: class );
return ! empty ( $attributes ) && ( $attributes [ 0 ] -> newInstance () -> getStrict ());
}
2018-01-02 15:13:32 -05:00
}