| 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\OAuth; |
| 24: | |
| 25: | use Psr\Log\LogLevel; |
| 26: | use SimpleID\Auth\AuthManager; |
| 27: | use SimpleID\Module; |
| 28: | use SimpleID\ModuleManager; |
| 29: | use SimpleID\Base\ScopeInfoCollectionEvent; |
| 30: | use SimpleID\Base\ConsentEvent; |
| 31: | use SimpleID\Base\RequestState; |
| 32: | use SimpleID\Crypt\SecurityToken; |
| 33: | use SimpleID\Protocols\ProtocolResult; |
| 34: | use SimpleID\Protocols\ProtocolResultEvent; |
| 35: | use SimpleID\Store\StoreManager; |
| 36: | use SimpleID\Util\Events\GenericStoppableEvent; |
| 37: | use SimpleID\Util\Events\BaseDataCollectionEvent; |
| 38: | use SimpleID\Util\Forms\FormState; |
| 39: | use SimpleID\Util\Forms\FormBuildEvent; |
| 40: | use SimpleID\Util\Forms\FormSubmitEvent; |
| 41: | use SimpleID\Util\UI\Template; |
| 42: | |
| 43: | |
| 44: | |
| 45: | |
| 46: | |
| 47: | |
| 48: | |
| 49: | class OAuthModule extends Module implements ProtocolResult { |
| 50: | |
| 51: | const DEFAULT_SCOPE = 'tag:simpleid.sf.net,2014:oauth:default'; |
| 52: | |
| 53: | |
| 54: | static private $oauth_scope_settings = NULL; |
| 55: | |
| 56: | |
| 57: | protected $oauth; |
| 58: | |
| 59: | |
| 60: | protected $mgr; |
| 61: | |
| 62: | static function init($f3) { |
| 63: | $f3->route('GET @oauth_auth: /oauth/auth', 'SimpleID\Protocols\OAuth\OAuthModule->auth'); |
| 64: | $f3->route('POST @oauth_token: /oauth/token', 'SimpleID\Protocols\OAuth\OAuthModule->token'); |
| 65: | $f3->route('POST @oauth_consent: /oauth/consent', 'SimpleID\Protocols\OAuth\OAuthModule->consent'); |
| 66: | $f3->route('POST @oauth_revoke: /oauth/revoke', 'SimpleID\Protocols\OAuth\OAuthModule->revoke'); |
| 67: | $f3->route('POST @oauth_introspect: /oauth/introspect', 'SimpleID\Protocols\OAuth\OAuthModule->introspect'); |
| 68: | $f3->route('GET @oauth_metadata: /.well-known/oauth-authorization-server', 'SimpleID\Protocols\OAuth\OAuthModule->metadata'); |
| 69: | } |
| 70: | |
| 71: | public function __construct() { |
| 72: | parent::__construct(); |
| 73: | $this->oauth = OAuthManager::instance(); |
| 74: | $this->mgr = ModuleManager::instance(); |
| 75: | } |
| 76: | |
| 77: | |
| 78: | |
| 79: | |
| 80: | |
| 81: | |
| 82: | |
| 83: | public function onPostInit(GenericStoppableEvent $event) { |
| 84: | $event = new ScopeInfoCollectionEvent(); |
| 85: | \Events::instance()->dispatch($event); |
| 86: | |
| 87: | self::$oauth_scope_settings = $event->getScopeInfoForType('oauth'); |
| 88: | } |
| 89: | |
| 90: | |
| 91: | |
| 92: | |
| 93: | |
| 94: | |
| 95: | |
| 96: | |
| 97: | |
| 98: | |
| 99: | |
| 100: | |
| 101: | |
| 102: | public function auth() { |
| 103: | $this->checkHttps('redirect'); |
| 104: | |
| 105: | $dispatcher = \Events::instance(); |
| 106: | |
| 107: | $request = new Request($this->f3->get('GET'), []); |
| 108: | |
| 109: | $this->logger->log(LogLevel::INFO, 'OAuth authorisation request: ', $request->toArray()); |
| 110: | |
| 111: | $response = new Response($request); |
| 112: | |
| 113: | $resolve_event = new OAuthEvent($request, $response, 'oauth_auth_resolve'); |
| 114: | $dispatcher->dispatch($resolve_event); |
| 115: | if ($response->isError()) { |
| 116: | if (isset($request['redirect_uri'])) { |
| 117: | $response->renderRedirect(); |
| 118: | } else { |
| 119: | $this->fatalError($this->f3->get('intl.common.protocol_error', $response['error']), 400); |
| 120: | } |
| 121: | return; |
| 122: | } |
| 123: | |
| 124: | $this->checkAuthRequest($request, $response); |
| 125: | |
| 126: | $resolve_event = new OAuthEvent($request, $response, 'oauth_auth_check'); |
| 127: | $dispatcher->dispatch($resolve_event); |
| 128: | if ($response->isError()) { |
| 129: | if (isset($request['redirect_uri'])) { |
| 130: | $response->renderRedirect(); |
| 131: | } else { |
| 132: | $this->fatalError($this->f3->get('intl.common.protocol_error', $response['error']), 400); |
| 133: | } |
| 134: | return; |
| 135: | } |
| 136: | |
| 137: | $this->processAuthRequest($request, $response); |
| 138: | } |
| 139: | |
| 140: | |
| 141: | |
| 142: | |
| 143: | |
| 144: | |
| 145: | |
| 146: | |
| 147: | |
| 148: | protected function checkAuthRequest($request, $response) { |
| 149: | $store = StoreManager::instance(); |
| 150: | |
| 151: | |
| 152: | if (!isset($request['response_type'])) { |
| 153: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: response_type not set.'); |
| 154: | $this->fatalError($this->f3->get('intl.core.oauth.missing_response_type'), 400); |
| 155: | return; |
| 156: | } |
| 157: | |
| 158: | $response_types = preg_split('/\s+/', $request['response_type']); |
| 159: | if ($response_types == false) { |
| 160: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: Incorrect response_type.'); |
| 161: | $this->fatalError($this->f3->get('intl.core.oauth.invalid_response_type'), 400); |
| 162: | return; |
| 163: | } |
| 164: | if (in_array('token', $response_types)) $response->setResponseMode(Response::FRAGMENT_RESPONSE_MODE); |
| 165: | |
| 166: | |
| 167: | if (!isset($request['client_id'])) { |
| 168: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: client_id not set'); |
| 169: | if (isset($request['redirect_uri'])) { |
| 170: | $response->setError('invalid_request', 'client_id not set')->renderRedirect(); |
| 171: | } else { |
| 172: | $this->fatalError($this->f3->get('intl.core.oauth.missing_client_id'), 400); |
| 173: | } |
| 174: | return; |
| 175: | } |
| 176: | |
| 177: | |
| 178: | $client = $store->loadClient($request['client_id'], 'SimpleID\Protocols\OAuth\OAuthClient'); |
| 179: | if ($client == NULL) { |
| 180: | $this->logger->log(LogLevel::ERROR, 'Client with client_id not found: ' . $request['client_id']); |
| 181: | if (isset($request['redirect_uri'])) { |
| 182: | $response->setError('invalid_request', 'client not found')->renderRedirect(); |
| 183: | } else { |
| 184: | $this->fatalError($this->f3->get('intl.core.oauth.client_not_found'), 400); |
| 185: | } |
| 186: | return; |
| 187: | } |
| 188: | |
| 189: | |
| 190: | if (isset($request['redirect_uri'])) { |
| 191: | |
| 192: | if (!$client->hasRedirectUri($request['redirect_uri'])) { |
| 193: | $this->logger->log(LogLevel::ERROR, 'Incorrect redirect URI: ' . $request['redirect_uri']); |
| 194: | $this->fatalError($this->f3->get('intl.core.oauth.invalid_redirect_uri'), 400); |
| 195: | return; |
| 196: | } |
| 197: | } elseif (isset($client['oauth']['redirect_uris'])) { |
| 198: | if (is_string($client['oauth']['redirect_uris'])) { |
| 199: | $response->setRedirectURI($client['oauth']['redirect_uris']); |
| 200: | } elseif (count($client['oauth']['redirect_uris']) == 1) { |
| 201: | $response->setRedirectURI($client['oauth']['redirect_uris'][0]); |
| 202: | } else { |
| 203: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: redirect_uri not specified in request when multiple redirect_uris are registered'); |
| 204: | $this->fatalError($this->f3->get('intl.core.oauth.ambiguous_redirect_uri'), 400); |
| 205: | return; |
| 206: | } |
| 207: | } else { |
| 208: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: redirect_uri not specified in request or client registration'); |
| 209: | $this->fatalError($this->f3->get('intl.core.oauth.missing_redirect_uri'), 400); |
| 210: | return; |
| 211: | } |
| 212: | |
| 213: | |
| 214: | $event = new BaseDataCollectionEvent('oauth_response_types'); |
| 215: | \Events::instance()->dispatch($event); |
| 216: | |
| 217: | $supported_response_types = $event->getResults(); |
| 218: | foreach ($response_types as $response_type) { |
| 219: | if (!in_array($response_type, $supported_response_types)) { |
| 220: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: unsupported response_type: ' . $response_type); |
| 221: | $response->setError('unsupported_response_type', 'unsupported response_type: ' . $response_type)->renderRedirect(); |
| 222: | return; |
| 223: | } |
| 224: | } |
| 225: | |
| 226: | |
| 227: | if ($client->isNative() && $request->paramContains('response_type', 'code') && !isset($request['code_challenge'])) { |
| 228: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: code_challenge required for native apps'); |
| 229: | $response->setError('invalid_request', 'code_challenge required for native apps')->renderRedirect(); |
| 230: | } |
| 231: | } |
| 232: | |
| 233: | |
| 234: | |
| 235: | |
| 236: | |
| 237: | |
| 238: | |
| 239: | |
| 240: | |
| 241: | |
| 242: | |
| 243: | |
| 244: | |
| 245: | protected function processAuthRequest($request, $response) { |
| 246: | $this->logger->log(LogLevel::INFO, 'Expanded OAuth authorisation request: ', $request->toArray()); |
| 247: | |
| 248: | $core_result = $this->checkIdentity($request); |
| 249: | |
| 250: | $event = new OAuthAuthRequestEvent($request, $response); |
| 251: | $event->setResult($core_result); |
| 252: | \Events::instance()->dispatch($event); |
| 253: | $result = $event->getResult(); |
| 254: | |
| 255: | switch ($result) { |
| 256: | case self::CHECKID_OK: |
| 257: | $this->logger->log(LogLevel::INFO, 'CHECKID_OK'); |
| 258: | |
| 259: | if (isset($request['scope'])) { |
| 260: | $scopes = $request->paramToArray('scope'); |
| 261: | } else { |
| 262: | $scopes = [ self::DEFAULT_SCOPE ]; |
| 263: | } |
| 264: | $this->grantAuth($request, $response, $scopes); |
| 265: | break; |
| 266: | case self::CHECKID_APPROVAL_REQUIRED: |
| 267: | $this->logger->log(LogLevel::INFO, 'CHECKID_APPROVAL_REQUIRED'); |
| 268: | if ($request->isImmediate()) { |
| 269: | $response->setError('consent_required', 'Consent required')->renderRedirect(); |
| 270: | } else { |
| 271: | $this->consentForm($request, $response); |
| 272: | } |
| 273: | break; |
| 274: | case self::CHECKID_REENTER_CREDENTIALS: |
| 275: | case self::CHECKID_LOGIN_REQUIRED: |
| 276: | $this->logger->log(LogLevel::INFO, 'CHECKID_LOGIN_REQUIRED'); |
| 277: | if ($request->isImmediate()) { |
| 278: | $response->setError('login_required', 'Login required')->renderRedirect(); |
| 279: | } else { |
| 280: | $token = new SecurityToken(); |
| 281: | $request_state = new RequestState(); |
| 282: | $request_state->setRoute('/oauth/auth')->setParams($request->toArray()); |
| 283: | $form_state = new FormState([ |
| 284: | 'mode' => AuthManager::MODE_CREDENTIALS, |
| 285: | 'auth_skip_activity' => true |
| 286: | ]); |
| 287: | $form_state->setRequest($request); |
| 288: | if ($result == self::CHECKID_REENTER_CREDENTIALS) { |
| 289: | $auth = AuthManager::instance(); |
| 290: | $user = $auth->getUser(); |
| 291: | $form_state['uid'] = $user['uid']; |
| 292: | $form_state['mode'] = AuthManager::MODE_REENTER_CREDENTIALS; |
| 293: | } |
| 294: | |
| 295: | |
| 296: | $auth_module = $this->mgr->getModule('SimpleID\Auth\AuthModule'); |
| 297: | $auth_module->loginForm([ |
| 298: | 'destination' => 'continue/' . rawurlencode($token->generate($request_state)) |
| 299: | ], $form_state); |
| 300: | exit; |
| 301: | } |
| 302: | break; |
| 303: | case self::CHECKID_INSUFFICIENT_TRUST: |
| 304: | $this->logger->log(LogLevel::INFO, 'CHECKID_INSUFFICIENT_TRUST'); |
| 305: | $response->setError('invalid_request', 'SimpleID does not support the requested level of trust')->renderRedirect(); |
| 306: | break; |
| 307: | } |
| 308: | } |
| 309: | |
| 310: | |
| 311: | |
| 312: | |
| 313: | |
| 314: | |
| 315: | |
| 316: | |
| 317: | |
| 318: | |
| 319: | protected function checkIdentity($request) { |
| 320: | $auth = AuthManager::instance(); |
| 321: | $store = StoreManager::instance(); |
| 322: | |
| 323: | |
| 324: | if (!$auth->isLoggedIn()) { |
| 325: | return self::CHECKID_LOGIN_REQUIRED; |
| 326: | } else { |
| 327: | $user = $auth->getUser(); |
| 328: | $uid = $user['uid']; |
| 329: | } |
| 330: | |
| 331: | |
| 332: | $cid = $request['client_id']; |
| 333: | |
| 334: | $client_prefs = isset($user->clients[$cid]) ? $user->clients[$cid] : NULL; |
| 335: | |
| 336: | if (isset($client_prefs['consents']['oauth'])) { |
| 337: | $consents = $client_prefs['consents']['oauth']; |
| 338: | } else { |
| 339: | return self::CHECKID_APPROVAL_REQUIRED; |
| 340: | } |
| 341: | |
| 342: | |
| 343: | if (isset($request['scope'])) { |
| 344: | $scopes = $request->paramToArray('scope'); |
| 345: | } else { |
| 346: | $scopes = [ self::DEFAULT_SCOPE ]; |
| 347: | } |
| 348: | if (count(array_diff($scopes, $consents)) > 0) return self::CHECKID_APPROVAL_REQUIRED; |
| 349: | |
| 350: | return self::CHECKID_OK; |
| 351: | } |
| 352: | |
| 353: | |
| 354: | |
| 355: | |
| 356: | |
| 357: | |
| 358: | |
| 359: | |
| 360: | |
| 361: | |
| 362: | |
| 363: | protected function grantAuth($request, $response, $scopes = NULL) { |
| 364: | $dispatcher = \Events::instance(); |
| 365: | $store = StoreManager::instance(); |
| 366: | |
| 367: | $user = AuthManager::instance()->getUser(); |
| 368: | $client = $store->loadClient($request['client_id'], 'SimpleID\Protocols\OAuth\OAuthClient'); |
| 369: | if ($scopes == NULL) { |
| 370: | if (isset($request['scope'])) { |
| 371: | $scopes = $request->paramToArray('scope'); |
| 372: | } else { |
| 373: | $scopes = [ self::DEFAULT_SCOPE ]; |
| 374: | } |
| 375: | } |
| 376: | |
| 377: | |
| 378: | $authorization = $store->loadAuth(Authorization::buildID($user, $client)); |
| 379: | |
| 380: | if ($authorization == null) { |
| 381: | $authorization = new Authorization($user, $client, $scopes); |
| 382: | } else { |
| 383: | $authorization->setScope($scopes); |
| 384: | } |
| 385: | |
| 386: | $result_event = new ProtocolResultEvent(self::CHECKID_OK, $user, $client); |
| 387: | $dispatcher->dispatch($result_event); |
| 388: | |
| 389: | if ($request->paramContains('response_type', 'code')) { |
| 390: | $additional = []; |
| 391: | if (isset($request['code_challenge'])) { |
| 392: | $additional['code_challenge'] = $request['code_challenge']; |
| 393: | $additional['code_challenge_method'] = (isset($request['code_challenge_method'])) ? $request['code_challenge_method'] : 'plain'; |
| 394: | } |
| 395: | $response['code'] = $authorization->issueCode((isset($request['redirect_uri'])) ? $request['redirect_uri'] : NULL, NULL, $additional); |
| 396: | } |
| 397: | |
| 398: | if ($request->paramContains('response_type', 'token')) { |
| 399: | $response->loadData($authorization->issueAccessToken($scopes, SIMPLEID_SHORT_TOKEN_EXPIRES_IN)); |
| 400: | |
| 401: | $token_event = new OAuthTokenGrantEvent('implicit', $authorization, $request, $response, $scopes); |
| 402: | $dispatcher->dispatch($token_event); |
| 403: | } |
| 404: | |
| 405: | $grant_auth_event = new OAuthAuthGrantEvent($authorization, $request, $response, $scopes); |
| 406: | $dispatcher->dispatch($grant_auth_event); |
| 407: | |
| 408: | $store->saveAuth($authorization); |
| 409: | $store->saveUser($user); |
| 410: | |
| 411: | $this->logger->log(LogLevel::DEBUG, 'Authorization granted: ', $response->toArray()); |
| 412: | |
| 413: | $response->renderRedirect(); |
| 414: | } |
| 415: | |
| 416: | |
| 417: | |
| 418: | |
| 419: | |
| 420: | |
| 421: | |
| 422: | public function token() { |
| 423: | $request = new Request($this->f3->get('POST')); |
| 424: | $response = new Response($request); |
| 425: | |
| 426: | $this->checkHttps('error'); |
| 427: | |
| 428: | $this->logger->log(LogLevel::INFO, 'OAuth token request: ', $request->toArray()); |
| 429: | |
| 430: | if (!isset($request['grant_type'])) { |
| 431: | $this->logger->log(LogLevel::ERROR, 'Protocol Error: grant_type not set.'); |
| 432: | $response->setError('invalid_request', 'grant_type not set'); |
| 433: | $response->renderJSON(); |
| 434: | return; |
| 435: | } |
| 436: | |
| 437: | $this->oauth->initClient(); |
| 438: | $client = $this->oauth->getClient(); |
| 439: | |
| 440: | if (!$this->oauth->isClientAuthenticated(true, isset($client['oauth']['token_endpoint_auth_method']) ? $client['oauth']['token_endpoint_auth_method'] : null)) { |
| 441: | $this->logger->log(LogLevel::ERROR, 'Client authentication failed'); |
| 442: | $response->setError('invalid_client', 'client authentication failed'); |
| 443: | $response->renderJSON(); |
| 444: | return; |
| 445: | } |
| 446: | |
| 447: | $grant_types = (isset($client['oauth']['grant_types'])) ? $client['oauth']['grant_types'] : [ 'authorization_code' ]; |
| 448: | if (!in_array($request['grant_type'], $grant_types)) { |
| 449: | $this->logger->log(LogLevel::ERROR, 'Grant type not registered by client'); |
| 450: | $response->setError('unauthorized_client', 'Grant type not registered by client'); |
| 451: | $response->renderJSON(); |
| 452: | return; |
| 453: | } |
| 454: | |
| 455: | switch ($request['grant_type']) { |
| 456: | case 'authorization_code': |
| 457: | $this->tokenFromCode($request, $response); |
| 458: | break; |
| 459: | case 'refresh_token': |
| 460: | $this->tokenFromRefreshToken($request, $response); |
| 461: | break; |
| 462: | case 'password': |
| 463: | case 'client_credentials': |
| 464: | |
| 465: | default: |
| 466: | |
| 467: | $this->logger->log(LogLevel::ERROR, 'Token request failed: unsupported grant type'); |
| 468: | $response->setError('unsupported_grant_type', 'grant type ' . $request['grant_type'] . ' is not supported'); |
| 469: | break; |
| 470: | } |
| 471: | |
| 472: | $this->logger->log(LogLevel::DEBUG, 'Token response: ', $response->toArray()); |
| 473: | $response->renderJSON(); |
| 474: | } |
| 475: | |
| 476: | |
| 477: | |
| 478: | |
| 479: | |
| 480: | |
| 481: | |
| 482: | |
| 483: | |
| 484: | protected function tokenFromCode($request, $response) { |
| 485: | |
| 486: | if (!isset($request['code']) || ($request['code'] == '')) { |
| 487: | $this->logger->log(LogLevel::ERROR, 'Token request failed: code not set'); |
| 488: | $response->setError('invalid_request', 'code not set'); |
| 489: | return; |
| 490: | } |
| 491: | |
| 492: | |
| 493: | $code = Code::decode($request['code']); |
| 494: | $authorization = $code->getAuthorization(); |
| 495: | if ($authorization == null) { |
| 496: | $this->logger->log(LogLevel::ERROR, 'Token request failed: Authorisation not found or expired'); |
| 497: | $response->setError('invalid_grant', 'Authorization code not found or expired'); |
| 498: | return; |
| 499: | } |
| 500: | $authorization->revokeTokensFromGrant($code); |
| 501: | |
| 502: | |
| 503: | |
| 504: | if (!$code->isValid()) { |
| 505: | $this->logger->log(LogLevel::ERROR, 'Token request failed: Authorisation code not found or expired: ' . $request['code']); |
| 506: | $response->setError('invalid_grant', 'Authorization code not found or expired'); |
| 507: | return; |
| 508: | } |
| 509: | |
| 510: | |
| 511: | if ($code->getRedirectURI()) { |
| 512: | if (!isset($request['redirect_uri']) || ($code->getRedirectURI() != $request['redirect_uri'])) { |
| 513: | $this->logger->log(LogLevel::ERROR, 'Token request failed: redirect_uri in request <' . $request['redirect_uri'] . '> does not match authorisation code <' . $code->getRedirectURI() . '>'); |
| 514: | $response->setError('invalid_grant', 'redirect_uri does not match'); |
| 515: | return; |
| 516: | } |
| 517: | } |
| 518: | |
| 519: | |
| 520: | $additional = $code->getAdditional(); |
| 521: | if (isset($additional['code_challenge'])) { |
| 522: | if (!isset($request['code_verifier'])) { |
| 523: | $this->logger->log(LogLevel::ERROR, 'Token request failed: code_verifier not found'); |
| 524: | $response->setError('invalid_grant', 'code_verifier not found'); |
| 525: | return; |
| 526: | } |
| 527: | |
| 528: | $code_verified = false; |
| 529: | switch ($additional['code_challenge_method']) { |
| 530: | case 'plain': |
| 531: | $test_code_challenge = $request['code_verifier']; |
| 532: | break; |
| 533: | case 'S256': |
| 534: | $test_code_challenge = trim(strtr(base64_encode(hash('sha256', $request['code_verifier'], true)), '+/', '-_'), '='); |
| 535: | break; |
| 536: | default: |
| 537: | $this->logger->log(LogLevel::ERROR, 'Token request failed: unknown code_challenge_method: ' . $additional['code_challenge_method']); |
| 538: | $response->setError('invalid_grant', 'unknown code_challenge_method'); |
| 539: | return; |
| 540: | } |
| 541: | $code_verified = $this->secureCompare($test_code_challenge, $additional['code_challenge']); |
| 542: | if (!$code_verified) { |
| 543: | $this->logger->log(LogLevel::ERROR, 'Token request failed: code_challenge in request <' . $test_code_challenge . '> does not match stored code_challenge <' . $additional['code_challenge'] . '>'); |
| 544: | $response->setError('invalid_grant', 'code_verifier does not match'); |
| 545: | return; |
| 546: | } |
| 547: | } |
| 548: | |
| 549: | $scope = $code->getScope(); |
| 550: | |
| 551: | |
| 552: | $code->clear(); |
| 553: | |
| 554: | $response->loadData($authorization->issueTokens($scope, SIMPLEID_SHORT_TOKEN_EXPIRES_IN, $code)); |
| 555: | |
| 556: | |
| 557: | $event = new OAuthTokenGrantEvent('authorization_code', $authorization, $request, $response, $scope); |
| 558: | \Events::instance()->dispatch($event); |
| 559: | } |
| 560: | |
| 561: | |
| 562: | |
| 563: | |
| 564: | |
| 565: | |
| 566: | |
| 567: | |
| 568: | protected function tokenFromRefreshToken($request, $response) { |
| 569: | $store = StoreManager::instance(); |
| 570: | $client = $this->oauth->getClient(); |
| 571: | |
| 572: | if (!isset($request['refresh_token']) || ($request['refresh_token'] == '')) { |
| 573: | $this->logger->log(LogLevel::ERROR, 'Token request failed: refresh_token not set'); |
| 574: | $response->setError('invalid_request', 'refresh_token not set'); |
| 575: | return; |
| 576: | } |
| 577: | |
| 578: | $refresh_token = RefreshToken::decode($request['refresh_token']); |
| 579: | if (!$refresh_token->isValid()) { |
| 580: | $this->logger->log(LogLevel::ERROR, 'Token request failed: Refresh token not valid'); |
| 581: | $response->setError('invalid_grant', 'Refresh token not valid'); |
| 582: | return; |
| 583: | } |
| 584: | |
| 585: | $authorization = $refresh_token->getAuthorization(); |
| 586: | if ($authorization->getClient()->getStoreID() != $client->getStoreID()) { |
| 587: | $this->logger->log(LogLevel::ERROR, 'Token request failed: this client (' . $client->getStoreID() . ') does not match the client stored in refresh token (' . $authorization->getClient()->getStoreID() . ')'); |
| 588: | $response->setError('invalid_grant', 'this client does not match the client stored in refresh token'); |
| 589: | return; |
| 590: | } |
| 591: | $authorization->revokeTokensFromGrant($refresh_token); |
| 592: | |
| 593: | $scope = $refresh_token->getScope(); |
| 594: | |
| 595: | |
| 596: | $refresh_token->revoke(); |
| 597: | $authorization->resetAuthState(); |
| 598: | $store->saveAuth($authorization); |
| 599: | |
| 600: | $response->loadData($authorization->issueTokens($scope, SIMPLEID_SHORT_TOKEN_EXPIRES_IN, $refresh_token)); |
| 601: | |
| 602: | |
| 603: | $event = new OAuthTokenGrantEvent('refresh_token', $authorization, $request, $response, $scope); |
| 604: | \Events::instance()->dispatch($event); |
| 605: | } |
| 606: | |
| 607: | |
| 608: | |
| 609: | |
| 610: | |
| 611: | |
| 612: | |
| 613: | |
| 614: | |
| 615: | |
| 616: | protected function consentForm($request, $response) { |
| 617: | $store = StoreManager::instance(); |
| 618: | $tpl = Template::instance(); |
| 619: | |
| 620: | $client = $store->loadClient($request['client_id'], 'SimpleID\Protocols\OAuth\OAuthClient'); |
| 621: | |
| 622: | $form_state = new FormState(); |
| 623: | $form_state->setRequest($request); |
| 624: | $form_state->setResponse($response); |
| 625: | |
| 626: | $request_state = new RequestState(); |
| 627: | $request_state->setParams($request->toArray()); |
| 628: | |
| 629: | $application_name = $client->getDisplayName(); |
| 630: | $application_type = (isset($client['oauth']['application_type'])) ? $client['oauth']['application_type'] : ''; |
| 631: | |
| 632: | $this->f3->set('application_name', $application_name); |
| 633: | $this->f3->set('application_type', $application_type); |
| 634: | |
| 635: | if (isset($client['logo_url'])) { |
| 636: | $this->f3->set('logo_url', $client['logo_url']); |
| 637: | } |
| 638: | |
| 639: | if (isset($request['scope'])) { |
| 640: | $scopes = $request->paramToArray('scope'); |
| 641: | } else { |
| 642: | $scopes = [ self::DEFAULT_SCOPE ]; |
| 643: | } |
| 644: | usort($scopes, [ $this, 'sortScopes' ]); |
| 645: | |
| 646: | $scope_list = []; |
| 647: | foreach ($scopes as $scope) { |
| 648: | $scope_list[$scope] = (isset(self::$oauth_scope_settings[$scope]['description'])) ? self::$oauth_scope_settings[$scope]['description'] : 'scope ' . $scope; |
| 649: | } |
| 650: | $this->f3->set('scope_list', $scope_list); |
| 651: | |
| 652: | if ($client->isDynamic()) { |
| 653: | $this->f3->set('client_dynamic', 'client-dynamic'); |
| 654: | } |
| 655: | |
| 656: | $client_info = []; |
| 657: | if (isset($client['oauth']['website'])) { |
| 658: | $client_info[] = $this->f3->get('intl.common.consent.website', $client['oauth']['website']); |
| 659: | } |
| 660: | if (isset($client['oauth']['policy_url'])) { |
| 661: | $client_info[] = $this->f3->get('intl.common.consent.policy_url', $client['oauth']['policy_url']); |
| 662: | } |
| 663: | if (isset($client['oauth']['tos_url'])) { |
| 664: | $client_info[] = $this->f3->get('intl.common.consent.tos_url', $client['oauth']['tos_url']); |
| 665: | } |
| 666: | if (isset($client['oauth']['contacts'])) { |
| 667: | $contacts = []; |
| 668: | |
| 669: | if (is_array($client['oauth']['contacts'])) { |
| 670: | foreach ($client['oauth']['contacts'] as $contact) { |
| 671: | $contacts[] = '<a href="mailto:' . $this->rfc3986_urlencode($contact) . '">' . $this->f3->clean($contact) . '</a>'; |
| 672: | } |
| 673: | } else { |
| 674: | $contacts[] = '<a href="mailto:' . $this->rfc3986_urlencode($client['oauth']['contacts']) . '">' . $this->f3->clean($client['oauth']['contacts']) . '</a>'; |
| 675: | } |
| 676: | |
| 677: | $client_info[] = $this->f3->get('intl.common.consent.contacts', implode(', ', $contacts)); |
| 678: | } |
| 679: | $this->f3->set('client_info', $client_info); |
| 680: | |
| 681: | $token = new SecurityToken(); |
| 682: | $this->f3->set('tk', $token->generate('oauth_consent', SecurityToken::OPTION_BIND_SESSION)); |
| 683: | $this->f3->set('fs', $token->generate($form_state->encode())); |
| 684: | |
| 685: | $this->f3->set('logout_destination', '/continue/' . rawurlencode($token->generate($request_state))); |
| 686: | $this->f3->set('user_header', true); |
| 687: | $this->f3->set('title', $this->f3->get('intl.core.oauth.oauth_title')); |
| 688: | $this->f3->set('page_class', 'is-dialog-page'); |
| 689: | $this->f3->set('layout', 'oauth_consent.html'); |
| 690: | |
| 691: | $event = new FormBuildEvent($form_state, 'oauth_consent_form_build'); |
| 692: | \Events::instance()->dispatch($event); |
| 693: | $tpl->mergeAttachments($event); |
| 694: | $this->f3->set('forms', $event->getBlocks()); |
| 695: | |
| 696: | header('X-Frame-Options: DENY'); |
| 697: | print $tpl->render('page.html'); |
| 698: | } |
| 699: | |
| 700: | |
| 701: | |
| 702: | |
| 703: | |
| 704: | |
| 705: | |
| 706: | |
| 707: | function consent() { |
| 708: | $auth = AuthManager::instance(); |
| 709: | $token = new SecurityToken(); |
| 710: | $store = StoreManager::instance(); |
| 711: | |
| 712: | if (!$auth->isLoggedIn()) { |
| 713: | |
| 714: | $auth_module = $this->mgr->getModule('SimpleID\Auth\AuthModule'); |
| 715: | $auth_module->loginForm(); |
| 716: | return; |
| 717: | } |
| 718: | $user = $auth->getUser(); |
| 719: | |
| 720: | $form_state = FormState::decode($token->getPayload($this->f3->get('POST.fs')), Request::class, Response::class); |
| 721: | |
| 722: | $request = $form_state->getRequest(); |
| 723: | |
| 724: | $response = $form_state->getResponse(); |
| 725: | |
| 726: | if (!$token->verify($this->f3->get('POST.tk'), 'oauth_consent')) { |
| 727: | $this->logger->log(LogLevel::WARNING, 'Security token ' . $this->f3->get('POST.tk') . ' invalid.'); |
| 728: | $this->f3->set('message', $this->f3->get('intl.common.invalid_tk')); |
| 729: | $this->consentForm($request, $response); |
| 730: | return; |
| 731: | } |
| 732: | |
| 733: | if ($this->f3->get('POST.op') == 'deny') { |
| 734: | $response->setError('access_denied')->renderRedirect(); |
| 735: | return; |
| 736: | } else { |
| 737: | $event = new FormSubmitEvent($form_state, 'oauth_consent_form_submit'); |
| 738: | \Events::instance()->dispatch($event); |
| 739: | |
| 740: | $client = $store->loadClient($request['client_id'], 'SimpleID\Protocols\OAuth\OAuthClient'); |
| 741: | $cid = $client->getStoreID(); |
| 742: | $now = time(); |
| 743: | |
| 744: | $consents = [ 'oauth' => $this->f3->get('POST.prefs.consents.oauth') ]; |
| 745: | |
| 746: | if (isset($user->clients[$cid])) { |
| 747: | $prefs = $user->clients[$cid]; |
| 748: | } else { |
| 749: | $prefs = [ |
| 750: | 'oauth' => [], |
| 751: | 'store_id' => $client->getStoreID(), |
| 752: | 'display_name' => $client->getDisplayName(), |
| 753: | 'display_html' => $client->getDisplayHTML(), |
| 754: | 'first_time' => $now, |
| 755: | 'consents' => [], |
| 756: | ]; |
| 757: | } |
| 758: | |
| 759: | $prefs['last_time'] = $now; |
| 760: | $prefs['consents'] = array_merge($prefs['consents'], $consents); |
| 761: | |
| 762: | $user->clients[$cid] = $prefs; |
| 763: | $store->saveUser($user); |
| 764: | } |
| 765: | |
| 766: | $this->processAuthRequest($request, $response); |
| 767: | } |
| 768: | |
| 769: | |
| 770: | |
| 771: | |
| 772: | |
| 773: | |
| 774: | |
| 775: | public function revoke() { |
| 776: | $request = new Request($this->f3->get('POST')); |
| 777: | $response = new Response($request); |
| 778: | |
| 779: | $this->checkHttps('error'); |
| 780: | |
| 781: | $this->logger->log(LogLevel::INFO, 'OAuth token revocation request: ', $request->toArray()); |
| 782: | |
| 783: | $this->oauth->initClient(); |
| 784: | $client = $this->oauth->getClient(); |
| 785: | |
| 786: | if (!$this->oauth->isClientAuthenticated(true, isset($client['oauth']['token_endpoint_auth_method']) ? $client['oauth']['token_endpoint_auth_method'] : null)) { |
| 787: | $this->logger->log(LogLevel::ERROR, 'Client authentication failed'); |
| 788: | $response->setError('invalid_client', 'client authentication failed'); |
| 789: | $response->renderJSON(); |
| 790: | return; |
| 791: | } |
| 792: | |
| 793: | $token = $this->inferTokenFromRequestBody($request, $response); |
| 794: | if ($response->isError()) { |
| 795: | $response->renderJSON(); |
| 796: | return; |
| 797: | } |
| 798: | |
| 799: | if (($token != null) && $token->isValid()) { |
| 800: | $authorization = $token->getAuthorization(); |
| 801: | if ($authorization->getClient()->getStoreID() != $client->getStoreID()) { |
| 802: | $this->logger->log(LogLevel::ERROR, 'Token revocation request failed: this client (' . $client->getStoreID() . ') does not match the client stored in token (' . $authorization->getClient()->getStoreID() . ')'); |
| 803: | $response->setError('invalid_grant', 'this client does not match the client stored in token'); |
| 804: | $response->renderJSON(); |
| 805: | return; |
| 806: | } |
| 807: | |
| 808: | $token->revoke(); |
| 809: | } |
| 810: | |
| 811: | |
| 812: | |
| 813: | $this->logger->log(LogLevel::INFO, 'OAuth token revoked: ', $request['token']); |
| 814: | $response['success'] = true; |
| 815: | $response->renderJSON(); |
| 816: | } |
| 817: | |
| 818: | |
| 819: | |
| 820: | |
| 821: | |
| 822: | |
| 823: | |
| 824: | public function introspect() { |
| 825: | $request = new Request($this->f3->get('POST')); |
| 826: | $response = new Response($request); |
| 827: | |
| 828: | $this->checkHttps('error'); |
| 829: | |
| 830: | $this->logger->log(LogLevel::INFO, 'OAuth token introspection request: ', $request->toArray()); |
| 831: | |
| 832: | $this->oauth->initClient(); |
| 833: | $client = $this->oauth->getClient(); |
| 834: | |
| 835: | if (!$this->oauth->isClientAuthenticated(true, isset($client['oauth']['token_endpoint_auth_method']) ? $client['oauth']['token_endpoint_auth_method'] : null)) { |
| 836: | $this->logger->log(LogLevel::ERROR, 'Client authentication failed'); |
| 837: | $response->setError('invalid_client', 'client authentication failed'); |
| 838: | $response->renderJSON(); |
| 839: | return; |
| 840: | } |
| 841: | |
| 842: | $token = $this->inferTokenFromRequestBody($request, $response); |
| 843: | if ($response->isError()) { |
| 844: | $response->renderJSON(); |
| 845: | return; |
| 846: | } |
| 847: | |
| 848: | if (($token == null) || (!$token->isValid())) { |
| 849: | $this->logger->log(LogLevel::INFO, 'OAuth token introspection result: not active'); |
| 850: | $response['active'] = false; |
| 851: | $response->renderJSON(); |
| 852: | return; |
| 853: | } |
| 854: | |
| 855: | $authorization = $token->getAuthorization(); |
| 856: | if ($authorization->getClient()->getStoreID() != $client->getStoreID()) { |
| 857: | $this->logger->log(LogLevel::ERROR, 'Token introspection request failed: this client (' . $client->getStoreID() . ') does not match the client stored in token (' . $authorization->getClient()->getStoreID() . ')'); |
| 858: | $response->setError('invalid_grant', 'this client does not match the client stored in token'); |
| 859: | $response->renderJSON(); |
| 860: | return; |
| 861: | } |
| 862: | |
| 863: | $expiry = $token->getExpiry(); |
| 864: | |
| 865: | $response['active'] = true; |
| 866: | $response['scope'] = implode(' ', $token->getScope()); |
| 867: | $response['client_id'] = $client->getStoreID(); |
| 868: | $response['token_type'] = $token->getType(); |
| 869: | if ($expiry != null) $response['exp'] = $expiry; |
| 870: | |
| 871: | $this->logger->log(LogLevel::INFO, 'OAuth token introspection result: active'); |
| 872: | $response->renderJSON(); |
| 873: | } |
| 874: | |
| 875: | |
| 876: | |
| 877: | |
| 878: | |
| 879: | |
| 880: | |
| 881: | public function metadata() { |
| 882: | $dispatcher = \Events::instance(); |
| 883: | |
| 884: | header('Content-Type: application/json'); |
| 885: | header('Content-Disposition: inline; filename=oauth-authorization-server'); |
| 886: | |
| 887: | $scope_info_event = new ScopeInfoCollectionEvent(); |
| 888: | $dispatcher->dispatch($scope_info_event); |
| 889: | $scopes = $scope_info_event->getScopeInfoForType('oauth'); |
| 890: | |
| 891: | $config = [ |
| 892: | 'issuer' => $this->getCanonicalHost(), |
| 893: | 'authorization_endpoint' => $this->getCanonicalURL('@oauth_auth', '', 'https'), |
| 894: | 'token_endpoint' => $this->getCanonicalURL('@oauth_token', '', 'https'), |
| 895: | 'revocation_endpoint' => $this->getCanonicalURL('@oauth_revoke', '', 'https'), |
| 896: | 'introspection_endpoint' => $this->getCanonicalURL('@oauth_introspect', '', 'https'), |
| 897: | 'scopes_supported' => array_keys($scopes), |
| 898: | 'response_types_supported' => [ 'code', 'token', 'code token' ], |
| 899: | 'response_modes_supported' => Response::getResponseModesSupported(), |
| 900: | 'grant_types_supported' => [ 'authorization_code', 'refresh_token' ], |
| 901: | 'token_endpoint_auth_methods_supported' => $this->oauth->getSupportedClientAuthMethods(), |
| 902: | 'revocation_endpoint_auth_methods_supported' => $this->oauth->getSupportedClientAuthMethods(), |
| 903: | 'introspection_endpoint_auth_methods_supported' => $this->oauth->getSupportedClientAuthMethods(), |
| 904: | 'code_challenge_methods_supported' => [ 'plain', 'S256' ], |
| 905: | 'service_documentation' => 'https://simpleid.org/docs/' |
| 906: | ]; |
| 907: | |
| 908: | $config_event = new BaseDataCollectionEvent('oauth_metadata', BaseDataCollectionEvent::MERGE_RECURSIVE); |
| 909: | $config_event->addResult($config); |
| 910: | $dispatcher->dispatch($config_event); |
| 911: | print json_encode($config_event->getResults()); |
| 912: | } |
| 913: | |
| 914: | |
| 915: | |
| 916: | |
| 917: | |
| 918: | public function onScopeInfoCollectionEvent(ScopeInfoCollectionEvent $event) { |
| 919: | $event->addScopeInfo('oauth', [ |
| 920: | self::DEFAULT_SCOPE => [ |
| 921: | 'description' => $this->f3->get('intl.common.scope.id'), |
| 922: | |
| 923: | ] |
| 924: | ]); |
| 925: | } |
| 926: | |
| 927: | |
| 928: | |
| 929: | |
| 930: | public function onOauthResponseTypes(BaseDataCollectionEvent $event) { |
| 931: | $event->addResult([ 'token', 'code' ]); |
| 932: | } |
| 933: | |
| 934: | |
| 935: | |
| 936: | |
| 937: | |
| 938: | public function onConsentRevoke(ConsentEvent $event) { |
| 939: | $cid = $event->getConsentID(); |
| 940: | $auth = AuthManager::instance(); |
| 941: | $store = StoreManager::instance(); |
| 942: | |
| 943: | $user = $auth->getUser(); |
| 944: | $client = $store->loadClient($cid, 'SimpleID\Protocols\OAuth\OAuthClient'); |
| 945: | |
| 946: | $aid = Authorization::buildID($user, $client); |
| 947: | |
| 948: | |
| 949: | $authorization = $store->loadAuth($aid); |
| 950: | |
| 951: | if ($authorization != null) { |
| 952: | $authorization->revokeAllTokens(); |
| 953: | $store->deleteAuth($authorization); |
| 954: | } |
| 955: | } |
| 956: | |
| 957: | |
| 958: | |
| 959: | |
| 960: | |
| 961: | |
| 962: | |
| 963: | |
| 964: | |
| 965: | |
| 966: | |
| 967: | |
| 968: | |
| 969: | |
| 970: | protected function rfc3986_urlencode($s) { |
| 971: | return str_replace('%7E', '~', rawurlencode($s)); |
| 972: | } |
| 973: | |
| 974: | |
| 975: | |
| 976: | |
| 977: | |
| 978: | |
| 979: | |
| 980: | |
| 981: | |
| 982: | |
| 983: | |
| 984: | |
| 985: | |
| 986: | |
| 987: | |
| 988: | |
| 989: | |
| 990: | protected function inferTokenFromRequestBody(Request $request, Response $response): ?Token { |
| 991: | if (!isset($request['token']) || ($request['token'] == '')) { |
| 992: | $this->logger->log(LogLevel::ERROR, 'Token operation request failed: token not set'); |
| 993: | $response->setError('invalid_request', 'token not set'); |
| 994: | return null; |
| 995: | } |
| 996: | |
| 997: | if (isset($request['token_type_hint'])) { |
| 998: | switch ($request['token_type_hint']) { |
| 999: | case 'access_token': |
| 1000: | $token = AccessToken::decode($request['token']); |
| 1001: | break; |
| 1002: | case 'refresh_token': |
| 1003: | $token = RefreshToken::decode($request['token']); |
| 1004: | break; |
| 1005: | default: |
| 1006: | $this->logger->log(LogLevel::ERROR, 'Token operation request failed: unsupported token type'); |
| 1007: | $response->setError('unsupported_token_type', 'unsupported token type'); |
| 1008: | return null; |
| 1009: | } |
| 1010: | } else { |
| 1011: | |
| 1012: | $token = AccessToken::decode($request['token']); |
| 1013: | if (!$token->isValid()) $token = RefreshToken::decode($request['token']); |
| 1014: | if (!$token->isValid()) $token = null; |
| 1015: | } |
| 1016: | |
| 1017: | return $token; |
| 1018: | } |
| 1019: | |
| 1020: | |
| 1021: | |
| 1022: | |
| 1023: | |
| 1024: | |
| 1025: | |
| 1026: | |
| 1027: | |
| 1028: | |
| 1029: | |
| 1030: | |
| 1031: | |
| 1032: | |
| 1033: | |
| 1034: | |
| 1035: | |
| 1036: | |
| 1037: | static function sortScopes($a, $b) { |
| 1038: | $a_required = (isset(self::$oauth_scope_settings[$a]['required'])) ? self::$oauth_scope_settings[$a]['required'] : false; |
| 1039: | $b_required = (isset(self::$oauth_scope_settings[$b]['required'])) ? self::$oauth_scope_settings[$b]['required'] : false; |
| 1040: | |
| 1041: | if ($a_required && !$b_required) return -1; |
| 1042: | if ($b_required && !$a_required) return 1; |
| 1043: | |
| 1044: | $a_weight= (isset(self::$oauth_scope_settings[$a]['weight'])) ? self::$oauth_scope_settings[$a]['weight'] : 0; |
| 1045: | $b_weight = (isset(self::$oauth_scope_settings[$b]['weight'])) ? self::$oauth_scope_settings[$b]['weight'] : 0; |
| 1046: | |
| 1047: | if ($a_weight < $b_weight) return -1; |
| 1048: | if ($a_weight > $b_weight) return 1; |
| 1049: | |
| 1050: | return strcasecmp($a, $b); |
| 1051: | } |
| 1052: | |
| 1053: | } |
| 1054: | ?> |
| 1055: | |