Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 79 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
OauthService | |
0.00% |
0 / 79 |
|
0.00% |
0 / 9 |
380 | |
0.00% |
0 / 1 |
sign | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
20 | |||
validate | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
validatePsrRequest | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getDataStore | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildCommonRequestFromPsr | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
validateBodyHash | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
calculateOauthBodyHash | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
buildAuthorizationHeader | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getOauthRequest | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * This program is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU General Public License |
6 | * as published by the Free Software Foundation; under version 2 |
7 | * of the License (non-upgradable). |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License |
15 | * along with this program; if not, write to the Free Software |
16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * |
18 | * Copyright (c) 2013 (original work) (update and modification) Open Assessment Technologies SA |
19 | * (under the project TAO-PRODUCT); |
20 | */ |
21 | |
22 | namespace oat\tao\model\oauth; |
23 | |
24 | use common_http_Credentials; |
25 | use common_http_InvalidSignatureException; |
26 | use IMSGlobal\LTI\OAuth\OAuthSignatureMethod_HMAC_SHA1; |
27 | use IMSGlobal\LTI\OAuth\OAuthRequest; |
28 | use IMSGlobal\LTI\OAuth\OAuthServer; |
29 | use IMSGlobal\LTI\OAuth\OAuthUtil; |
30 | use oat\oatbox\service\ConfigurableService; |
31 | use common_http_Request; |
32 | use IMSGlobal\LTI\OAuth\OAuthException; |
33 | use oat\oatbox\service\exception\InvalidService; |
34 | use oat\oatbox\service\exception\InvalidServiceManagerException; |
35 | use oat\tao\model\oauth\lockout\LockOutException; |
36 | use oat\tao\model\oauth\lockout\LockoutInterface; |
37 | use Psr\Http\Message\ServerRequestInterface; |
38 | use tao_models_classes_oauth_Exception; |
39 | |
40 | /** |
41 | * Oauth Services based on the TAO DataStore implementation |
42 | * |
43 | * @access public |
44 | * @author Joel Bout, <joel@taotesting.com> |
45 | * @package tao |
46 | */ |
47 | class OauthService extends ConfigurableService implements \common_http_SignatureService |
48 | { |
49 | public const SERVICE_ID = 'tao/OauthService'; |
50 | |
51 | public const OPTION_LOCKOUT_SERVICE = 'lockout'; |
52 | public const OPTION_DATA_STORE = 'store'; |
53 | |
54 | protected const OAUTH_BODY_HASH_PARAM = 'oauth_body_hash'; |
55 | protected const OAUTH_CONSUMER_KEY = 'oauth_consumer_key'; |
56 | |
57 | //oauth_consumer_secret |
58 | |
59 | /** |
60 | * Adds a signature to the request |
61 | * |
62 | * @access public |
63 | * |
64 | * @param common_http_Request $request |
65 | * @param common_http_Credentials $credentials |
66 | * @param boolean $authorizationHeader Move the signature parameters into the Authorization header of the request |
67 | * |
68 | * @return common_http_Request |
69 | * @throws InvalidService |
70 | * @throws InvalidServiceManagerException |
71 | * @throws tao_models_classes_oauth_Exception |
72 | * @author Joel Bout, <joel@taotesting.com> |
73 | */ |
74 | public function sign( |
75 | common_http_Request $request, |
76 | common_http_Credentials $credentials, |
77 | $authorizationHeader = false |
78 | ) { |
79 | |
80 | if (!$credentials instanceof \tao_models_classes_oauth_Credentials) { |
81 | throw new tao_models_classes_oauth_Exception('Invalid credentals: ' . gettype($credentials)); |
82 | } |
83 | |
84 | $oauthRequest = $this->getOauthRequest($request); |
85 | $dataStore = $this->getDataStore(); |
86 | $consumer = $dataStore->getOauthConsumer($credentials); |
87 | $token = $dataStore->new_request_token($consumer); |
88 | |
89 | $allInitialParameters = $request->getParams(); |
90 | //oauth_body_hash is used for the signing computation |
91 | if ($authorizationHeader) { |
92 | // the signature should be computed from encoded versions |
93 | $allInitialParameters[self::OAUTH_BODY_HASH_PARAM] = $this->calculateOauthBodyHash($request->getBody()); |
94 | } |
95 | |
96 | $signedRequest = OAuthRequest::from_consumer_and_token( |
97 | $consumer, |
98 | $token, |
99 | $oauthRequest->get_normalized_http_method(), |
100 | $oauthRequest->to_url(), |
101 | $allInitialParameters |
102 | ); |
103 | $signature_method = new OAuthSignatureMethod_HMAC_SHA1(); |
104 | $signedRequest->sign_request($signature_method, $consumer, $token); |
105 | //common_logger::d('Base string from TAO/Joel: '.$signedRequest->get_signature_base_string()); |
106 | |
107 | if ($authorizationHeader) { |
108 | $signatureHeaders = ["Authorization" => $this->buildAuthorizationHeader($signedRequest)]; |
109 | $signedRequest = new common_http_Request( |
110 | $request->getUrl(), |
111 | $signedRequest->get_normalized_http_method(), |
112 | $request->getParams(), |
113 | array_merge($signatureHeaders, $request->getHeaders()), |
114 | $request->getBody() |
115 | ); |
116 | } else { |
117 | $signedRequest = new common_http_Request( |
118 | $signedRequest->to_url(), |
119 | $signedRequest->get_normalized_http_method(), |
120 | $signedRequest->get_parameters(), |
121 | $request->getHeaders(), |
122 | $request->getBody() |
123 | ); |
124 | } |
125 | |
126 | return $signedRequest; |
127 | } |
128 | |
129 | /** |
130 | * Validates the signature of the current request |
131 | * |
132 | * @param common_http_Request $request |
133 | * @param common_http_Credentials|null $credentials |
134 | * |
135 | * @return array [OAuthConsumer, token] |
136 | * @throws InvalidService |
137 | * @throws InvalidServiceManagerException |
138 | * @throws common_http_InvalidSignatureException |
139 | * @throws LockOutException |
140 | * @author Joel Bout, <joel@taotesting.com> |
141 | */ |
142 | public function validate(common_http_Request $request, common_http_Credentials $credentials = null) |
143 | { |
144 | $server = new OAuthServer($this->getDataStore()); |
145 | $method = new OAuthSignatureMethod_HMAC_SHA1(); |
146 | $server->add_signature_method($method); |
147 | |
148 | $oauthRequest = $this->getOauthRequest($request); |
149 | $oauthBodyHash = $oauthRequest->get_parameter(self::OAUTH_BODY_HASH_PARAM); |
150 | if ($oauthBodyHash !== null && !$this->validateBodyHash($request->getBody(), $oauthBodyHash)) { |
151 | throw new common_http_InvalidSignatureException('Validation failed: invalid body hash'); |
152 | } |
153 | /** @var LockoutInterface $lockoutService */ |
154 | $lockoutService = $this->getSubService(self::OPTION_LOCKOUT_SERVICE); |
155 | try { |
156 | if (!$lockoutService->isAllowed()) { |
157 | throw new LockOutException('Blocked'); |
158 | } |
159 | return $server->verify_request($oauthRequest); |
160 | } catch (OAuthException $e) { |
161 | |
162 | /** @var LockoutInterface $lockoutService */ |
163 | $lockoutService = $this->getSubService(self::OPTION_LOCKOUT_SERVICE); |
164 | $lockoutService->logFailedAttempt(); |
165 | |
166 | throw new common_http_InvalidSignatureException('Validation failed: ' . $e->getMessage()); |
167 | } |
168 | } |
169 | |
170 | /** |
171 | * Wrapper over parent validate method to support PSR Request object |
172 | * |
173 | * @param ServerRequestInterface $request |
174 | * @param common_http_Credentials|null $credentials |
175 | * |
176 | * @return array [OAuthConsumer, token] |
177 | * @throws InvalidService |
178 | * @throws InvalidServiceManagerException |
179 | * @throws common_http_InvalidSignatureException |
180 | */ |
181 | public function validatePsrRequest(ServerRequestInterface $request, common_http_Credentials $credentials = null) |
182 | { |
183 | $oldRequest = $this->buildCommonRequestFromPsr($request); |
184 | return $this->validate($oldRequest, $credentials); |
185 | } |
186 | |
187 | /** |
188 | * @return ImsOauthDataStoreInterface |
189 | * @throws InvalidService |
190 | * @throws InvalidServiceManagerException |
191 | */ |
192 | public function getDataStore() |
193 | { |
194 | return $this->getSubService(self::OPTION_DATA_STORE); |
195 | } |
196 | |
197 | /** |
198 | * @param ServerRequestInterface $request |
199 | * @return common_http_Request |
200 | */ |
201 | private function buildCommonRequestFromPsr(ServerRequestInterface $request) |
202 | { |
203 | $body = (string) $request->getBody(); |
204 | // https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1 |
205 | $contentTypeHeaders = $request->getHeader('Content-Type'); |
206 | $params = reset($contentTypeHeaders) === 'application/x-www-form-urlencoded' |
207 | ? OAuthUtil::parse_parameters($body) |
208 | : []; |
209 | |
210 | return new common_http_Request( |
211 | $request->getUri(), |
212 | $request->getMethod(), |
213 | $params, |
214 | $request->getHeaders(), |
215 | $body |
216 | ); |
217 | } |
218 | |
219 | /** |
220 | * Check if $bodyHash is valid hash for $body contents |
221 | * @param string $body |
222 | * @param string $bodyHash |
223 | * @return bool |
224 | */ |
225 | protected function validateBodyHash($body, $bodyHash) |
226 | { |
227 | // Check should be added here after ensuring it will not break existing LTI clients |
228 | // This method was initially added to be overwritten in \oat\taoLti\models\classes\Lis\LisOauthService |
229 | // where we need to perform real check |
230 | return true; |
231 | } |
232 | |
233 | /** |
234 | * @param string $body |
235 | * @return string |
236 | */ |
237 | protected function calculateOauthBodyHash($body) |
238 | { |
239 | return base64_encode(sha1($body, true)); |
240 | } |
241 | |
242 | /** |
243 | * As per the OAuth body hashing specification, all of the OAuth parameters must be sent as part of the |
244 | * Authorization header. |
245 | * In particular, OAuth parameters from the request URL and POST body will be ignored. |
246 | * Return the Authorization header |
247 | */ |
248 | private function buildAuthorizationHeader(OAuthRequest $signedRequest) |
249 | { |
250 | $headerPrefix = 'Authorization: '; |
251 | $authorizationHeader = $signedRequest->to_header(); |
252 | if (mb_strpos($authorizationHeader, $headerPrefix) === 0) { |
253 | $authorizationHeader = mb_substr($authorizationHeader, mb_strlen($headerPrefix)); |
254 | } |
255 | |
256 | return $authorizationHeader; |
257 | } |
258 | |
259 | /** |
260 | * Transform common_http_Request into an OAuth request |
261 | * @param common_http_Request $request |
262 | * @return OAuthRequest |
263 | */ |
264 | private function getOauthRequest(common_http_Request $request) |
265 | { |
266 | $params = []; |
267 | |
268 | // In LTI launches oauth params are passed as POST params, but in LIS requests |
269 | // they located in Authorization header. We try to extract them for further verification |
270 | $authHeader = $request->getHeaderValue('Authorization'); |
271 | if (!empty($authHeader)) { |
272 | $params = OAuthUtil::split_header($authHeader[0]); |
273 | } |
274 | |
275 | $params = array_merge($params, $request->getParams()); |
276 | |
277 | \common_Logger::d('OAuth Request created:' . $request->getUrl() . ' using ' . $request->getMethod()); |
278 | |
279 | return new OAuthRequest($request->getMethod(), $request->getUrl(), $params); |
280 | } |
281 | } |