2018-01-02 15:13:32 -05:00
< ? php
2025-06-30 09:04:05 -04:00
2018-01-02 15:13:32 -05:00
/**
2024-05-23 03:26:56 -04:00
* SPDX - FileCopyrightText : 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
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 ;
2024-11-27 11:01:25 -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 ;
2024-03-15 07:51:31 -04:00
use OCP\Authentication\Token\IToken ;
2024-11-27 11:01:25 -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 ;
2024-07-15 09:25:45 -04: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 {
2024-11-27 11:01:25 -05:00
private array $excludedUserBackEnds = [ 'user_saml' => true , 'user_globalsiteselector' => true ];
2018-01-02 15:13:32 -05:00
public function __construct (
2024-11-27 11:01:25 -05:00
private ControllerMethodReflector $reflector ,
private ISession $session ,
private IUserSession $userSession ,
private ITimeFactory $timeFactory ,
private IProvider $tokenProvider ,
2024-07-15 09:25:45 -04:00
private readonly LoggerInterface $logger ,
2024-11-27 11:01:25 -05:00
private readonly IRequest $request ,
private readonly Manager $userManager ,
2024-03-01 12:37:47 -05:00
) {
2018-01-02 15:13:32 -05:00
}
/**
* @ throws NotConfirmedException
*/
2024-11-27 11:01:25 -05:00
public function beforeController ( Controller $controller , string $methodName ) {
2023-04-24 11:13:18 -04:00
$reflectionMethod = new ReflectionMethod ( $controller , $methodName );
2024-11-27 11:01:25 -05:00
if ( ! $this -> needsPasswordConfirmation ( $reflectionMethod )) {
return ;
}
2018-10-11 15:56:24 -04:00
2024-11-27 11:01:25 -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
}
2024-11-27 11:01:25 -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 [ IToken :: SCOPE_SKIP_PASSWORD_VALIDATION ]) && $scope [ IToken :: SCOPE_SKIP_PASSWORD_VALIDATION ] === true ) {
// Users logging in from SSO backends cannot confirm their password by design
return ;
}
if ( $this -> isPasswordConfirmationStrict ( $reflectionMethod )) {
$authHeader = $this -> request -> getHeader ( 'Authorization' );
2025-06-23 12:13:39 -04:00
if ( ! str_starts_with ( strtolower ( $authHeader ), 'basic ' )) {
throw new NotConfirmedException ( 'Required authorization header missing' );
}
2024-11-27 11:01:25 -05:00
[, $password ] = explode ( ':' , base64_decode ( substr ( $authHeader , 6 )), 2 );
2025-04-02 09:50:05 -04:00
$loginName = $this -> session -> get ( 'loginname' );
$loginResult = $this -> userManager -> checkPassword ( $loginName , $password );
2024-11-27 11:01:25 -05:00
if ( $loginResult === false ) {
throw new NotConfirmedException ();
2024-03-01 12:37:47 -05:00
}
2024-11-27 11:01:25 -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
2024-11-27 11:01:25 -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 ;
}
2024-11-27 11:01:25 -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 ;
}
2024-11-27 11:01:25 -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
}