OpenIdConsumer.class.php

Go to the documentation of this file.
00001 <?php
00002 /***************************************************************************
00003  *   Copyright (C) 2007-2008 by Anton E. Lebedevich                        *
00004  *                                                                         *
00005  *   This program is free software; you can redistribute it and/or modify  *
00006  *   it under the terms of the GNU Lesser General Public License as        *
00007  *   published by the Free Software Foundation; either version 3 of the    *
00008  *   License, or (at your option) any later version.                       *
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             ) { // smart mode
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             ) { // dumb or handle invalidation mode
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 ?>