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