Passed
Pull Request — master (#1078)
by Pauli
04:56
created

AmpacheMiddleware::checkAuthentication()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 11
nc 6
nop 0
dl 0
loc 16
rs 9.2222
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Morris Jobke <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Morris Jobke 2013, 2014
12
 * @copyright Pauli Järvinen 2018 - 2023
13
 */
14
15
namespace OCA\Music\Middleware;
16
17
use OCP\IRequest;
18
use OCP\AppFramework\Controller;
19
use OCP\AppFramework\Middleware;
20
21
use OCA\Music\AppFramework\BusinessLayer\BusinessLayerException;
22
use OCA\Music\AppFramework\Core\Logger;
23
use OCA\Music\Controller\AmpacheController;
24
use OCA\Music\Db\AmpacheSession;
25
use OCA\Music\Db\AmpacheSessionMapper;
26
use OCA\Music\Db\AmpacheUserMapper;
27
use OCA\Music\Utility\Random;
28
use OCA\Music\Utility\Util;
29
30
/**
31
 * Handles the session management on Ampache login/logout.
32
 * Checks the authentication on each Ampache API call before the
33
 * request is allowed to be passed to AmpacheController.
34
 * Map identified exceptions from the controller to proper Ampache error results.
35
 */
36
class AmpacheMiddleware extends Middleware {
37
38
	const SESSION_EXPIRY_TIME = 6000;
39
40
	private $request;
41
	private $ampacheSessionMapper;
42
	private $ampacheUserMapper;
43
	private $logger;
44
45
	public function __construct(
46
			IRequest $request, AmpacheSessionMapper $ampacheSessionMapper, AmpacheUserMapper $ampacheUserMapper, Logger $logger) {
47
		$this->request = $request;
48
		$this->ampacheSessionMapper = $ampacheSessionMapper;
49
		$this->ampacheUserMapper = $ampacheUserMapper;
50
		$this->logger = $logger;
51
	}
52
53
	/**
54
	 * This runs all the security checks before a method call. The
55
	 * security checks are determined by inspecting the controller method
56
	 * annotations
57
	 *
58
	 * NOTE: Type declarations cannot be used on this function signature because that would be
59
	 * in conflict with the base class which is not in our hands.
60
	 *
61
	 * @param Controller $controller the controller that is being called
62
	 * @param string $methodName the name of the method
63
	 * @throws AmpacheException when a security check fails
64
	 */
65
	public function beforeController($controller, $methodName) {
66
		if ($controller instanceof AmpacheController) {
67
			if ($methodName === 'jsonApi') {
68
				$controller->setJsonMode(true);
69
			}
70
71
			// authenticate on 'handshake' and check the session token on any other action
72
			$action = $this->request->getParam('action');
73
			if ($action === 'handshake') {
74
				$this->handleHandshake($controller);
75
			} else {
76
				$this->handleNonHandshakeAction($controller, $action);
77
			}
78
		}
79
	}
80
	
81
	private function handleHandshake(AmpacheController $controller) : void {
82
		$user = $this->request->getParam('user');
83
		$timestamp = (int)$this->request->getParam('timestamp');
84
		$auth = $this->request->getParam('auth');
85
		$version = $this->request->getParam('version');
86
87
		$currentTime = \time();
88
		$expiryDate = $currentTime + self::SESSION_EXPIRY_TIME;
89
90
		$this->checkHandshakeTimestamp($timestamp, $currentTime);
91
		$apiKeyId = $this->checkHandshakeAuthentication($user, $timestamp, $auth);
92
		$session = $this->startNewSession($user, $expiryDate, $version, $apiKeyId);
93
		$controller->setSession($session);
94
	}
95
96
	private function checkHandshakeTimestamp(int $timestamp, int $currentTime) : void {
97
		if ($timestamp === 0) {
98
			throw new AmpacheException('Invalid Login - cannot parse time', 401);
99
		}
100
		if ($timestamp < ($currentTime - self::SESSION_EXPIRY_TIME)) {
101
			throw new AmpacheException('Invalid Login - session is outdated', 401);
102
		}
103
		// Allow the timestamp to be at maximum 10 minutes in the future. The client may use its
104
		// own system clock to generate the timestamp and that may differ from the server's time.
105
		if ($timestamp > $currentTime + 600) {
106
			throw new AmpacheException('Invalid Login - timestamp is in future', 401);
107
		}
108
	}
109
110
	private function checkHandshakeAuthentication(?string $user, int $timestamp, ?string $auth) : int {
111
		if ($user === null || $auth === null) {
112
			throw new AmpacheException('Invalid Login - required credentials missing', 401);
113
		}
114
115
		$hashes = $this->ampacheUserMapper->getPasswordHashes($user);
116
117
		foreach ($hashes as $keyId => $hash) {
118
			$expectedHash = \hash('sha256', $timestamp . $hash);
119
120
			if ($expectedHash === $auth) {
121
				return (int)$keyId;
122
			}
123
		}
124
125
		throw new AmpacheException('Invalid Login - passphrase does not match', 401);
126
	}
127
128
	private function startNewSession(string $user, int $expiryDate, ?string $apiVersion, int $apiKeyId) : AmpacheSession {
129
		// create new session
130
		$session = new AmpacheSession();
131
		$session->setUserId($user);
132
		$session->setToken(Random::secure(16));
133
		$session->setExpiry($expiryDate);
134
		$session->setApiVersion(Util::truncate($apiVersion, 16));
135
		$session->setAmpacheUserId($apiKeyId);
136
137
		// save session to the database
138
		$this->ampacheSessionMapper->insert($session);
0 ignored issues
show
Deprecated Code introduced by
The function OCA\Music\AppFramework\D...xtcloudMapper::insert() has been deprecated: 14.0.0 Move over to QBMapper ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

138
		/** @scrutinizer ignore-deprecated */ $this->ampacheSessionMapper->insert($session);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
139
140
		return $session;
141
	}
142
143
	private function handleNonHandshakeAction(AmpacheController $controller, ?string $action) : void {
144
		$token = $this->request->getParam('auth') ?: $this->request->getParam('ssid');
145
146
		// 'ping' is allowed without a session (but if session token is passed, then it has to be valid)
147
		if (!($action === 'ping' && empty($token))) {
148
			$session = $this->getExistingSession($token);
149
			$controller->setSession($session);
150
		}
151
152
		if ($action === 'goodbye') {
153
			$this->ampacheSessionMapper->delete($session);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $session does not seem to be defined for all execution paths leading up to this point.
Loading history...
Deprecated Code introduced by
The function OCA\Music\AppFramework\D...xtcloudMapper::delete() has been deprecated: 14.0.0 Move over to QBMapper ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

153
			/** @scrutinizer ignore-deprecated */ $this->ampacheSessionMapper->delete($session);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
154
		}
155
156
		if ($action === null) {
157
			throw new AmpacheException("Required argument 'action' missing", 400);
158
		}
159
	}
160
161
	private function getExistingSession(?string $token) : AmpacheSession {
162
		if (empty($token)) {
163
			throw new AmpacheException('Invalid Login - session token missing', 401);
164
		} else {
165
			try {
166
				// extend the session deadline on any authorized API call
167
				$this->ampacheSessionMapper->extend($token, \time() + self::SESSION_EXPIRY_TIME);
168
				return $this->ampacheSessionMapper->findByToken($token);
169
			} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
170
				throw new AmpacheException('Invalid Login - invalid session token', 401);
171
			}
172
		}
173
	}
174
175
	/**
176
	 * If an AmpacheException is being caught, the appropiate ampache
177
	 * exception response is rendered
178
	 *
179
	 * NOTE: Type declarations cannot be used on this function signature because that would be
180
	 * in conflict with the base class which is not in our hands.
181
	 *
182
	 * @param Controller $controller the controller that is being called
183
	 * @param string $methodName the name of the method that will be called on
184
	 *                           the controller
185
	 * @param \Exception $exception the thrown exception
186
	 * @throws \Exception the passed in exception if it wasn't handled
187
	 * @return \OCP\AppFramework\Http\Response object if the exception was handled
188
	 */
189
	public function afterException($controller, $methodName, \Exception $exception) {
190
		if ($controller instanceof AmpacheController) {
191
			if ($exception instanceof AmpacheException) {
192
				return $controller->ampacheErrorResponse($exception->getCode(), $exception->getMessage());
193
			} elseif ($exception instanceof BusinessLayerException) {
194
				return $controller->ampacheErrorResponse(404, 'Entity not found');
195
			}
196
		}
197
		throw $exception;
198
	}
199
200
}
201