tao-test/app/tao/models/classes/oauth/OauthService.php

288 lines
11 KiB
PHP

<?php
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2013 (original work) (update and modification) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*
*/
namespace oat\tao\model\oauth;
use common_http_Credentials;
use common_http_InvalidSignatureException;
use IMSGlobal\LTI\OAuth\OAuthSignatureMethod_HMAC_SHA1;
use IMSGlobal\LTI\OAuth\OAuthRequest;
use IMSGlobal\LTI\OAuth\OAuthServer;
use IMSGlobal\LTI\OAuth\OAuthUtil;
use oat\oatbox\service\ConfigurableService;
use common_http_Request;
use IMSGlobal\LTI\OAuth\OAuthException;
use oat\oatbox\service\exception\InvalidService;
use oat\oatbox\service\exception\InvalidServiceManagerException;
use oat\tao\model\oauth\lockout\LockOutException;
use oat\tao\model\oauth\lockout\LockoutInterface;
use Psr\Http\Message\ServerRequestInterface;
use tao_models_classes_oauth_Exception;
/**
* Oauth Services based on the TAO DataStore implementation
*
* @access public
* @author Joel Bout, <joel@taotesting.com>
* @package tao
*/
class OauthService extends ConfigurableService implements \common_http_SignatureService
{
public const SERVICE_ID = 'tao/OauthService';
public const OPTION_LOCKOUT_SERVICE = 'lockout';
public const OPTION_DATA_STORE = 'store';
protected const OAUTH_BODY_HASH_PARAM = 'oauth_body_hash';
protected const OAUTH_CONSUMER_KEY = 'oauth_consumer_key';
//oauth_consumer_secret
/**
* Adds a signature to the request
*
* @access public
*
* @param common_http_Request $request
* @param common_http_Credentials $credentials
* @param $authorizationHeader boolean Move the signature parameters into the Authorization header of the request
*
* @return common_http_Request
* @throws InvalidService
* @throws InvalidServiceManagerException
* @throws tao_models_classes_oauth_Exception
* @author Joel Bout, <joel@taotesting.com>
*/
public function sign(common_http_Request $request, common_http_Credentials $credentials, $authorizationHeader = false)
{
if (!$credentials instanceof \tao_models_classes_oauth_Credentials) {
throw new tao_models_classes_oauth_Exception('Invalid credentals: ' . gettype($credentials));
}
$oauthRequest = $this->getOauthRequest($request);
$dataStore = $this->getDataStore();
$consumer = $dataStore->getOauthConsumer($credentials);
$token = $dataStore->new_request_token($consumer);
$allInitialParameters = array_merge($request->getParams(), $request->getHeaders());
//oauth_body_hash is used for the signing computation
if ($authorizationHeader) {
// the signature should be computed from encoded versions
$oauthBodyHash = $this->calculateOauthBodyHash($request->getBody());
$allInitialParameters = array_merge(
$allInitialParameters,
[self::OAUTH_BODY_HASH_PARAM => $oauthBodyHash]
);
}
$signedRequest = OAuthRequest::from_consumer_and_token(
$consumer,
$token,
$oauthRequest->get_normalized_http_method(),
$oauthRequest->to_url(),
$allInitialParameters
);
$signature_method = new OAuthSignatureMethod_HMAC_SHA1();
$signedRequest->sign_request($signature_method, $consumer, $token);
//common_logger::d('Base string from TAO/Joel: '.$signedRequest->get_signature_base_string());
if ($authorizationHeader) {
$combinedParameters = $signedRequest->get_parameters();
$signatureParameters = array_diff_assoc($combinedParameters, $allInitialParameters);
// if $authorizationHeader is true then $oauthBodyHash should be defined
/** @noinspection PhpUndefinedVariableInspection */
$signatureParameters[self::OAUTH_BODY_HASH_PARAM] = $oauthBodyHash;
$signatureHeaders = ["Authorization" => $this->buildAuthorizationHeader($signatureParameters)];
$signedRequest = new common_http_Request(
$signedRequest->to_url(),
$signedRequest->get_normalized_http_method(),
$request->getParams(),
array_merge($signatureHeaders, $request->getHeaders()),
$request->getBody()
);
} else {
$signedRequest = new common_http_Request(
$signedRequest->to_url(),
$signedRequest->get_normalized_http_method(),
$signedRequest->get_parameters(),
$request->getHeaders(),
$request->getBody()
);
}
return $signedRequest;
}
/**
* Validates the signature of the current request
*
* @param common_http_Request $request
* @param common_http_Credentials|null $credentials
*
* @return array [OAuthConsumer, token]
* @throws InvalidService
* @throws InvalidServiceManagerException
* @throws common_http_InvalidSignatureException
* @throws LockOutException
* @author Joel Bout, <joel@taotesting.com>
*/
public function validate(common_http_Request $request, common_http_Credentials $credentials = null)
{
$server = new OAuthServer($this->getDataStore());
$method = new OAuthSignatureMethod_HMAC_SHA1();
$server->add_signature_method($method);
$oauthRequest = $this->getOauthRequest($request);
$oauthBodyHash = $oauthRequest->get_parameter(self::OAUTH_BODY_HASH_PARAM);
if ($oauthBodyHash !== null && !$this->validateBodyHash($request->getBody(), $oauthBodyHash)) {
throw new common_http_InvalidSignatureException('Validation failed: invalid body hash');
}
/** @var LockoutInterface $lockoutService */
$lockoutService = $this->getSubService(self::OPTION_LOCKOUT_SERVICE);
try {
if(!$lockoutService->isAllowed()){
throw new LockOutException('Blocked');
}
return $server->verify_request($oauthRequest);
} catch (OAuthException $e) {
/** @var LockoutInterface $lockoutService */
$lockoutService = $this->getSubService(self::OPTION_LOCKOUT_SERVICE);
$lockoutService->logFailedAttempt();
throw new common_http_InvalidSignatureException('Validation failed: ' . $e->getMessage());
}
}
/**
* Wrapper over parent validate method to support PSR Request object
*
* @param ServerRequestInterface $request
* @param common_http_Credentials|null $credentials
*
* @return array [OAuthConsumer, token]
* @throws InvalidService
* @throws InvalidServiceManagerException
* @throws common_http_InvalidSignatureException
*/
public function validatePsrRequest(ServerRequestInterface $request, common_http_Credentials $credentials = null)
{
$oldRequest = $this->buildCommonRequestFromPsr($request);
return $this->validate($oldRequest, $credentials);
}
/**
* @return ImsOauthDataStoreInterface
* @throws InvalidService
* @throws InvalidServiceManagerException
*/
public function getDataStore()
{
return $this->getSubService(self::OPTION_DATA_STORE);
}
/**
* @param ServerRequestInterface $request
* @return common_http_Request
*/
private function buildCommonRequestFromPsr(ServerRequestInterface $request)
{
$body = (string) $request->getBody();
// https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
$contentTypeHeaders = $request->getHeader('Content-Type');
$params = reset($contentTypeHeaders) === 'application/x-www-form-urlencoded'
? OAuthUtil::parse_parameters($body)
: [];
return new common_http_Request(
$request->getUri(),
$request->getMethod(),
$params,
$request->getHeaders(),
$body
);
}
/**
* Check if $bodyHash is valid hash for $body contents
* @param string $body
* @param string $bodyHash
* @return bool
*/
protected function validateBodyHash($body, $bodyHash)
{
// Check should be added here after ensuring it will not break existing LTI clients
// This method was initially added to be overwritten in \oat\taoLti\models\classes\Lis\LisOauthService
// where we need to perform real check
return true;
}
/**
* @param string $body
* @return string
*/
protected function calculateOauthBodyHash($body)
{
return base64_encode(sha1($body, true));
}
/**
* As per the OAuth body hashing specification, all of the OAuth parameters must be sent as part of the Authorization header.
* In particular, OAuth parameters from the request URL and POST body will be ignored.
* Return the Authorization header
*/
private function buildAuthorizationHeader($signatureParameters)
{
$authorizationHeader = 'OAuth realm=""';
foreach ($signatureParameters as $key => $value) {
$authorizationHeader .= ',' . $key . '=' . '"' . urlencode($value) . '"';
}
return $authorizationHeader;
}
/**
* Transform common_http_Request into an OAuth request
* @param common_http_Request $request
* @return OAuthRequest
*/
private function getOauthRequest(common_http_Request $request)
{
$params = [];
// In LTI launches oauth params are passed as POST params, but in LIS requests
// they located in Authorization header. We try to extract them for further verification
$authHeader = $request->getHeaderValue('Authorization');
if (!empty($authHeader)) {
$params = OAuthUtil::split_header($authHeader[0]);
}
$params = array_merge($params, $request->getParams());
\common_Logger::d('OAuth Request created:' . $request->getUrl() . ' using ' . $request->getMethod());
return new OAuthRequest($request->getMethod(), $request->getUrl(), $params);
}
}