AuthorizeController.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. <?php
  2. namespace OAuth2\Controller;
  3. use OAuth2\Storage\ClientInterface;
  4. use OAuth2\ScopeInterface;
  5. use OAuth2\RequestInterface;
  6. use OAuth2\ResponseInterface;
  7. use OAuth2\Scope;
  8. use InvalidArgumentException;
  9. /**
  10. * @see AuthorizeControllerInterface
  11. */
  12. class AuthorizeController implements AuthorizeControllerInterface
  13. {
  14. /**
  15. * @var string
  16. */
  17. private $scope;
  18. /**
  19. * @var int
  20. */
  21. private $state;
  22. /**
  23. * @var mixed
  24. */
  25. private $client_id;
  26. /**
  27. * @var string
  28. */
  29. private $redirect_uri;
  30. /**
  31. * The response type
  32. *
  33. * @var string
  34. */
  35. private $response_type;
  36. /**
  37. * @var ClientInterface
  38. */
  39. protected $clientStorage;
  40. /**
  41. * @var array
  42. */
  43. protected $responseTypes;
  44. /**
  45. * @var array
  46. */
  47. protected $config;
  48. /**
  49. * @var ScopeInterface
  50. */
  51. protected $scopeUtil;
  52. /**
  53. * Constructor
  54. *
  55. * @param ClientInterface $clientStorage REQUIRED Instance of OAuth2\Storage\ClientInterface to retrieve client information
  56. * @param array $responseTypes OPTIONAL Array of OAuth2\ResponseType\ResponseTypeInterface objects. Valid array
  57. * keys are "code" and "token"
  58. * @param array $config OPTIONAL Configuration options for the server:
  59. * @param ScopeInterface $scopeUtil OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope
  60. * @code
  61. * $config = array(
  62. * 'allow_implicit' => false, // if the controller should allow the "implicit" grant type
  63. * 'enforce_state' => true // if the controller should require the "state" parameter
  64. * 'require_exact_redirect_uri' => true, // if the controller should require an exact match on the "redirect_uri" parameter
  65. * 'redirect_status_code' => 302, // HTTP status code to use for redirect responses
  66. * );
  67. * @endcode
  68. */
  69. public function __construct(ClientInterface $clientStorage, array $responseTypes = array(), array $config = array(), ScopeInterface $scopeUtil = null)
  70. {
  71. $this->clientStorage = $clientStorage;
  72. $this->responseTypes = $responseTypes;
  73. $this->config = array_merge(array(
  74. 'allow_implicit' => false,
  75. 'enforce_state' => true,
  76. 'require_exact_redirect_uri' => true,
  77. 'redirect_status_code' => 302,
  78. ), $config);
  79. if (is_null($scopeUtil)) {
  80. $scopeUtil = new Scope();
  81. }
  82. $this->scopeUtil = $scopeUtil;
  83. }
  84. /**
  85. * Handle the authorization request
  86. *
  87. * @param RequestInterface $request
  88. * @param ResponseInterface $response
  89. * @param boolean $is_authorized
  90. * @param mixed $user_id
  91. * @return mixed|void
  92. * @throws InvalidArgumentException
  93. */
  94. public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null)
  95. {
  96. if (!is_bool($is_authorized)) {
  97. throw new InvalidArgumentException('Argument "is_authorized" must be a boolean. This method must know if the user has granted access to the client.');
  98. }
  99. // We repeat this, because we need to re-validate. The request could be POSTed
  100. // by a 3rd-party (because we are not internally enforcing NONCEs, etc)
  101. if (!$this->validateAuthorizeRequest($request, $response)) {
  102. return;
  103. }
  104. // If no redirect_uri is passed in the request, use client's registered one
  105. if (empty($this->redirect_uri)) {
  106. $clientData = $this->clientStorage->getClientDetails($this->client_id);
  107. $registered_redirect_uri = $clientData['redirect_uri'];
  108. }
  109. // the user declined access to the client's application
  110. if ($is_authorized === false) {
  111. $redirect_uri = $this->redirect_uri ?: $registered_redirect_uri;
  112. $this->setNotAuthorizedResponse($request, $response, $redirect_uri, $user_id);
  113. return;
  114. }
  115. // build the parameters to set in the redirect URI
  116. if (!$params = $this->buildAuthorizeParameters($request, $response, $user_id)) {
  117. return;
  118. }
  119. $authResult = $this->responseTypes[$this->response_type]->getAuthorizeResponse($params, $user_id);
  120. list($redirect_uri, $uri_params) = $authResult;
  121. if (empty($redirect_uri) && !empty($registered_redirect_uri)) {
  122. $redirect_uri = $registered_redirect_uri;
  123. }
  124. $uri = $this->buildUri($redirect_uri, $uri_params);
  125. // return redirect response
  126. $response->setRedirect($this->config['redirect_status_code'], $uri);
  127. }
  128. /**
  129. * Set not authorized response
  130. *
  131. * @param RequestInterface $request
  132. * @param ResponseInterface $response
  133. * @param string $redirect_uri
  134. * @param mixed $user_id
  135. */
  136. protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null)
  137. {
  138. $error = 'access_denied';
  139. $error_message = 'The user denied access to your application';
  140. $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->state, $error, $error_message);
  141. }
  142. /**
  143. * We have made this protected so this class can be extended to add/modify
  144. * these parameters
  145. *
  146. * @TODO: add dependency injection for the parameters in this method
  147. *
  148. * @param RequestInterface $request
  149. * @param ResponseInterface $response
  150. * @param mixed $user_id
  151. * @return array
  152. */
  153. protected function buildAuthorizeParameters($request, $response, $user_id)
  154. {
  155. // @TODO: we should be explicit with this in the future
  156. $params = array(
  157. 'scope' => $this->scope,
  158. 'state' => $this->state,
  159. 'client_id' => $this->client_id,
  160. 'redirect_uri' => $this->redirect_uri,
  161. 'response_type' => $this->response_type,
  162. );
  163. return $params;
  164. }
  165. /**
  166. * Validate the OAuth request
  167. *
  168. * @param RequestInterface $request
  169. * @param ResponseInterface $response
  170. * @return bool
  171. */
  172. public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response)
  173. {
  174. // Make sure a valid client id was supplied (we can not redirect because we were unable to verify the URI)
  175. if (!$client_id = $request->query('client_id', $request->request('client_id'))) {
  176. // We don't have a good URI to use
  177. $response->setError(400, 'invalid_client', "No client id supplied");
  178. return false;
  179. }
  180. // Get client details
  181. if (!$clientData = $this->clientStorage->getClientDetails($client_id)) {
  182. $response->setError(400, 'invalid_client', 'The client id supplied is invalid');
  183. return false;
  184. }
  185. $registered_redirect_uri = isset($clientData['redirect_uri']) ? $clientData['redirect_uri'] : '';
  186. // Make sure a valid redirect_uri was supplied. If specified, it must match the clientData URI.
  187. // @see http://tools.ietf.org/html/rfc6749#section-3.1.2
  188. // @see http://tools.ietf.org/html/rfc6749#section-4.1.2.1
  189. // @see http://tools.ietf.org/html/rfc6749#section-4.2.2.1
  190. if ($supplied_redirect_uri = $request->query('redirect_uri', $request->request('redirect_uri'))) {
  191. // validate there is no fragment supplied
  192. $parts = parse_url($supplied_redirect_uri);
  193. if (isset($parts['fragment']) && $parts['fragment']) {
  194. $response->setError(400, 'invalid_uri', 'The redirect URI must not contain a fragment');
  195. return false;
  196. }
  197. // validate against the registered redirect uri(s) if available
  198. if ($registered_redirect_uri && !$this->validateRedirectUri($supplied_redirect_uri, $registered_redirect_uri)) {
  199. $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI provided is missing or does not match', '#section-3.1.2');
  200. return false;
  201. }
  202. $redirect_uri = $supplied_redirect_uri;
  203. } else {
  204. // use the registered redirect_uri if none has been supplied, if possible
  205. if (!$registered_redirect_uri) {
  206. $response->setError(400, 'invalid_uri', 'No redirect URI was supplied or stored');
  207. return false;
  208. }
  209. if (count(explode(' ', $registered_redirect_uri)) > 1) {
  210. $response->setError(400, 'invalid_uri', 'A redirect URI must be supplied when multiple redirect URIs are registered', '#section-3.1.2.3');
  211. return false;
  212. }
  213. $redirect_uri = $registered_redirect_uri;
  214. }
  215. // Select the response type
  216. $response_type = $request->query('response_type', $request->request('response_type'));
  217. // for multiple-valued response types - make them alphabetical
  218. if (false !== strpos($response_type, ' ')) {
  219. $types = explode(' ', $response_type);
  220. sort($types);
  221. $response_type = ltrim(implode(' ', $types));
  222. }
  223. $state = $request->query('state', $request->request('state'));
  224. // type and client_id are required
  225. if (!$response_type || !in_array($response_type, $this->getValidResponseTypes())) {
  226. $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_request', 'Invalid or missing response type', null);
  227. return false;
  228. }
  229. if ($response_type == self::RESPONSE_TYPE_AUTHORIZATION_CODE) {
  230. if (!isset($this->responseTypes['code'])) {
  231. $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'authorization code grant type not supported', null);
  232. return false;
  233. }
  234. if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'authorization_code')) {
  235. $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null);
  236. return false;
  237. }
  238. if ($this->responseTypes['code']->enforceRedirect() && !$redirect_uri) {
  239. $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI is mandatory and was not supplied');
  240. return false;
  241. }
  242. } else {
  243. if (!$this->config['allow_implicit']) {
  244. $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'implicit grant type not supported', null);
  245. return false;
  246. }
  247. if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'implicit')) {
  248. $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null);
  249. return false;
  250. }
  251. }
  252. // validate requested scope if it exists
  253. $requestedScope = $this->scopeUtil->getScopeFromRequest($request);
  254. if ($requestedScope) {
  255. // restrict scope by client specific scope if applicable,
  256. // otherwise verify the scope exists
  257. $clientScope = $this->clientStorage->getClientScope($client_id);
  258. if ((empty($clientScope) && !$this->scopeUtil->scopeExists($requestedScope))
  259. || (!empty($clientScope) && !$this->scopeUtil->checkScope($requestedScope, $clientScope))) {
  260. $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_scope', 'An unsupported scope was requested', null);
  261. return false;
  262. }
  263. } else {
  264. // use a globally-defined default scope
  265. $defaultScope = $this->scopeUtil->getDefaultScope($client_id);
  266. if (false === $defaultScope) {
  267. $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_client', 'This application requires you specify a scope parameter', null);
  268. return false;
  269. }
  270. $requestedScope = $defaultScope;
  271. }
  272. // Validate state parameter exists (if configured to enforce this)
  273. if ($this->config['enforce_state'] && !$state) {
  274. $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, null, 'invalid_request', 'The state parameter is required');
  275. return false;
  276. }
  277. // save the input data and return true
  278. $this->scope = $requestedScope;
  279. $this->state = $state;
  280. $this->client_id = $client_id;
  281. // Only save the SUPPLIED redirect URI (@see http://tools.ietf.org/html/rfc6749#section-4.1.3)
  282. $this->redirect_uri = $supplied_redirect_uri;
  283. $this->response_type = $response_type;
  284. return true;
  285. }
  286. /**
  287. * Build the absolute URI based on supplied URI and parameters.
  288. *
  289. * @param string $uri An absolute URI.
  290. * @param array $params Parameters to be append as GET.
  291. *
  292. * @return string
  293. * An absolute URI with supplied parameters.
  294. *
  295. * @ingroup oauth2_section_4
  296. */
  297. private function buildUri($uri, $params)
  298. {
  299. $parse_url = parse_url($uri);
  300. // Add our params to the parsed uri
  301. foreach ($params as $k => $v) {
  302. if (isset($parse_url[$k])) {
  303. $parse_url[$k] .= "&" . http_build_query($v, '', '&');
  304. } else {
  305. $parse_url[$k] = http_build_query($v, '', '&');
  306. }
  307. }
  308. // Put the uri back together
  309. return
  310. ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "")
  311. . ((isset($parse_url["user"])) ? $parse_url["user"]
  312. . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") . "@" : "")
  313. . ((isset($parse_url["host"])) ? $parse_url["host"] : "")
  314. . ((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "")
  315. . ((isset($parse_url["path"])) ? $parse_url["path"] : "")
  316. . ((isset($parse_url["query"]) && !empty($parse_url['query'])) ? "?" . $parse_url["query"] : "")
  317. . ((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : "")
  318. ;
  319. }
  320. protected function getValidResponseTypes()
  321. {
  322. return array(
  323. self::RESPONSE_TYPE_ACCESS_TOKEN,
  324. self::RESPONSE_TYPE_AUTHORIZATION_CODE,
  325. );
  326. }
  327. /**
  328. * Internal method for validating redirect URI supplied
  329. *
  330. * @param string $inputUri The submitted URI to be validated
  331. * @param string $registeredUriString The allowed URI(s) to validate against. Can be a space-delimited string of URIs to
  332. * allow for multiple URIs
  333. * @return bool
  334. * @see http://tools.ietf.org/html/rfc6749#section-3.1.2
  335. */
  336. protected function validateRedirectUri($inputUri, $registeredUriString)
  337. {
  338. if (!$inputUri || !$registeredUriString) {
  339. return false; // if either one is missing, assume INVALID
  340. }
  341. $registered_uris = preg_split('/\s+/', $registeredUriString);
  342. foreach ($registered_uris as $registered_uri) {
  343. if ($this->config['require_exact_redirect_uri']) {
  344. // the input uri is validated against the registered uri using exact match
  345. if (strcmp($inputUri, $registered_uri) === 0) {
  346. return true;
  347. }
  348. } else {
  349. $registered_uri_length = strlen($registered_uri);
  350. if ($registered_uri_length === 0) {
  351. return false;
  352. }
  353. // the input uri is validated against the registered uri using case-insensitive match of the initial string
  354. // i.e. additional query parameters may be applied
  355. if (strcasecmp(substr($inputUri, 0, $registered_uri_length), $registered_uri) === 0) {
  356. return true;
  357. }
  358. }
  359. }
  360. return false;
  361. }
  362. /**
  363. * Convenience method to access the scope
  364. *
  365. * @return string
  366. */
  367. public function getScope()
  368. {
  369. return $this->scope;
  370. }
  371. /**
  372. * Convenience method to access the state
  373. *
  374. * @return int
  375. */
  376. public function getState()
  377. {
  378. return $this->state;
  379. }
  380. /**
  381. * Convenience method to access the client id
  382. *
  383. * @return mixed
  384. */
  385. public function getClientId()
  386. {
  387. return $this->client_id;
  388. }
  389. /**
  390. * Convenience method to access the redirect url
  391. *
  392. * @return string
  393. */
  394. public function getRedirectUri()
  395. {
  396. return $this->redirect_uri;
  397. }
  398. /**
  399. * Convenience method to access the response type
  400. *
  401. * @return string
  402. */
  403. public function getResponseType()
  404. {
  405. return $this->response_type;
  406. }
  407. }