| 1: | <?php |
| 2: | |
| 3: | |
| 4: | |
| 5: | |
| 6: | |
| 7: | |
| 8: | |
| 9: | |
| 10: | |
| 11: | |
| 12: | |
| 13: | |
| 14: | |
| 15: | |
| 16: | |
| 17: | |
| 18: | |
| 19: | |
| 20: | |
| 21: | |
| 22: | |
| 23: | namespace SimpleID\Protocols\OpenID; |
| 24: | |
| 25: | use Psr\Log\LogLevel; |
| 26: | use SimpleID\Module; |
| 27: | use SimpleID\ModuleManager; |
| 28: | use SimpleID\Base\RouteContentNegotiationEvent; |
| 29: | use SimpleID\Base\ScopeInfoCollectionEvent; |
| 30: | use SimpleID\Base\RequestState; |
| 31: | use SimpleID\Auth\AuthManager; |
| 32: | use SimpleID\Crypt\Random; |
| 33: | use SimpleID\Crypt\SecurityToken; |
| 34: | use SimpleID\Protocols\ProtocolResult; |
| 35: | use SimpleID\Protocols\ProtocolResultEvent; |
| 36: | use SimpleID\Store\StoreManager; |
| 37: | use SimpleID\Util\Events\BaseDataCollectionEvent; |
| 38: | use SimpleID\Util\Events\UIBuildEvent; |
| 39: | use SimpleID\Util\Forms\FormBuildEvent; |
| 40: | use SimpleID\Util\Forms\FormSubmitEvent; |
| 41: | use SimpleID\Util\Forms\FormState; |
| 42: | use SimpleID\Util\UI\Template; |
| 43: | |
| 44: | |
| 45: | |
| 46: | |
| 47: | class OpenIDModule extends Module implements ProtocolResult { |
| 48: | |
| 49: | |
| 50: | const OPENID_RETURN_TO = 'http://specs.openid.net/auth/2.0/return_to'; |
| 51: | |
| 52: | const DEFAULT_SCOPE = 'tag:simpleid.sf.net,2021:openid:default'; |
| 53: | |
| 54: | |
| 55: | protected $cache; |
| 56: | |
| 57: | |
| 58: | protected $mgr; |
| 59: | |
| 60: | static function init($f3) { |
| 61: | $f3->route('GET @openid_provider_xrds: /openid/xrds', 'SimpleID\Protocols\OpenID\OpenIDModule->providerXRDS'); |
| 62: | $f3->route('GET @openid_user_xrds: /user/@uid/xrds', 'SimpleID\Protocols\OpenID\OpenIDModule->userXRDS'); |
| 63: | $f3->route('POST @openid_consent: /openid/consent', 'SimpleID\Protocols\OpenID\OpenIDModule->consent'); |
| 64: | } |
| 65: | |
| 66: | function __construct() { |
| 67: | parent::__construct(); |
| 68: | $this->cache = \Cache::instance(); |
| 69: | $this->mgr = ModuleManager::instance(); |
| 70: | } |
| 71: | |
| 72: | |
| 73: | |
| 74: | |
| 75: | public function onRouteContentNegotiationEvent(RouteContentNegotiationEvent $event) { |
| 76: | $route = $event->getRoute(); |
| 77: | $content_type = $event->negotiate([ 'text/html', 'application/xml', 'application/xhtml+xml', 'application/xrds+xml' ]); |
| 78: | |
| 79: | switch ($route) { |
| 80: | case 'index': |
| 81: | $_request = $event->getRequest(); |
| 82: | |
| 83: | if (isset($_request['openid.mode'])) { |
| 84: | $this->start(new Request($_request)); |
| 85: | $event->stopPropagation(); |
| 86: | } elseif ($content_type == 'application/xrds+xml') { |
| 87: | $this->providerXRDS(); |
| 88: | $event->stopPropagation(); |
| 89: | } else { |
| 90: | |
| 91: | header('X-XRDS-Location: ' . $this->getCanonicalURL('@openid_provider_xrds')); |
| 92: | return; |
| 93: | } |
| 94: | break; |
| 95: | |
| 96: | case 'user': |
| 97: | if ($content_type == 'application/xrds+xml') { |
| 98: | $this->userXRDS(); |
| 99: | $event->stopPropagation(); |
| 100: | } |
| 101: | break; |
| 102: | } |
| 103: | } |
| 104: | |
| 105: | |
| 106: | |
| 107: | |
| 108: | |
| 109: | |
| 110: | |
| 111: | |
| 112: | |
| 113: | |
| 114: | |
| 115: | |
| 116: | |
| 117: | |
| 118: | |
| 119: | public function start($request) { |
| 120: | switch ($request['openid.mode']) { |
| 121: | case 'associate': |
| 122: | $this->associate($request); |
| 123: | break; |
| 124: | case 'checkid_immediate': |
| 125: | case 'checkid_setup': |
| 126: | $token = new SecurityToken(); |
| 127: | $request_state = new RequestState(); |
| 128: | $request_state->setParams($request->toArray()); |
| 129: | $this->checkHttps('redirect', true, $this->getCanonicalURL('continue/' . rawurlencode($token->generate($request_state)), '', 'https')); |
| 130: | |
| 131: | $this->checkid($request); |
| 132: | break; |
| 133: | case 'check_authentication': |
| 134: | $this->check_authentication($request); |
| 135: | break; |
| 136: | default: |
| 137: | if (isset($request['openid.return_to'])) { |
| 138: | |
| 139: | $this->fatalError($this->f3->get('intl.core.openid.invalid_message'), 400); |
| 140: | } else { |
| 141: | |
| 142: | $this->directError('Invalid OpenID message.', [], $request); |
| 143: | } |
| 144: | } |
| 145: | } |
| 146: | |
| 147: | |
| 148: | |
| 149: | |
| 150: | |
| 151: | |
| 152: | |
| 153: | |
| 154: | |
| 155: | |
| 156: | |
| 157: | |
| 158: | |
| 159: | |
| 160: | protected function associate($request) { |
| 161: | $this->logger->log(LogLevel::DEBUG, 'SimpleID\Protocols\OpenID\OpenIDModule->associate'); |
| 162: | $this->logger->log(LogLevel::INFO, 'OpenID association request', $request->toArray()); |
| 163: | |
| 164: | $assoc_types = Association::getAssociationTypes(); |
| 165: | $session_types = Association::getSessionTypes($this->isHttps(), $request->getVersion()); |
| 166: | |
| 167: | |
| 168: | if (($request->getVersion() == Message::OPENID_VERSION_1_1) && !isset($request['openid.session_type'])) $request['openid.session_type'] = ''; |
| 169: | $assoc_type = $request['openid.assoc_type']; |
| 170: | $session_type = $request['openid.session_type']; |
| 171: | |
| 172: | |
| 173: | $dh_modulus = (isset($request['openid.dh_modulus'])) ? $request['openid.dh_modulus'] : NULL; |
| 174: | $dh_gen = (isset($request['openid.dh_gen'])) ? $request['openid.dh_gen'] : NULL; |
| 175: | $dh_consumer_public = $request['openid.dh_consumer_public']; |
| 176: | |
| 177: | if (!isset($request['openid.session_type']) || !isset($request['openid.assoc_type'])) { |
| 178: | $this->logger->log(LogLevel::ERROR, 'Association failed: openid.session_type or openid.assoc_type not set'); |
| 179: | $this->directError('openid.session_type or openid.assoc_type not set', [], $request); |
| 180: | return; |
| 181: | } |
| 182: | |
| 183: | |
| 184: | if (!array_key_exists($assoc_type, $assoc_types)) { |
| 185: | $error = [ |
| 186: | 'error_code' => 'unsupported-type', |
| 187: | 'session_type' => 'DH-SHA1', |
| 188: | 'assoc_type' => 'HMAC-SHA1' |
| 189: | ]; |
| 190: | $this->logger->log(LogLevel::ERROR, 'Association failed: The association type is not supported by SimpleID.'); |
| 191: | $this->directError('The association type is not supported by SimpleID.', $error, $request); |
| 192: | return; |
| 193: | } |
| 194: | |
| 195: | if (!array_key_exists($session_type, $session_types)) { |
| 196: | $error = [ |
| 197: | 'error_code' => 'unsupported-type', |
| 198: | 'session_type' => 'DH-SHA1', |
| 199: | 'assoc_type' => 'HMAC-SHA1' |
| 200: | ]; |
| 201: | $this->logger->log(LogLevel::ERROR, 'Association failed: The session type is not supported by SimpleID.'); |
| 202: | $this->directError('The session type is not supported by SimpleID.', $error, $request); |
| 203: | return; |
| 204: | } |
| 205: | |
| 206: | if ($session_type == 'DH-SHA1' || $session_type == 'DH-SHA256') { |
| 207: | if (!$dh_consumer_public) { |
| 208: | $this->logger->log(LogLevel::ERROR, 'Association failed: openid.dh_consumer_public not set'); |
| 209: | $this->directError('openid.dh_consumer_public not set', [], $request); |
| 210: | return; |
| 211: | } |
| 212: | } |
| 213: | |
| 214: | $association = new Association(Association::ASSOCIATION_SHARED, $assoc_type); |
| 215: | $this->logger->log(LogLevel::INFO, 'Created association: ' . $association->toString()); |
| 216: | |
| 217: | $this->cache->set($association->getHandle() . '.openid_association', $association, SIMPLEID_SHORT_TOKEN_EXPIRES_IN); |
| 218: | |
| 219: | $response = new Response($request); |
| 220: | $response->setArray($association->getOpenIDResponse($session_type, $dh_consumer_public, $dh_modulus, $dh_gen)); |
| 221: | $response->set('expires_in', strval(SIMPLEID_SHORT_TOKEN_EXPIRES_IN)); |
| 222: | $this->logger->log(LogLevel::INFO, 'Association response', $response->toArray()); |
| 223: | |
| 224: | $response->render(); |
| 225: | } |
| 226: | |
| 227: | |
| 228: | |
| 229: | |
| 230: | |
| 231: | |
| 232: | |
| 233: | |
| 234: | |
| 235: | |
| 236: | |
| 237: | |
| 238: | |
| 239: | |
| 240: | |
| 241: | |
| 242: | |
| 243: | |
| 244: | |
| 245: | |
| 246: | |
| 247: | |
| 248: | public function checkid($request) { |
| 249: | $immediate = ($request['openid.mode'] == 'checkid_immediate'); |
| 250: | $version = $request->getVersion(); |
| 251: | |
| 252: | $this->logger->log(LogLevel::INFO, 'OpenID authentication request: ' . (($immediate) ? 'immediate' : 'setup') . '; ', $request->toArray()); |
| 253: | |
| 254: | |
| 255: | if ($version == Message::OPENID_VERSION_1_1) { |
| 256: | if (!isset($request['openid.return_to'])) { |
| 257: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: openid.return_to not set.'); |
| 258: | $this->fatalError($this->f3->get('intl.core.openid.missing_return_to'), 400); |
| 259: | return; |
| 260: | } |
| 261: | if (!isset($request['openid.identity'])) { |
| 262: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: openid.identity not set.'); |
| 263: | $this->fatalError($this->f3->get('intl.core.openid.missing_identity'), 400); |
| 264: | return; |
| 265: | } |
| 266: | } |
| 267: | |
| 268: | if ($version == Message::OPENID_VERSION_2) { |
| 269: | if (isset($request['openid.identity']) && !isset($request['openid.claimed_id'])) { |
| 270: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: openid.identity set, but not openid.claimed_id.'); |
| 271: | $this->fatalError($this->f3->get('intl.core.openid.missing_claimed_id'), 400); |
| 272: | return; |
| 273: | } |
| 274: | |
| 275: | if (!isset($request['openid.realm']) && !isset($request['openid.return_to'])) { |
| 276: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: openid.return_to not set when openid.realm is not set.'); |
| 277: | $this->fatalError($this->f3->get('intl.core.openid.realm_but_no_return_to'), 400); |
| 278: | return; |
| 279: | } |
| 280: | } |
| 281: | |
| 282: | if (isset($request['openid.return_to'])) { |
| 283: | $realm = $request->getRealm(); |
| 284: | |
| 285: | if (!$request->returnToMatches($realm)) { |
| 286: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: openid.return_to does not match realm.'); |
| 287: | $this->indirectError($request['openid.return_to'], 'Protocol Error: openid.return_to does not match realm.', [], $request); |
| 288: | return; |
| 289: | } |
| 290: | } |
| 291: | |
| 292: | if (isset($request['openid.identity'])) { |
| 293: | |
| 294: | $this->logger->log(LogLevel::DEBUG, 'openid.identity found, use openIDCheckIdentity'); |
| 295: | $result = $this->openIDCheckIdentity($request, $immediate); |
| 296: | } else { |
| 297: | $this->logger->log(LogLevel::DEBUG, 'openid.identity not found, trying extensions'); |
| 298: | |
| 299: | |
| 300: | $event = new OpenIDCheckEvent($request, $immediate); |
| 301: | \Events::instance()->dispatch($event); |
| 302: | |
| 303: | $result = $event->getResult(); |
| 304: | } |
| 305: | |
| 306: | switch ($result) { |
| 307: | case self::CHECKID_APPROVAL_REQUIRED: |
| 308: | $this->logger->log(LogLevel::INFO, 'CHECKID_APPROVAL_REQUIRED'); |
| 309: | if ($immediate) { |
| 310: | $response = $this->createApprovalRequiredResponse($request); |
| 311: | $response->render($request['openid.return_to']); |
| 312: | } else { |
| 313: | $response = $this->createOKResponse($request); |
| 314: | $this->consentForm($request, $response, $result); |
| 315: | } |
| 316: | break; |
| 317: | case self::CHECKID_RETURN_TO_SUSPECT: |
| 318: | $this->logger->log(LogLevel::INFO, 'CHECKID_RETURN_TO_SUSPECT'); |
| 319: | if ($immediate) { |
| 320: | $response = $this->createErrorResponse($request, $immediate); |
| 321: | $response->render($request['openid.return_to']); |
| 322: | } else { |
| 323: | $response = $this->createOKResponse($request); |
| 324: | $this->consentForm($request, $response, $result); |
| 325: | } |
| 326: | break; |
| 327: | case self::CHECKID_OK: |
| 328: | $this->logger->log(LogLevel::INFO, 'CHECKID_OK'); |
| 329: | $this->logActivity($request); |
| 330: | $response = $this->createOKResponse($request); |
| 331: | $this->signResponse($response, isset($request['openid.assoc_handle']) ? $request['openid.assoc_handle'] : NULL); |
| 332: | $response->render($request['openid.return_to']); |
| 333: | break; |
| 334: | case self::CHECKID_REENTER_CREDENTIALS: |
| 335: | case self::CHECKID_LOGIN_REQUIRED: |
| 336: | $this->logger->log(LogLevel::INFO, 'CHECKID_REENTER_CREDENTIALS | CHECKID_LOGIN_REQUIRED'); |
| 337: | if ($immediate) { |
| 338: | $response = $this->createLoginRequiredResponse($request, $result); |
| 339: | $response->render($request['openid.return_to']); |
| 340: | } else { |
| 341: | $token = new SecurityToken(); |
| 342: | $request_state = new RequestState(); |
| 343: | $request_state->setParams($request->toArray()); |
| 344: | $form_state = new FormState([ |
| 345: | 'cancel' => 'openid', |
| 346: | 'mode' => AuthManager::MODE_CREDENTIALS, |
| 347: | 'auth_skip_activity' => true |
| 348: | ]); |
| 349: | $form_state->setRequest($request); |
| 350: | if ($result == self::CHECKID_REENTER_CREDENTIALS) { |
| 351: | $auth = AuthManager::instance(); |
| 352: | $user = $auth->getUser(); |
| 353: | $form_state['uid'] = $user['uid']; |
| 354: | $form_state['mode'] = AuthManager::MODE_REENTER_CREDENTIALS; |
| 355: | } |
| 356: | |
| 357: | |
| 358: | $auth_module = $this->mgr->getModule('SimpleID\Auth\AuthModule'); |
| 359: | $auth_module->loginForm([ |
| 360: | 'destination' => 'continue/' . rawurlencode($token->generate($request_state)) |
| 361: | ], $form_state); |
| 362: | exit; |
| 363: | } |
| 364: | break; |
| 365: | case self::CHECKID_IDENTITIES_NOT_MATCHING: |
| 366: | case self::CHECKID_IDENTITY_NOT_EXIST: |
| 367: | $this->logger->log(LogLevel::INFO, 'CHECKID_IDENTITIES_NOT_MATCHING | CHECKID_IDENTITY_NOT_EXIST'); |
| 368: | $response = $this->createErrorResponse($request, $immediate); |
| 369: | if ($immediate) { |
| 370: | $response->render($request['openid.return_to']); |
| 371: | } else { |
| 372: | $this->consentForm($request, $response, $result); |
| 373: | } |
| 374: | break; |
| 375: | case self::CHECKID_PROTOCOL_ERROR: |
| 376: | if (isset($request['openid.return_to'])) { |
| 377: | $response = $this->createErrorResponse($request, $immediate); |
| 378: | $response->render($request['openid.return_to']); |
| 379: | } else { |
| 380: | $this->fatalError('Unrecognised request.', 400); |
| 381: | } |
| 382: | break; |
| 383: | } |
| 384: | } |
| 385: | |
| 386: | |
| 387: | |
| 388: | |
| 389: | |
| 390: | |
| 391: | |
| 392: | |
| 393: | |
| 394: | |
| 395: | |
| 396: | |
| 397: | protected function openIDCheckIdentity($request, $immediate) { |
| 398: | $auth = AuthManager::instance(); |
| 399: | $store = StoreManager::instance(); |
| 400: | $config = $this->f3->get('config'); |
| 401: | |
| 402: | $realm = $request->getRealm(); |
| 403: | $cid = RelyingParty::buildID($realm); |
| 404: | |
| 405: | |
| 406: | if (!$auth->isLoggedIn()) { |
| 407: | return self::CHECKID_LOGIN_REQUIRED; |
| 408: | } else { |
| 409: | $user = $auth->getUser(); |
| 410: | $uid = $user['uid']; |
| 411: | } |
| 412: | |
| 413: | |
| 414: | |
| 415: | if ($request['openid.identity'] == Request::OPENID_IDENTIFIER_SELECT) { |
| 416: | $test_user = $store->loadUser($uid); |
| 417: | $identity = $test_user['openid']['identity']; |
| 418: | |
| 419: | $this->logger->log(LogLevel::INFO, 'OpenID identifier selection: Selected ' . $uid . ' [' . $identity . ']'); |
| 420: | } else { |
| 421: | $identity = $request['openid.identity']; |
| 422: | |
| 423: | $test_user = $store->findUser('openid.identity', $identity); |
| 424: | } |
| 425: | if ($test_user == NULL) return self::CHECKID_IDENTITY_NOT_EXIST; |
| 426: | if ($test_user['uid'] != $user['uid']) { |
| 427: | $this->logger->log(LogLevel::NOTICE, 'Requested user ' . $test_user['uid'] . ' does not match logged in user ' . $user['uid']); |
| 428: | return self::CHECKID_IDENTITIES_NOT_MATCHING; |
| 429: | } |
| 430: | |
| 431: | |
| 432: | $event = new OpenIDCheckEvent($request, $immediate, $identity); |
| 433: | \Events::instance()->dispatch($event); |
| 434: | |
| 435: | |
| 436: | if ($request['openid.identity'] == Request::OPENID_IDENTIFIER_SELECT) { |
| 437: | $request['openid.claimed_id'] = $identity; |
| 438: | $request['openid.identity'] = $identity; |
| 439: | } |
| 440: | |
| 441: | |
| 442: | $client_prefs = (isset($user->clients[$cid])) ? $user->clients[$cid] : NULL; |
| 443: | |
| 444: | if (($request->getVersion() == Message::OPENID_VERSION_2) && $config['openid_verify_return_url']) { |
| 445: | $verified = FALSE; |
| 446: | |
| 447: | $relying_party = $this->loadRelyingParty($realm); |
| 448: | if ($relying_party->getServices() == null) { |
| 449: | $services = []; |
| 450: | } else { |
| 451: | $services = $relying_party->getServices()->getByType(self::OPENID_RETURN_TO); |
| 452: | } |
| 453: | |
| 454: | $this->logger->log(LogLevel::INFO, 'OpenID 2 discovery: ' . count($services) . ' matching services'); |
| 455: | |
| 456: | if ($services) { |
| 457: | $return_to_uris = []; |
| 458: | |
| 459: | foreach ($services as $service) { |
| 460: | $return_to_uris = array_merge($return_to_uris, $service['uri']); |
| 461: | } |
| 462: | foreach ($return_to_uris as $return_to) { |
| 463: | if ($request->returnToMatches($return_to, $config['openid_strict_realm_check'])) { |
| 464: | $this->logger->log(LogLevel::INFO, 'OpenID 2 discovery: verified'); |
| 465: | $verified = TRUE; |
| 466: | break; |
| 467: | } |
| 468: | } |
| 469: | } |
| 470: | |
| 471: | if (!$verified) { |
| 472: | if (($client_prefs != NULL) && isset($client_prefs['consents']['openid']) && $client_prefs['consents']['openid']) { |
| 473: | $this->logger->log(LogLevel::NOTICE, 'OpenID 2 discovery: not verified, but overridden by user preference'); |
| 474: | } else { |
| 475: | $this->logger->log(LogLevel::NOTICE, 'OpenID 2 discovery: not verified'); |
| 476: | $event->setResult(self::CHECKID_RETURN_TO_SUSPECT); |
| 477: | } |
| 478: | } |
| 479: | } |
| 480: | |
| 481: | |
| 482: | |
| 483: | if (($client_prefs != NULL) && isset($client_prefs['consents']['openid']) && $client_prefs['consents']['openid']) { |
| 484: | $this->logger->log(LogLevel::INFO, 'Automatic set for realm ' . $realm); |
| 485: | $event->setResult(self::CHECKID_OK); |
| 486: | |
| 487: | $final_assertion_result = $event->getResult(); |
| 488: | |
| 489: | if ($final_assertion_result == self::CHECKID_OK) { |
| 490: | if (!isset($user->clients[$cid])) $user->clients[$cid] = []; |
| 491: | $user->clients[$cid]['last_time'] = time(); |
| 492: | $store->saveUser($user); |
| 493: | } |
| 494: | |
| 495: | return $final_assertion_result; |
| 496: | } else { |
| 497: | $event->setResult(self::CHECKID_APPROVAL_REQUIRED); |
| 498: | return $event->getResult(); |
| 499: | } |
| 500: | } |
| 501: | |
| 502: | |
| 503: | |
| 504: | |
| 505: | |
| 506: | |
| 507: | |
| 508: | |
| 509: | protected function createOKResponse($request) { |
| 510: | $rand = new Random(); |
| 511: | $nonce = gmdate('Y-m-d\TH:i:s\Z') . bin2hex($rand->bytes(4)); |
| 512: | |
| 513: | $response = new Response($request); |
| 514: | $response->setArray([ |
| 515: | 'mode' => 'id_res', |
| 516: | 'op_endpoint' => $this->getCanonicalURL(), |
| 517: | 'response_nonce' => $nonce |
| 518: | ]); |
| 519: | |
| 520: | if (isset($request['openid.assoc_handle'])) $response['assoc_handle'] = $request['openid.assoc_handle']; |
| 521: | if (isset($request['openid.identity'])) $response['identity'] = $request['openid.identity']; |
| 522: | if (isset($request['openid.return_to'])) $response['return_to'] = $request['openid.return_to']; |
| 523: | |
| 524: | if (($request->getVersion() == Message::OPENID_VERSION_2) && isset($request['openid.claimed_id'])) { |
| 525: | $response['claimed_id'] = $request['openid.claimed_id']; |
| 526: | } |
| 527: | |
| 528: | $event = new OpenIDResponseBuildEvent(true, $request, $response); |
| 529: | \Events::instance()->dispatch($event); |
| 530: | |
| 531: | $this->logger->log(LogLevel::INFO, 'OpenID authentication response', $event->getResponse()->toArray()); |
| 532: | return $event->getResponse(); |
| 533: | } |
| 534: | |
| 535: | |
| 536: | |
| 537: | |
| 538: | |
| 539: | |
| 540: | |
| 541: | |
| 542: | |
| 543: | |
| 544: | protected function createApprovalRequiredResponse($request) { |
| 545: | $response = new Response($request); |
| 546: | |
| 547: | if ($request->getVersion() == Message::OPENID_VERSION_2) { |
| 548: | $response['mode'] = 'setup_needed'; |
| 549: | } else { |
| 550: | $token = new SecurityToken(); |
| 551: | $request_state = new RequestState(); |
| 552: | $request_state->setParams($request->toArray()); |
| 553: | |
| 554: | $request['openid.mode'] = 'checkid_setup'; |
| 555: | $response->setArray([ |
| 556: | 'mode' => 'id_res', |
| 557: | 'user_setup_url' => $this->getCanonicalURL('auth/login/continue/' . rawurlencode($token->generate($request_state))) |
| 558: | ]); |
| 559: | } |
| 560: | |
| 561: | $event = new OpenIDResponseBuildEvent(false, $request, $response); |
| 562: | \Events::instance()->dispatch($event); |
| 563: | |
| 564: | $this->logger->log(LogLevel::INFO, 'OpenID authentication response', $event->getResponse()->toArray()); |
| 565: | return $event->getResponse(); |
| 566: | } |
| 567: | |
| 568: | |
| 569: | |
| 570: | |
| 571: | |
| 572: | |
| 573: | |
| 574: | |
| 575: | |
| 576: | |
| 577: | |
| 578: | protected function createLoginRequiredResponse($request, $result = self::CHECKID_LOGIN_REQUIRED) { |
| 579: | $response = new Response($request); |
| 580: | |
| 581: | if ($request->getVersion() == Message::OPENID_VERSION_2) { |
| 582: | $response['mode'] = 'setup_needed'; |
| 583: | } else { |
| 584: | $token = new SecurityToken(); |
| 585: | $request_state = new RequestState(); |
| 586: | $request_state->setParams($request->toArray()); |
| 587: | $query = ($result == self::CHECKID_REENTER_CREDENTIALS) ? 'mode=' . AuthManager::MODE_REENTER_CREDENTIALS : ''; |
| 588: | |
| 589: | $response->setArray([ |
| 590: | 'mode' => 'id_res', |
| 591: | 'user_setup_url' => $this->getCanonicalURL('auth/login/continue/' . rawurlencode($token->generate($request_state)), $query) |
| 592: | ]); |
| 593: | } |
| 594: | |
| 595: | $event = new OpenIDResponseBuildEvent(false, $request, $response); |
| 596: | \Events::instance()->dispatch($event); |
| 597: | |
| 598: | $this->logger->log(LogLevel::INFO, 'OpenID authentication response', $event->getResponse()->toArray()); |
| 599: | return $event->getResponse(); |
| 600: | } |
| 601: | |
| 602: | |
| 603: | |
| 604: | |
| 605: | |
| 606: | |
| 607: | |
| 608: | |
| 609: | |
| 610: | |
| 611: | |
| 612: | |
| 613: | protected function createErrorResponse($request, $immediate = false) { |
| 614: | $response = new Response($request); |
| 615: | $version = $request->getVersion(); |
| 616: | |
| 617: | if ($immediate) { |
| 618: | if ($version == Message::OPENID_VERSION_2) { |
| 619: | $response['mode'] = 'setup_needed'; |
| 620: | } else { |
| 621: | $response['mode'] = 'id_res'; |
| 622: | } |
| 623: | } else { |
| 624: | $response['mode'] = 'cancel'; |
| 625: | } |
| 626: | |
| 627: | $event = new OpenIDResponseBuildEvent(false, $request, $response); |
| 628: | \Events::instance()->dispatch($event); |
| 629: | |
| 630: | $this->logger->log(LogLevel::INFO, 'OpenID authentication response', $event->getResponse()->toArray()); |
| 631: | return $event->getResponse(); |
| 632: | } |
| 633: | |
| 634: | |
| 635: | |
| 636: | |
| 637: | |
| 638: | |
| 639: | |
| 640: | |
| 641: | |
| 642: | |
| 643: | |
| 644: | protected function signResponse($response, $assoc_handle = NULL) { |
| 645: | $cache = \Cache::instance(); |
| 646: | |
| 647: | if (!$assoc_handle) { |
| 648: | $assoc = new Association(Association::ASSOCIATION_PRIVATE); |
| 649: | $response['assoc_handle'] = $assoc->getHandle(); |
| 650: | } else { |
| 651: | $assoc = $cache->get(rawurlencode($assoc_handle) . '.openid_association'); |
| 652: | |
| 653: | if ($assoc->getCreationTime() + SIMPLEID_SHORT_TOKEN_EXPIRES_IN < time()) { |
| 654: | |
| 655: | $this->logger->log(LogLevel::NOTICE, 'Association handle ' . $assoc->getHandle() . ' expired. Using stateless mode.'); |
| 656: | $response['invalidate_handle'] = $assoc_handle; |
| 657: | $assoc = new Association(Association::ASSOCIATION_PRIVATE); |
| 658: | $response['assoc_handle'] = $assoc->getHandle(); |
| 659: | } |
| 660: | } |
| 661: | |
| 662: | |
| 663: | |
| 664: | if ($assoc->isPrivate() && isset($response['response_nonce'])) { |
| 665: | $cache->set(rawurlencode($response['response_nonce']) . '.openid_response' , [ |
| 666: | 'response_nonce' => $response['response_nonce'], |
| 667: | 'association' => $assoc |
| 668: | ], SIMPLEID_SHORT_TOKEN_EXPIRES_IN); |
| 669: | } |
| 670: | |
| 671: | $response['sig'] = $assoc->sign($response); |
| 672: | |
| 673: | $this->logger->log(LogLevel::INFO, 'OpenID signed authentication response', $response->toArray()); |
| 674: | |
| 675: | return $response; |
| 676: | } |
| 677: | |
| 678: | |
| 679: | |
| 680: | |
| 681: | |
| 682: | |
| 683: | |
| 684: | |
| 685: | |
| 686: | protected function check_authentication($request) { |
| 687: | $this->logger->log(LogLevel::DEBUG, 'SimpleID\Protocols\OpenID\OpenIDModule->check_authentication'); |
| 688: | $this->logger->log(LogLevel::INFO, 'OpenID direct verification', $request->toArray()); |
| 689: | |
| 690: | $cache = \Cache::instance(); |
| 691: | |
| 692: | $response = new Response($request); |
| 693: | $response['is_valid'] = ($this->verifySignatures($request)) ? 'true' : 'false'; |
| 694: | |
| 695: | |
| 696: | if (isset($request['openid.invalidate_handle'])) { |
| 697: | $invalid_assoc = $cache->get(rawurlencode($request['openid.invalidate_handle']) . 'openid_association'); |
| 698: | |
| 699: | if (!$invalid_assoc || ($invalid_assoc->getCreationTime() + SIMPLEID_SHORT_TOKEN_EXPIRES_IN < time())) { |
| 700: | |
| 701: | $response['invalidate_handle'] = $request['openid.invalidate_handle']; |
| 702: | } |
| 703: | } |
| 704: | |
| 705: | $this->logger->log(LogLevel::INFO, 'OpenID direct verification response', $response->toArray()); |
| 706: | |
| 707: | $response->render(); |
| 708: | } |
| 709: | |
| 710: | |
| 711: | |
| 712: | |
| 713: | |
| 714: | |
| 715: | |
| 716: | |
| 717: | protected function verifySignatures($request) { |
| 718: | $cache = \Cache::instance(); |
| 719: | |
| 720: | |
| 721: | $stateless = (isset($request['openid.response_nonce'])) ? $cache->get(rawurlencode($request['openid.response_nonce']) . '.openid_response') : NULL; |
| 722: | if ($stateless == NULL) { |
| 723: | $this->logger->log(LogLevel::NOTICE, 'Response nonce not found: ' . $request['openid.response_nonce']); |
| 724: | return false; |
| 725: | } |
| 726: | $cache->clear(rawurlencode($request['openid.response_nonce']) . '.openid_response'); |
| 727: | |
| 728: | $association = $stateless['association']; |
| 729: | |
| 730: | if (!$association->isPrivate()) { |
| 731: | $this->logger->log(LogLevel::WARNING, 'Attempting to verify an association with a shared key.'); |
| 732: | return FALSE; |
| 733: | } |
| 734: | |
| 735: | if ($association->getHandle() != $request['openid.assoc_handle']) { |
| 736: | $this->logger->log(LogLevel::WARNING, 'Attempting to verify a response_nonce more than once, or private association expired.'); |
| 737: | return FALSE; |
| 738: | } else { |
| 739: | $signature = $association->sign($request); |
| 740: | $this->logger->log(LogLevel::DEBUG, '***** Signature: ' . $signature); |
| 741: | |
| 742: | if ($signature != $request['openid.sig']) { |
| 743: | $this->logger->log(LogLevel::WARNING, 'Signature supplied in request does not match the signature generated.'); |
| 744: | return FALSE; |
| 745: | } |
| 746: | } |
| 747: | |
| 748: | return true; |
| 749: | } |
| 750: | |
| 751: | |
| 752: | |
| 753: | |
| 754: | |
| 755: | |
| 756: | |
| 757: | |
| 758: | |
| 759: | |
| 760: | |
| 761: | |
| 762: | |
| 763: | |
| 764: | |
| 765: | |
| 766: | |
| 767: | protected function consentForm($request, $response, $reason = self::CHECKID_APPROVAL_REQUIRED) { |
| 768: | $tpl = Template::instance(); |
| 769: | $token = new SecurityToken(); |
| 770: | $auth = AuthManager::instance(); |
| 771: | $user = $auth->getUser(); |
| 772: | |
| 773: | $form_state = new FormState([ |
| 774: | 'code' => $reason |
| 775: | ]); |
| 776: | $form_state->setRequest($request); |
| 777: | $form_state->setResponse($response); |
| 778: | $cancel = ($response['mode'] == 'cancel'); |
| 779: | |
| 780: | $realm = $request->getRealm(); |
| 781: | $request_state = new RequestState(); |
| 782: | $request_state->setParams($request->toArray()); |
| 783: | |
| 784: | $this->f3->set('realm', $realm); |
| 785: | |
| 786: | if ($cancel) { |
| 787: | $this->f3->set('requested_identity', $request['openid.identity']); |
| 788: | $this->f3->set('switch_url', $this->getCanonicalURL('auth/logout/continue/' . rawurlencode($token->generate($request_state)), '', 'detect')); |
| 789: | } else { |
| 790: | $base_path = $this->f3->get('base_path'); |
| 791: | |
| 792: | $form_state['prefs'] = (isset($user->clients[$realm])) ? $user->clients[$realm] : []; |
| 793: | |
| 794: | $event = new FormBuildEvent($form_state, 'openid_consent_form_build'); |
| 795: | \Events::instance()->dispatch($event); |
| 796: | $tpl->mergeAttachments($event); |
| 797: | $this->f3->set('forms', $event->getBlocks()); |
| 798: | |
| 799: | if ($reason == self::CHECKID_RETURN_TO_SUSPECT) { |
| 800: | $this->f3->set('return_to_suspect', true); |
| 801: | $this->f3->set('suspect_url', 'http://simpleid.org/documentation/troubleshooting/returnto-discovery-failure'); |
| 802: | $this->f3->set('js_data.intl.openid_suspect', $this->f3->get('intl.core.openid.suspect_js_1') . "\n\n" . $this->f3->get('intl.core.openid.suspect_js_2')); |
| 803: | } |
| 804: | } |
| 805: | |
| 806: | $this->f3->set('tk', $token->generate('openid_consent', SecurityToken::OPTION_BIND_SESSION)); |
| 807: | $this->f3->set('fs', $token->generate($form_state->encode())); |
| 808: | |
| 809: | $this->f3->set('cancel', $cancel); |
| 810: | |
| 811: | $this->f3->set('logout_destination', '/continue/' . rawurlencode($token->generate($request_state))); |
| 812: | $this->f3->set('user_header', true); |
| 813: | $this->f3->set('title', $this->f3->get('intl.core.openid.openid_title')); |
| 814: | $this->f3->set('page_class', 'is-dialog-page'); |
| 815: | $this->f3->set('layout', 'openid_consent.html'); |
| 816: | |
| 817: | header('X-Frame-Options: DENY'); |
| 818: | print $tpl->render('page.html'); |
| 819: | } |
| 820: | |
| 821: | |
| 822: | |
| 823: | |
| 824: | |
| 825: | |
| 826: | |
| 827: | |
| 828: | |
| 829: | public function consent() { |
| 830: | $auth = AuthManager::instance(); |
| 831: | $token = new SecurityToken(); |
| 832: | $store = StoreManager::instance(); |
| 833: | |
| 834: | if (!$auth->isLoggedIn()) { |
| 835: | |
| 836: | $auth_module = $this->mgr->getModule('SimpleID\Auth\AuthModule'); |
| 837: | $auth_module->loginForm(); |
| 838: | return; |
| 839: | } |
| 840: | $user = $auth->getUser(); |
| 841: | |
| 842: | $form_state = FormState::decode($token->getPayload($this->f3->get('POST.fs')), Request::class, Response::class); |
| 843: | |
| 844: | $request = $form_state->getRequest(); |
| 845: | |
| 846: | $response = $form_state->getResponse(); |
| 847: | $reason = $form_state['code']; |
| 848: | |
| 849: | if (!$token->verify($this->f3->get('POST.tk'), 'openid_consent')) { |
| 850: | $this->logger->log(LogLevel::WARNING, 'Security token ' . $this->f3->get('POST.tk') . ' invalid.'); |
| 851: | $this->f3->set('message', $this->f3->get('intl.common.invalid_tk')); |
| 852: | $this->consentForm($request, $response, $reason); |
| 853: | return; |
| 854: | } |
| 855: | |
| 856: | $return_to = $response['return_to']; |
| 857: | if ($return_to == null) $return_to = $request['openid.return_to']; |
| 858: | |
| 859: | if ($this->f3->get('POST.op') == 'cancel') { |
| 860: | $response = $this->createErrorResponse($request, false); |
| 861: | if (!$return_to) $this->f3->set('message', $this->f3->get('intl.common.login_cancelled')); |
| 862: | } else { |
| 863: | $event = new FormSubmitEvent($form_state, 'openid_consent_form_submit'); |
| 864: | \Events::instance()->dispatch($event); |
| 865: | |
| 866: | $consents = [ 'openid' => ($this->f3->exists('POST.prefs.consents.openid') && ($this->f3->get('POST.prefs.consents.openid') == 'true')) ]; |
| 867: | $this->logActivity($request, $consents); |
| 868: | |
| 869: | $this->signResponse($response, isset($response['assoc_handle']) ? $response['assoc_handle'] : NULL); |
| 870: | if (!$return_to) $this->f3->set('message', $this->f3->get('intl.common.login_success')); |
| 871: | } |
| 872: | |
| 873: | if ($return_to) { |
| 874: | $response->render($return_to); |
| 875: | } else { |
| 876: | $this->f3->reroute('/'); |
| 877: | } |
| 878: | } |
| 879: | |
| 880: | |
| 881: | |
| 882: | |
| 883: | |
| 884: | |
| 885: | |
| 886: | |
| 887: | public function onLoginFormCancel(FormSubmitEvent $event) { |
| 888: | $form_state = $event->getFormState(); |
| 889: | |
| 890: | if ($form_state['cancel'] == 'openid') { |
| 891: | |
| 892: | $request = $form_state->getRequest(); |
| 893: | if (isset($request['openid.return_to'])) { |
| 894: | $return_to = $request['openid.return_to']; |
| 895: | $response = $this->createErrorResponse($request, FALSE); |
| 896: | $response->render($return_to); |
| 897: | $event->stopPropagation(); |
| 898: | return; |
| 899: | } |
| 900: | } |
| 901: | } |
| 902: | |
| 903: | |
| 904: | |
| 905: | |
| 906: | |
| 907: | |
| 908: | |
| 909: | |
| 910: | protected function logActivity($request, $consents = NULL) { |
| 911: | $store = StoreManager::instance(); |
| 912: | |
| 913: | $auth = AuthManager::instance(); |
| 914: | $user = $auth->getUser(); |
| 915: | |
| 916: | $realm = $request->getRealm(); |
| 917: | $cid = RelyingParty::buildID($realm); |
| 918: | $relying_party = $this->loadRelyingParty($realm, true); |
| 919: | |
| 920: | $now = time(); |
| 921: | |
| 922: | $event = new ProtocolResultEvent(self::CHECKID_OK, $user, $this->loadRelyingParty($realm, true)); |
| 923: | \Events::instance()->dispatch($event); |
| 924: | |
| 925: | if (isset($user->clients[$cid])) { |
| 926: | $prefs = $user->clients[$cid]; |
| 927: | } else { |
| 928: | $prefs = [ |
| 929: | 'openid' => [ |
| 930: | 'version' => $request->getVersion() |
| 931: | ], |
| 932: | 'store_id' => $relying_party->getStoreID(), |
| 933: | 'display_name' => $relying_party->getDisplayName(), |
| 934: | 'display_html' => $relying_party->getDisplayHTML(), |
| 935: | 'first_time' => $now, |
| 936: | 'consents' => [] |
| 937: | ]; |
| 938: | } |
| 939: | |
| 940: | $prefs['last_time'] = $now; |
| 941: | |
| 942: | if ($consents != null) { |
| 943: | $prefs['consents'] = $consents; |
| 944: | } |
| 945: | |
| 946: | $user->clients[$cid] = $prefs; |
| 947: | $store->saveUser($user); |
| 948: | } |
| 949: | |
| 950: | |
| 951: | |
| 952: | |
| 953: | |
| 954: | |
| 955: | |
| 956: | |
| 957: | |
| 958: | |
| 959: | |
| 960: | |
| 961: | protected function directError($error, $additional = [], $request = NULL) { |
| 962: | $this->f3->status(400); |
| 963: | |
| 964: | $error = Response::createError($error, $additional, $request); |
| 965: | $error->render(); |
| 966: | } |
| 967: | |
| 968: | |
| 969: | |
| 970: | |
| 971: | |
| 972: | |
| 973: | |
| 974: | |
| 975: | |
| 976: | |
| 977: | |
| 978: | |
| 979: | protected function indirectError($url, $error, $additional = [], $request = NULL) { |
| 980: | $error = Response::createError($error, $additional, $request); |
| 981: | $error->render($url); |
| 982: | } |
| 983: | |
| 984: | |
| 985: | |
| 986: | |
| 987: | |
| 988: | |
| 989: | |
| 990: | |
| 991: | |
| 992: | |
| 993: | |
| 994: | |
| 995: | |
| 996: | public function loadRelyingParty($realm, $allow_stale = false) { |
| 997: | $store = StoreManager::instance(); |
| 998: | |
| 999: | $cid = RelyingParty::buildID($realm); |
| 1000: | |
| 1001: | |
| 1002: | $relying_party = $store->loadClient($cid); |
| 1003: | if ($relying_party == null) $relying_party = new RelyingParty($realm); |
| 1004: | |
| 1005: | if ((time() - $relying_party->getDiscoveryTime() > SIMPLEID_SHORT_TOKEN_EXPIRES_IN) && !$allow_stale) { |
| 1006: | $this->logger->log(LogLevel::INFO, 'OpenID 2 RP discovery: realm: ' . $realm); |
| 1007: | $relying_party->discover(); |
| 1008: | $store->saveClient($relying_party); |
| 1009: | } |
| 1010: | |
| 1011: | return $relying_party; |
| 1012: | } |
| 1013: | |
| 1014: | |
| 1015: | |
| 1016: | |
| 1017: | |
| 1018: | |
| 1019: | public function providerXRDS() { |
| 1020: | $this->logger->log(LogLevel::DEBUG, 'Providing XRDS.'); |
| 1021: | |
| 1022: | $tpl = Template::instance(); |
| 1023: | |
| 1024: | $event = new BaseDataCollectionEvent('xrds_types'); |
| 1025: | \Events::instance()->dispatch($event); |
| 1026: | |
| 1027: | $this->f3->set('types', $event->getResults()); |
| 1028: | |
| 1029: | header('Content-Disposition: inline; filename=yadis.xml'); |
| 1030: | print $tpl->render('openid_provider_xrds.xml', 'application/xrds+xml'); |
| 1031: | } |
| 1032: | |
| 1033: | |
| 1034: | |
| 1035: | |
| 1036: | |
| 1037: | |
| 1038: | |
| 1039: | public function userXRDS() { |
| 1040: | $store = StoreManager::instance(); |
| 1041: | $user = $store->loadUser($this->f3->get('PARAMS.uid')); |
| 1042: | |
| 1043: | if ($user != NULL) { |
| 1044: | $tpl = Template::instance(); |
| 1045: | |
| 1046: | if ($user->hasLocalOpenIDIdentity()) { |
| 1047: | $this->f3->set('local_id', $user['openid']["identity"]); |
| 1048: | } |
| 1049: | header('Content-Disposition: inline; filename=yadis.xml'); |
| 1050: | print $tpl->render('openid_user_xrds.xml', 'application/xrds+xml'); |
| 1051: | } else { |
| 1052: | $this->fatalError($this->f3->get('intl.common.user_not_found', $this->f3->get('PARAMS.uid')), 404); |
| 1053: | } |
| 1054: | } |
| 1055: | |
| 1056: | |
| 1057: | |
| 1058: | |
| 1059: | |
| 1060: | |
| 1061: | |
| 1062: | public function onScopeInfoCollectionEvent(ScopeInfoCollectionEvent $event) { |
| 1063: | $event->addScopeInfo('openid', [ |
| 1064: | self::DEFAULT_SCOPE => [ |
| 1065: | 'description' => $this->f3->get('intl.common.scope.id'), |
| 1066: | ] |
| 1067: | ]); |
| 1068: | } |
| 1069: | |
| 1070: | |
| 1071: | |
| 1072: | |
| 1073: | |
| 1074: | |
| 1075: | |
| 1076: | |
| 1077: | public function onProfileBlocks(UIBuildEvent $event) { |
| 1078: | $auth = AuthManager::instance(); |
| 1079: | $user = $auth->getUser(); |
| 1080: | $tpl = Template::instance(); |
| 1081: | |
| 1082: | $this->f3->set('js_data.intl', [ 'code' => addslashes($this->f3->get('intl.core.openid.profile_js')) ]); |
| 1083: | |
| 1084: | $xrds_url = $this->getCanonicalURL('user/'. $user['uid'] . '/xrds', '', 'detect'); |
| 1085: | $hive = [ |
| 1086: | 'config' => $this->f3->get('config'), |
| 1087: | 'intl' => $this->f3->get('intl'), |
| 1088: | 'user' => $user, |
| 1089: | 'xrds_url' => $xrds_url |
| 1090: | ]; |
| 1091: | |
| 1092: | $event->addBlock('discovery', $tpl->render('openid_profile.html', false, $hive), 1, [ |
| 1093: | 'title' => $this->f3->get('intl.core.openid.discovery_title'), |
| 1094: | 'links' => [ [ 'href' => 'http://simpleid.org/documentation/getting-started/setting-identity/claim-your-identifier', 'name' => $this->f3->get('intl.common.more_info') ] ], |
| 1095: | ]); |
| 1096: | } |
| 1097: | } |
| 1098: | |
| 1099: | ?> |
| 1100: | |