# 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 ?>