AuthAction.php 9.48 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\authclient;

use yii\base\Action;
use yii\base\Exception;
12
use yii\base\InvalidConfigException;
13
use yii\base\NotSupportedException;
14
use yii\web\Response;
15 16 17 18 19
use yii\web\HttpException;
use yii\web\NotFoundHttpException;
use Yii;

/**
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
 * AuthAction performs authentication via different auth clients.
 * It supports [[OpenId]], [[OAuth1] and [[OAuth2]] client types.
 *
 * Usage:
 * ~~~
 * class SiteController extends Controller
 * {
 *     public function actions()
 *     {
 *         return [
 *             'auth' => [
 *                 'class' => 'yii\authclient\AuthAction',
 *                 'successCallback' => [$this, 'successCallback'],
 *             ],
 *         ]
 *     }
 *
 *     public function successCallback($client)
 *     {
munawer-t committed
39
 *         $attributes = $client->getUserAttributes();
40 41 42 43 44 45 46 47 48 49
 *         // user login or signup comes here
 *     }
 * }
 * ~~~
 *
 * Usually authentication via external services is performed inside the popup window.
 * This action handles the redirection and closing of popup window correctly.
 *
 * @see Collection
 * @see \yii\authclient\widgets\Choice
50
 *
Qiang Xue committed
51 52 53
 * @property string $cancelUrl Cancel URL.
 * @property string $successUrl Successful URL.
 *
54 55 56 57 58 59
 * @author Paul Klimov <klimov.paul@gmail.com>
 * @since 2.0
 */
class AuthAction extends Action
{
	/**
60
	 * @var string name of the auth client collection application component.
61
	 * It should point to [[Collection]] instance.
62
	 */
63
	public $clientCollection = 'authClientCollection';
64
	/**
65
	 * @var string name of the GET param, which is used to passed auth client id to this action.
66
	 * Note: watch for the naming, make sure you do not choose name used in some auth protocol.
67
	 */
68
	public $clientIdGetParamName = 'authclient';
69 70
	/**
	 * @var callable PHP callback, which should be triggered in case of successful authentication.
71 72 73 74 75 76
	 * This callback should accept [[ClientInterface]] instance as an argument.
	 * For example:
	 *
	 * ~~~
	 * public function onAuthSuccess($client)
	 * {
munawer-t committed
77
	 *     $attributes = $client->getUserAttributes();
78 79 80 81 82 83 84
	 *     // user login or signup comes here
	 * }
	 * ~~~
	 *
	 * If this callback returns [[Response]] instance, it will be used as action response,
	 * otherwise redirection to [[successUrl]] will be performed.
	 *
85 86 87 88 89 90 91 92 93 94
	 */
	public $successCallback;
	/**
	 * @var string the redirect url after successful authorization.
	 */
	private $_successUrl = '';
	/**
	 * @var string the redirect url after unsuccessful authorization (e.g. user canceled).
	 */
	private $_cancelUrl = '';
95 96 97 98 99
	/**
	 * @var string name or alias of the view file, which should be rendered in order to perform redirection.
	 * If not set default one will be used.
	 */
	public $redirectView;
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161

	/**
	 * @param string $url successful URL.
	 */
	public function setSuccessUrl($url)
	{
		$this->_successUrl = $url;
	}

	/**
	 * @return string successful URL.
	 */
	public function getSuccessUrl()
	{
		if (empty($this->_successUrl)) {
			$this->_successUrl = $this->defaultSuccessUrl();
		}
		return $this->_successUrl;
	}

	/**
	 * @param string $url cancel URL.
	 */
	public function setCancelUrl($url)
	{
		$this->_cancelUrl = $url;
	}

	/**
	 * @return string cancel URL.
	 */
	public function getCancelUrl()
	{
		if (empty($this->_cancelUrl)) {
			$this->_cancelUrl = $this->defaultCancelUrl();
		}
		return $this->_cancelUrl;
	}

	/**
	 * Creates default {@link successUrl} value.
	 * @return string success URL value.
	 */
	protected function defaultSuccessUrl()
	{
		return Yii::$app->getUser()->getReturnUrl();
	}

	/**
	 * Creates default {@link cancelUrl} value.
	 * @return string cancel URL value.
	 */
	protected function defaultCancelUrl()
	{
		return Yii::$app->getRequest()->getAbsoluteUrl();
	}

	/**
	 * Runs the action.
	 */
	public function run()
	{
162 163 164 165 166 167
		if (!empty($_GET[$this->clientIdGetParamName])) {
			$clientId = $_GET[$this->clientIdGetParamName];
			/** @var \yii\authclient\Collection $collection */
			$collection = Yii::$app->getComponent($this->clientCollection);
			if (!$collection->hasClient($clientId)) {
				throw new NotFoundHttpException("Unknown auth client '{$clientId}'");
168
			}
169 170
			$client = $collection->getClient($clientId);
			return $this->auth($client);
171 172 173 174 175 176
		} else {
			throw new NotFoundHttpException();
		}
	}

	/**
177
	 * @param mixed $client auth client instance.
178
	 * @return Response response instance.
179
	 * @throws \yii\base\NotSupportedException on invalid client.
180
	 */
181
	protected function auth($client)
182
	{
183 184 185 186 187 188
		if ($client instanceof OpenId) {
			return $this->authOpenId($client);
		} elseif ($client instanceof OAuth2) {
			return $this->authOAuth2($client);
		} elseif ($client instanceof OAuth1) {
			return $this->authOAuth1($client);
189
		} else {
190
			throw new NotSupportedException('Provider "' . get_class($client) . '" is not supported.');
191 192 193 194
		}
	}

	/**
195 196 197 198
	 * This method is invoked in case of successful authentication via auth client.
	 * @param ClientInterface $client auth client instance.
	 * @throws InvalidConfigException on invalid success callback.
	 * @return Response response instance.
199
	 */
200
	protected function authSuccess($client)
201
	{
202 203 204 205 206 207 208
		if (!is_callable($this->successCallback)) {
			throw new InvalidConfigException('"' . get_class($this) . '::successCallback" should be a valid callback.');
		}
		$response = call_user_func($this->successCallback, $client);
		if ($response instanceof Response) {
			return $response;
		}
209 210 211 212 213 214 215 216 217 218 219
		return $this->redirectSuccess();
	}

	/**
	 * Redirect to the given URL or simply close the popup window.
	 * @param mixed $url URL to redirect, could be a string or array config to generate a valid URL.
	 * @param boolean $enforceRedirect indicates if redirect should be performed even in case of popup window.
	 * @return \yii\web\Response response instance.
	 */
	public function redirect($url, $enforceRedirect = true)
	{
220 221 222 223 224 225
		$viewFile = $this->redirectView;
		if ($viewFile === null) {
			$viewFile = __DIR__ . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'redirect.php';
		} else {
			$viewFile = Yii::getAlias($viewFile);
		}
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
		$viewData = [
			'url' => $url,
			'enforceRedirect' => $enforceRedirect,
		];
		$response = Yii::$app->getResponse();
		$response->content = Yii::$app->getView()->renderFile($viewFile, $viewData);
		return $response;
	}

	/**
	 * Redirect to the URL. If URL is null, {@link successUrl} will be used.
	 * @param string $url URL to redirect.
	 * @return \yii\web\Response response instance.
	 */
	public function redirectSuccess($url = null)
	{
		if ($url === null) {
			$url = $this->getSuccessUrl();
		}
		return $this->redirect($url);
	}

	/**
	 * Redirect to the {@link cancelUrl} or simply close the popup window.
	 * @param string $url URL to redirect.
	 * @return \yii\web\Response response instance.
	 */
	public function redirectCancel($url = null)
	{
		if ($url === null) {
			$url = $this->getCancelUrl();
		}
		return $this->redirect($url, false);
	}

	/**
262 263 264 265 266
	 * Performs OpenID auth flow.
	 * @param OpenId $client auth client instance.
	 * @return Response action response.
	 * @throws Exception on failure.
	 * @throws HttpException on failure.
267
	 */
268
	protected function authOpenId($client)
269 270 271 272
	{
		if (!empty($_REQUEST['openid_mode'])) {
			switch ($_REQUEST['openid_mode']) {
				case 'id_res':
273 274
					if ($client->validate()) {
						return $this->authSuccess($client);
275
					} else {
276
						throw new HttpException(400, 'Unable to complete the authentication because the required data was not received.');
277 278 279 280 281 282 283 284 285 286
					}
					break;
				case 'cancel':
					$this->redirectCancel();
					break;
				default:
					throw new HttpException(400);
					break;
			}
		} else {
287
			$url = $client->buildAuthUrl();
288 289 290 291 292 293
			return Yii::$app->getResponse()->redirect($url);
		}
		return $this->redirectCancel();
	}

	/**
294 295 296
	 * Performs OAuth1 auth flow.
	 * @param OAuth1 $client auth client instance.
	 * @return Response action response.
297
	 */
298
	protected function authOAuth1($client)
299 300 301 302 303 304 305 306 307 308 309 310
	{
		// user denied error
		if (isset($_GET['denied'])) {
			return $this->redirectCancel();
		}

		if (isset($_REQUEST['oauth_token'])) {
			$oauthToken = $_REQUEST['oauth_token'];
		}

		if (!isset($oauthToken)) {
			// Get request token.
311
			$requestToken = $client->fetchRequestToken();
312
			// Get authorization URL.
313
			$url = $client->buildAuthUrl($requestToken);
314 315 316 317
			// Redirect to authorization URL.
			return Yii::$app->getResponse()->redirect($url);
		} else {
			// Upgrade to access token.
318
			$client->fetchAccessToken();
319
			return $this->authSuccess($client);
320 321 322 323
		}
	}

	/**
324 325 326 327
	 * Performs OAuth2 auth flow.
	 * @param OAuth2 $client auth client instance.
	 * @return Response action response.
	 * @throws \yii\base\Exception on failure.
328
	 */
329
	protected function authOAuth2($client)
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
	{
		if (isset($_GET['error'])) {
			if ($_GET['error'] == 'access_denied') {
				// user denied error
				return $this->redirectCancel();
			} else {
				// request error
				if (isset($_GET['error_description'])) {
					$errorMessage = $_GET['error_description'];
				} elseif (isset($_GET['error_message'])) {
					$errorMessage = $_GET['error_message'];
				} else {
					$errorMessage = http_build_query($_GET);
				}
				throw new Exception('Auth error: ' . $errorMessage);
			}
		}

		// Get the access_token and save them to the session.
		if (isset($_GET['code'])) {
			$code = $_GET['code'];
351
			$token = $client->fetchAccessToken($code);
352
			if (!empty($token)) {
353
				return $this->authSuccess($client);
354 355 356 357
			} else {
				return $this->redirectCancel();
			}
		} else {
358
			$url = $client->buildAuthUrl();
359 360 361 362
			return Yii::$app->getResponse()->redirect($url);
		}
	}
}