00001 <?php
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00020 final class OpenIdConsumer
00021 {
00022 const DIFFIE_HELLMAN_P = '155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443';
00023 const DIFFIE_HELLMAN_G = 2;
00024 const ASSOCIATION_TYPE = 'HMAC-SHA1';
00025 const NAMESPACE_2_0 = 'http://specs.openid.net/auth/2.0';
00026
00027 private $extensions = array();
00028
00029 private $randomSource = null;
00030 private $numberFactory = null;
00031
00035 private $httpClient = null;
00036
00037 public function __construct(
00038 RandomSource $randomSource,
00039 BigNumberFactory $numberFactory,
00040 HttpClient $httpClient
00041 )
00042 {
00043 $this->randomSource = $randomSource;
00044 $this->numberFactory = $numberFactory;
00045 $this->httpClient = $httpClient;
00046 }
00047
00051 public static function create(
00052 RandomSource $randomSource,
00053 BigNumberFactory $numberFactory,
00054 HttpClient $httpClient
00055 )
00056 {
00057 return new self($randomSource, $numberFactory, $httpClient);
00058 }
00059
00067 public function associate(
00068 HttpUrl $server,
00069 OpenIdConsumerAssociationManager $manager
00070 )
00071 {
00072 Assert::isTrue($server->isValid());
00073
00074 if ($association = $manager->findByServer($server))
00075 return $association;
00076
00077 $dhParameters = new DiffieHellmanParameters(
00078 $this->numberFactory->makeNumber(self::DIFFIE_HELLMAN_G),
00079 $this->numberFactory->makeNumber(self::DIFFIE_HELLMAN_P)
00080 );
00081
00082 $keyPair = DiffieHellmanKeyPair::generate(
00083 $dhParameters,
00084 $this->randomSource
00085 );
00086
00087 $request = HttpRequest::create()->
00088 setMethod(HttpMethod::post())->
00089 setUrl($server)->
00090 setPostVar('openid.ns', self::NAMESPACE_2_0)->
00091 setPostVar('openid.mode', 'associate')->
00092 setPostVar('openid.assoc_type', self::ASSOCIATION_TYPE)->
00093 setPostVar('openid.session_type', 'DH-SHA1')->
00094 setPostVar(
00095 'openid.dh_modulus',
00096 base64_encode($dhParameters->getModulus()->toBinary())
00097 )->
00098 setPostVar(
00099 'openid.dh_gen',
00100 base64_encode($dhParameters->getGen()->toBinary())
00101 )->
00102 setPostVar(
00103 'openid.dh_consumer_public',
00104 base64_encode($keyPair->getPublic()->toBinary())
00105 );
00106
00107 $response = $this->httpClient->
00108 setFollowLocation(true)->
00109 send($request);
00110
00111 if ($response->getStatus()->getId() != HttpStatus::CODE_200)
00112 throw new OpenIdException('bad response code from server');
00113
00114 $result = $this->parseKeyValueFormat($response->getBody());
00115
00116 if (empty($result['assoc_handle']))
00117 throw new OpenIdException('can\t live without handle');
00118
00119 if (
00120 !isset($result['assoc_type'])
00121 || $result['assoc_type'] !== self::ASSOCIATION_TYPE
00122 )
00123 throw new OpenIdException('bad association type');
00124
00125 if (
00126 !isset($result['expires_in'])
00127 || !is_numeric($result['expires_in'])
00128 )
00129 throw new OpenIdException('bad expires');
00130
00131 if (
00132 isset($result['session_type'])
00133 && $result['session_type'] == 'DH-SHA1'
00134 && isset($result['dh_server_public'])
00135 ) {
00136 $secret =
00137 sha1(
00138 $keyPair->
00139 makeSharedKey(
00140 $this->numberFactory->makeFromBinary(
00141 base64_decode($result['dh_server_public'])
00142 )
00143 )->
00144 toBinary(),
00145 true
00146 )
00147 ^ base64_decode($result['enc_mac_key']);
00148 } elseif (
00149 empty($result['session_type'])
00150 && isset($result['mac_key'])
00151 ) {
00152 $secret = base64_decode($result['mac_key']);
00153 } else {
00154 throw new OpenIdException('no secret in answer');
00155 }
00156
00157 return $manager->makeAndSave(
00158 $result['assoc_handle'],
00159 $result['assoc_type'],
00160 $secret,
00161 Timestamp::makeNow()->
00162 modify('+ '.$result['expires_in'].' seconds'),
00163 $server
00164 );
00165 }
00166
00167 private function makeCheckIdRequest(
00168 OpenIdCredentials $credentials,
00169 HttpUrl $returnTo,
00170 $trustRoot = null,
00171 $association = null
00172 )
00173 {
00174 Assert::isTrue($returnTo->isValid());
00175
00176 $view = RedirectView::create(
00177 $credentials->getServer()->toString()
00178 );
00179
00180 $model = Model::create()->
00181 set(
00182 'openid.ns',
00183 self::NAMESPACE_2_0
00184 )->
00185 set(
00186 'openid.identity',
00187 $credentials->getRealId()->toString()
00188 )->
00189 set(
00190 'openid.return_to',
00191 $returnTo->toString()
00192 )->
00193 set(
00194 'openid.claimed_id',
00195 $credentials->getRealId()->toString()
00196 );
00197
00198 foreach ($this->extensions as $extension) {
00199 $extension->addParamsToModel($model);
00200 }
00201
00202 if ($association) {
00203 Assert::isTrue(
00204 $association instanceof OpenIdConsumerAssociation
00205 && $association->getServer()->toString()
00206 == $credentials->getServer()->toString()
00207 );
00208
00209 $model->set(
00210 'openid.assoc_handle',
00211 $association->getHandle()
00212 );
00213 }
00214
00215 if ($trustRoot) {
00216 Assert::isTrue(
00217 $trustRoot instanceof HttpUrl
00218 && $trustRoot->isValid()
00219 );
00220
00221 $model->
00222 set(
00223 'openid.trust_root',
00224 $trustRoot->toString()
00225 )->
00226 set(
00227 'openid.realm',
00228 $trustRoot->toString()
00229 );
00230 }
00231
00232 return ModelAndView::create()->setModel($model)->setView($view);
00233 }
00234
00244 public function checkIdImmediate(
00245 OpenIdCredentials $credentials,
00246 HttpUrl $returnTo,
00247 $trustRoot = null,
00248 $association = null
00249 )
00250 {
00251 $mav = $this->makeCheckIdRequest(
00252 $credentials,
00253 $returnTo,
00254 $trustRoot,
00255 $association
00256 );
00257
00258 $mav->getModel()->
00259 set('openid.mode', 'checkid_immediate');
00260
00261 return $mav;
00262 }
00263
00273 public function checkIdSetup(
00274 OpenIdCredentials $credentials,
00275 HttpUrl $returnTo,
00276 $trustRoot = null,
00277 $association = null
00278 )
00279 {
00280 $mav = $this->makeCheckIdRequest(
00281 $credentials,
00282 $returnTo,
00283 $trustRoot,
00284 $association
00285 );
00286
00287 $mav->getModel()->
00288 set('openid.mode', 'checkid_setup');
00289
00290 return $mav;
00291 }
00292
00299 public function doContinue(HttpRequest $request, $manager = null)
00300 {
00301 if ($manager)
00302 Assert::isTrue($manager instanceof OpenIdConsumerAssociationManager);
00303
00304 $parameters = $this->parseGetParameters($request->getGet());
00305
00306
00307 foreach ($this->extensions as $extension) {
00308 $extension->parseResponce($request, $parameters);
00309 }
00310
00311 if (!isset($parameters['openid.mode']))
00312 throw new WrongArgumentException('not an openid request');
00313
00314 if ($parameters['openid.mode'] == 'id_res') {
00315 if (isset($parameters['openid.user_setup_url'])) {
00316 $setupUrl = HttpUrl::create()->parse(
00317 $parameters['openid.user_setup_url']
00318 );
00319
00320 Assert::isTrue($setupUrl->isValid());
00321
00322 return new OpenIdConsumerSetupRequired($setupUrl);
00323 }
00324 } elseif ($parameters['openid.mode'] = 'cancel') {
00325 return new OpenIdConsumerCancel();
00326 }
00327
00328 if (!isset($parameters['openid.assoc_handle']))
00329 throw new WrongArgumentException('no association handle');
00330
00331 if (!isset($parameters['openid.identity']))
00332 throw new WrongArgumentException('no identity');
00333
00334 $identity =
00335 HttpUrl::create()->
00336 parse($parameters['openid.identity']);
00337
00338 Assert::isTrue($identity->isValid(), 'invalid identity');
00339 $identity->makeComparable();
00340
00341 $signedFields = array();
00342 if (isset($parameters['openid.signed'], $parameters['openid.sig'])) {
00343 $signedFields = explode(',', $parameters['openid.signed']);
00344
00345 if (!in_array('identity', $signedFields))
00346 throw new WrongArgumentException('identity must be signed');
00347 } else
00348 throw new WrongArgumentException('no signature in response');
00349
00350 if (
00351 $manager
00352 && (
00353 $association = $manager->findByHandle(
00354 $parameters['openid.assoc_handle'],
00355 self::ASSOCIATION_TYPE
00356 )
00357 )
00358 && !isset($parameters['openid.invalidate_handle'])
00359 ) {
00360 $tokenContents = null;
00361 foreach ($signedFields as $signedField) {
00362 $tokenContents .=
00363 $signedField
00364 .':'
00365 .$parameters['openid.'.strtr($signedField, '.', '_')]
00366 ."\n";
00367 }
00368
00369 if (
00370 base64_encode(
00371 CryptoFunctions::hmacsha1(
00372 $association->getSecret(),
00373 $tokenContents
00374 )
00375 )
00376 != $parameters['openid.sig']
00377 )
00378 throw new WrongArgumentException('signature mismatch');
00379
00380 return new OpenIdConsumerPositive($identity);
00381
00382 } elseif (
00383 !$manager
00384 || isset($parameters['openid.invalidate_handle'])
00385 ) {
00386 if ($this->checkAuthentication($parameters, $manager))
00387 return new OpenIdConsumerPositive($identity);
00388 else
00389 return new OpenIdConsumerFail();
00390 }
00391
00392 Assert::isUnreachable();
00393 }
00394
00399 public function addExtension(OpenIdExtension $extension)
00400 {
00401 $this->extensions[] = $extension;
00402
00403 return $this;
00404 }
00405
00409 private function checkAuthentication(
00410 array $parameters,
00411 $manager = null
00412 )
00413 {
00414 $credentials = new OpenIdCredentials(
00415 HttpUrl::create()->parse($parameters['openid.identity']),
00416 $this->httpClient
00417 );
00418
00419 $request = HttpRequest::create()->
00420 setMethod(HttpMethod::post())->
00421 setUrl($credentials->getServer());
00422
00423 if (isset($parameters['openid.invalidate_handle']) && $manager)
00424 $request->setPostVar(
00425 'openid.invalidate_handle',
00426 $parameters['openid.invalidate_handle']
00427 );
00428
00429 foreach (explode(',', $parameters['openid.signed']) as $key) {
00430 $key = 'openid.'.$key;
00431 $request->setPostVar($key, $parameters[$key]);
00432 }
00433
00434 $request->
00435 setPostVar('openid.mode', 'check_authentication')->
00436 setPostVar(
00437 'openid.assoc_handle',
00438 $parameters['openid.assoc_handle']
00439 )->
00440 setPostVar(
00441 'openid.sig',
00442 $parameters['openid.sig']
00443 )->
00444 setPostVar(
00445 'openid.signed',
00446 $parameters['openid.signed']
00447 );
00448
00449 $response = $this->httpClient->send($request);
00450 if ($response->getStatus()->getId() != HttpStatus::CODE_200)
00451 throw new OpenIdException('bad response code from server');
00452
00453 $result = $this->parseKeyValueFormat($response->getBody());
00454
00455 if (
00456 !isset($result['is_valid'])
00457 || (
00458 $result['is_valid'] !== 'true'
00459 &&
00460 $result['is_valid'] !== 'false'
00461 )
00462 )
00463 throw new OpenIdException('strange response given');
00464
00465 if ($result['is_valid'] === 'true') {
00466 if (isset($result['invalidate_handle']) && $manager) {
00467 $manager->purgeByHandle($result['invalidate_handle']);
00468 }
00469
00470 return true;
00471 } elseif ($result['is_valid'] === 'false')
00472 return false;
00473
00474 Assert::isUnreachable();
00475 }
00476
00477 private function parseKeyValueFormat($raw)
00478 {
00479 $result = array();
00480 $lines = explode("\n", $raw);
00481
00482 foreach ($lines as $line) {
00483 if (!empty($line) && strpos($line, ':') !== false) {
00484 list($key, $value) = explode(':', $line, 2);
00485 $result[trim($key)] = trim($value);
00486 }
00487 }
00488
00489 return $result;
00490 }
00491
00492 private function parseGetParameters(array $get)
00493 {
00494 $result = array();
00495 foreach ($get as $key => $value) {
00496 if (strpos($key, 'openid') === 0) {
00497 $key = preg_replace('/^openid_/', 'openid.', $key);
00498 $result[$key] = $value;
00499 }
00500 }
00501
00502 return $result;
00503 }
00504 }
00505 ?>