├── Config ├── Migration │ └── 1339124026_firstmigrationoauth.php ├── Schema │ └── schema.sql └── routes.php ├── Controller ├── Component │ └── OAuthComponent.php ├── OAuthAppController.php └── OAuthController.php ├── LICENSE ├── Model ├── AccessToken.php ├── AuthCode.php ├── Behavior │ └── HashedFieldBehavior.php ├── Client.php ├── OAuthAppModel.php └── RefreshToken.php ├── README.markdown ├── Test ├── Case │ ├── AllOAuthTestsTest.php │ └── Model │ │ └── Behavior │ │ └── HashedFieldBehaviorTest.php └── Fixture │ └── AccessTokenFixture.php ├── View └── OAuth │ ├── authorize.ctp │ ├── login.ctp │ └── userinfo.ctp └── composer.json /Config/Migration/1339124026_firstmigrationoauth.php: -------------------------------------------------------------------------------- 1 | array( 21 | 'create_table' => array( 22 | 'access_tokens' => array( 23 | 'oauth_token' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 40, 'key' => 'primary', 'collate' => 'utf8_general_ci', 'charset' => 'utf8'), 24 | 'client_id' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 36, 'collate' => 'utf8_general_ci', 'charset' => 'utf8', 'after' => 'oauth_token'), 25 | 'user_id' => array('type' => 'integer', 'null' => false, 'default' => NULL, 'after' => 'client_id'), 26 | 'expires' => array('type' => 'integer', 'null' => false, 'default' => NULL, 'after' => 'user_id'), 27 | 'scope' => array('type' => 'string', 'null' => true, 'default' => NULL, 'collate' => 'utf8_general_ci', 'charset' => 'utf8', 'after' => 'expires'), 28 | 'indexes' => array( 29 | 'PRIMARY' => array('column' => 'oauth_token', 'unique' => 1), 30 | ), 31 | 'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'MyISAM'), 32 | ), 33 | 34 | 'auth_codes' => array( 35 | 'code' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 40, 'key' => 'primary', 'collate' => 'utf8_general_ci', 'charset' => 'utf8'), 36 | 'client_id' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 36, 'collate' => 'utf8_general_ci', 'charset' => 'utf8', 'after' => 'code'), 37 | 'user_id' => array('type' => 'integer', 'null' => false, 'default' => NULL, 'after' => 'client_id'), 38 | 'redirect_uri' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 200, 'collate' => 'utf8_general_ci', 'charset' => 'utf8', 'after' => 'user_id'), 39 | 'expires' => array('type' => 'integer', 'null' => false, 'default' => NULL, 'after' => 'redirect_uri'), 40 | 'scope' => array('type' => 'string', 'null' => true, 'default' => NULL, 'collate' => 'utf8_general_ci', 'charset' => 'utf8', 'after' => 'expires'), 41 | 'indexes' => array( 42 | 'PRIMARY' => array('column' => 'code', 'unique' => 1), 43 | ), 44 | 'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'MyISAM'), 45 | ), 46 | 47 | 'clients' => array( 48 | 'client_id' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 20, 'key' => 'primary', 'collate' => 'utf8_general_ci', 'charset' => 'utf8'), 49 | 'client_secret' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 40, 'collate' => 'utf8_general_ci', 'charset' => 'utf8', 'after' => 'client_id'), 50 | 'redirect_uri' => array('type' => 'string', 'null' => false, 'default' => NULL, 'collate' => 'utf8_general_ci', 'charset' => 'utf8', 'after' => 'client_secret'), 51 | 'user_id' => array('type' => 'integer', 'null' => true, 'default' => NULL, 'after' => 'redirect_uri'), 52 | 'indexes' => array( 53 | 'PRIMARY' => array('column' => 'client_id', 'unique' => 1), 54 | ), 55 | 'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'MyISAM'), 56 | ), 57 | 58 | 'refresh_tokens' => array( 59 | 'refresh_token' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 40, 'key' => 'primary', 'collate' => 'utf8_general_ci', 'charset' => 'utf8'), 60 | 'client_id' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 36, 'collate' => 'utf8_general_ci', 'charset' => 'utf8', 'after' => 'refresh_token'), 61 | 'user_id' => array('type' => 'integer', 'null' => false, 'default' => NULL, 'after' => 'client_id'), 62 | 'expires' => array('type' => 'integer', 'null' => false, 'default' => NULL, 'after' => 'user_id'), 63 | 'scope' => array('type' => 'string', 'null' => true, 'default' => NULL, 'collate' => 'utf8_general_ci', 'charset' => 'utf8', 'after' => 'expires'), 64 | 'indexes' => array( 65 | 'PRIMARY' => array('column' => 'refresh_token', 'unique' => 1), 66 | ), 67 | 'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'MyISAM'), 68 | ), 69 | ), 70 | ), 71 | 'down' => array( 72 | 'drop_table' => array( 73 | 'access_tokens', 'auth_codes', 'clients', 'refresh_tokens' 74 | ), 75 | ), 76 | ); 77 | 78 | /** 79 | * Before migration callback 80 | * 81 | * @param string $direction, up or down direction of migration process 82 | * @return boolean Should process continue 83 | * @access public 84 | */ 85 | public function before($direction) { 86 | return true; 87 | } 88 | 89 | /** 90 | * After migration callback 91 | * 92 | * @param string $direction, up or down direction of migration process 93 | * @return boolean Should process continue 94 | * @access public 95 | */ 96 | public function after($direction) { 97 | return true; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /Config/Schema/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `access_tokens`; 2 | CREATE TABLE `access_tokens` ( 3 | `oauth_token` varchar(40) NOT NULL, 4 | `client_id` char(36) NOT NULL, 5 | `user_id` int(11) unsigned NOT NULL, 6 | `expires` int(11) NOT NULL, 7 | `scope` varchar(255) DEFAULT NULL, 8 | PRIMARY KEY (`oauth_token`) 9 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 10 | 11 | 12 | DROP TABLE IF EXISTS `auth_codes`; 13 | CREATE TABLE `auth_codes` ( 14 | `code` varchar(40) NOT NULL, 15 | `client_id` char(36) NOT NULL, 16 | `user_id` int(11) unsigned NOT NULL, 17 | `redirect_uri` varchar(200) NOT NULL, 18 | `expires` int(11) NOT NULL, 19 | `scope` varchar(255) DEFAULT NULL, 20 | PRIMARY KEY (`code`) 21 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 22 | 23 | 24 | DROP TABLE IF EXISTS `clients`; 25 | CREATE TABLE `clients` ( 26 | `client_id` char(20) NOT NULL, 27 | `client_secret` char(40) NOT NULL, 28 | `redirect_uri` varchar(255) NOT NULL, 29 | `user_id` int(11) DEFAULT NULL, 30 | PRIMARY KEY (`client_id`) 31 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 32 | 33 | 34 | DROP TABLE IF EXISTS `refresh_tokens`; 35 | CREATE TABLE `refresh_tokens` ( 36 | `refresh_token` varchar(40) NOT NULL, 37 | `client_id` char(36) NOT NULL, 38 | `user_id` int(11) unsigned NOT NULL, 39 | `expires` int(11) NOT NULL, 40 | `scope` varchar(255) DEFAULT NULL, 41 | PRIMARY KEY (`refresh_token`) 42 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -------------------------------------------------------------------------------- /Config/routes.php: -------------------------------------------------------------------------------- 1 | 'OAuth', 'plugin' => 'o_auth')); 4 | -------------------------------------------------------------------------------- /Controller/Component/OAuthComponent.php: -------------------------------------------------------------------------------- 1 | 15 | * @see https://github.com/thomseddon/cakephp-oauth-server 16 | * 17 | */ 18 | 19 | App::uses('Component', 'Controller'); 20 | App::uses('Router', 'Routing'); 21 | App::uses('Security', 'Utility'); 22 | App::uses('Hash', 'Utility'); 23 | App::uses('AuthComponent', 'Controller'); 24 | 25 | App::import('Vendor', 'oauth2-php/lib/OAuth2'); 26 | App::import('Vendor', 'oauth2-php/lib/IOAuth2Storage'); 27 | App::import('Vendor', 'oauth2-php/lib/IOAuth2RefreshTokens'); 28 | App::import('Vendor', 'oauth2-php/lib/IOAuth2GrantUser'); 29 | App::import('Vendor', 'oauth2-php/lib/IOAuth2GrantCode'); 30 | 31 | class OAuthComponent extends Component implements IOAuth2Storage, IOAuth2RefreshTokens, IOAuth2GrantUser, IOAuth2GrantCode { 32 | 33 | /** 34 | * AccessToken object. 35 | * 36 | * @var object 37 | */ 38 | public $AccessToken; 39 | 40 | /** 41 | * Array of allowed actions 42 | * 43 | * @var array 44 | */ 45 | protected $allowedActions = array('token', 'authorize', 'login'); 46 | 47 | /** 48 | * An array containing the model and fields to authenticate users against 49 | * 50 | * Inherits theses defaults: 51 | * 52 | * $this->OAuth->authenticate = array( 53 | * 'userModel' => 'User', 54 | * 'fields' => array( 55 | * 'username' => 'username', 56 | * 'password' => 'password' 57 | * ) 58 | * ); 59 | * 60 | * Which can be overridden in your beforeFilter: 61 | * 62 | * $this->OAuth->authenticate = array( 63 | * 'fields' => array( 64 | * 'username' => 'email' 65 | * ) 66 | * ); 67 | * 68 | * 69 | * $this->OAuth->authenticate 70 | * 71 | * @var array 72 | */ 73 | public $authenticate; 74 | 75 | /** 76 | * Defaults for $authenticate 77 | * 78 | * @var array 79 | */ 80 | protected $_authDefaults = array( 81 | 'userModel' => 'User', 82 | 'fields' => array('username' => 'username', 'password' => 'password') 83 | ); 84 | 85 | /** 86 | * AuthCode object. 87 | * 88 | * @var object 89 | */ 90 | public $AuthCode; 91 | 92 | /** 93 | * Clients object. 94 | * 95 | * @var object 96 | */ 97 | public $Client; 98 | 99 | /** 100 | * Array of globally supported grant types 101 | * 102 | * By default = array('authorization_code', 'refresh_token', 'password'); 103 | * Other grant mechanisms are not supported in the current release 104 | * 105 | * @var array 106 | */ 107 | public $grantTypes = array('authorization_code', 'refresh_token', 'password'); 108 | 109 | /** 110 | * OAuth2 Object 111 | * 112 | * @var object 113 | */ 114 | public $OAuth2; 115 | 116 | /** 117 | * RefreshToken object. 118 | * 119 | * @var object 120 | */ 121 | public $RefreshToken; 122 | 123 | /** 124 | * User object 125 | * 126 | * @var object 127 | */ 128 | public $User; 129 | 130 | /** 131 | * Static storage for current user 132 | * 133 | * @var array 134 | */ 135 | protected $_user = false; 136 | 137 | /** 138 | * Constructor - Adds class associations 139 | * 140 | * @see OAuth2::__construct(). 141 | */ 142 | public function __construct(ComponentCollection $collection, $settings = array()) { 143 | parent::__construct($collection, $settings); 144 | $this->OAuth2 = new OAuth2($this); 145 | $this->AccessToken = ClassRegistry::init(array('class' => 'OAuth.AccessToken', 'alias' => 'AccessToken')); 146 | $this->AuthCode = ClassRegistry::init(array('class' => 'OAuth.AuthCode', 'alias' => 'AuthCode')); 147 | $this->Client = ClassRegistry::init(array('class' => 'OAuth.Client', 'alias' => 'Client')); 148 | $this->RefreshToken = ClassRegistry::init(array('class' => 'OAuth.RefreshToken', 'alias' => 'RefreshToken')); 149 | } 150 | 151 | /** 152 | * Initializes OAuthComponent for use in the controller 153 | * 154 | * @param Controller $controller A reference to the instantiating controller object 155 | * @return void 156 | */ 157 | public function initialize(Controller $controller) { 158 | $this->request = $controller->request; 159 | $this->response = $controller->response; 160 | $this->_methods = $controller->methods; 161 | 162 | if (Configure::read('debug') > 0) { 163 | Debugger::checkSecurityKeys(); 164 | } 165 | } 166 | 167 | /** 168 | * Main engine that checks valid access_token and stores the associated user for retrival 169 | * 170 | * @see AuthComponent::startup() 171 | * 172 | * @param type $controller 173 | * @return boolean 174 | */ 175 | public function startup(Controller $controller) { 176 | $methods = array_flip(array_map('strtolower', $controller->methods)); 177 | $action = strtolower($controller->request->params['action']); 178 | 179 | $this->authenticate = Hash::merge($this->_authDefaults, $this->authenticate); 180 | $this->User = ClassRegistry::init(array( 181 | 'class' => $this->authenticate['userModel'], 182 | 'alias' => $this->authenticate['userModel'] 183 | )); 184 | 185 | $isMissingAction = ( 186 | $controller->scaffold === false && 187 | !isset($methods[$action]) 188 | ); 189 | if ($isMissingAction) { 190 | return true; 191 | } 192 | 193 | $allowedActions = $this->allowedActions; 194 | $isAllowed = ( 195 | $this->allowedActions == array('*') || 196 | in_array($action, array_map('strtolower', $allowedActions)) 197 | ); 198 | if ($isAllowed) { 199 | return true; 200 | } 201 | 202 | try { 203 | $this->isAuthorized(); 204 | $this->user(null, $this->AccessToken->id); 205 | } catch (OAuth2AuthenticateException $e) { 206 | $e->sendHttpResponse(); 207 | return false; 208 | } 209 | return true; 210 | } 211 | 212 | /** 213 | * Checks if user is valid using OAuth2-php library 214 | * 215 | * @see OAuth2::getBearerToken() 216 | * @see OAuth2::verifyAccessToken() 217 | * 218 | * @return boolean true if carrying valid token, false if not 219 | */ 220 | public function isAuthorized() { 221 | try { 222 | $this->AccessToken->id = $this->getBearerToken(); 223 | $this->verifyAccessToken($this->AccessToken->id); 224 | } catch (OAuth2AuthenticateException $e) { 225 | return false; 226 | } 227 | return true; 228 | } 229 | 230 | /** 231 | * Takes a list of actions in the current controller for which authentication is not required, or 232 | * no parameters to allow all actions. 233 | * 234 | * You can use allow with either an array, or var args. 235 | * 236 | * `$this->OAuth->allow(array('edit', 'add'));` or 237 | * `$this->OAuth->allow('edit', 'add');` or 238 | * `$this->OAuth->allow();` to allow all actions. 239 | * 240 | * @param string|array $action,... Controller action name or array of actions 241 | * @return void 242 | */ 243 | public function allow($action = null) { 244 | $args = func_get_args(); 245 | if (empty($args) || $action === null) { 246 | $this->allowedActions = $this->_methods; 247 | } else { 248 | if (isset($args[0]) && is_array($args[0])) { 249 | $args = $args[0]; 250 | } 251 | $this->allowedActions = array_merge($this->allowedActions, $args); 252 | } 253 | } 254 | 255 | /** 256 | * Removes items from the list of allowed/no authentication required actions. 257 | * 258 | * You can use deny with either an array, or var args. 259 | * 260 | * `$this->OAuth->deny(array('edit', 'add'));` or 261 | * `$this->OAuth->deny('edit', 'add');` or 262 | * `$this->OAuth->deny();` to remove all items from the allowed list 263 | * 264 | * @param string|array $action,... Controller action name or array of actions 265 | * @return void 266 | * @see OAuthComponent::allow() 267 | */ 268 | public function deny($action = null) { 269 | $args = func_get_args(); 270 | if (empty($args) || $action === null) { 271 | $this->allowedActions = array(); 272 | } else { 273 | if (isset($args[0]) && is_array($args[0])) { 274 | $args = $args[0]; 275 | } 276 | foreach ($args as $arg) { 277 | $i = array_search($arg, $this->allowedActions); 278 | if (is_int($i)) { 279 | unset($this->allowedActions[$i]); 280 | } 281 | } 282 | $this->allowedActions = array_values($this->allowedActions); 283 | } 284 | } 285 | /** 286 | * Gets the user associated to the current access token. 287 | * 288 | * Will return array of all user fields by default 289 | * You can specify specific fields like so: 290 | * 291 | * $id = $this->OAuth->user('id'); 292 | * 293 | * @param type $field 294 | * @return mixed array of user fields if $field is blank, string value if $field is set and $fields is avaliable, false on failure 295 | */ 296 | public function user($field = null, $token = null) { 297 | if (!$this->_user) { 298 | $this->AccessToken->bindModel(array( 299 | 'belongsTo' => array( 300 | 'User' => array( 301 | 'className' => $this->authenticate['userModel'], 302 | 'foreignKey' => 'user_id' 303 | ) 304 | ) 305 | )); 306 | $token = empty($token) ? $this->getBearerToken() : $token; 307 | $data = $this->AccessToken->find('first', array( 308 | 'conditions' => array('oauth_token' => $token), 309 | 'recursive' => 1 310 | )); 311 | if (!$data) { 312 | return false; 313 | } 314 | $this->_user = $data['User']; 315 | } 316 | if (empty($field)) { 317 | return $this->_user; 318 | } elseif (isset($this->_user[$field])) { 319 | return $this->_user[$field]; 320 | } 321 | return false; 322 | } 323 | 324 | /** 325 | * Convenience function for hashing client_secret (or whatever else) 326 | * 327 | * @param string $password 328 | * @return string Hashed password 329 | */ 330 | public static function hash($password) { 331 | return Security::hash($password, null, true); 332 | } 333 | 334 | /** 335 | * Convenience function to invalidate all a users tokens, for example when they change their password 336 | * 337 | * @param int $user_id 338 | * @param string $tokens 'both' (default) to remove both AccessTokens and RefreshTokens or remove just one type using 'access' or 'refresh' 339 | */ 340 | public function invalidateUserTokens($user_id, $tokens = 'both') { 341 | if ($tokens == 'access' || $tokens == 'both') { 342 | $this->AccessToken->deleteAll(array('user_id' => $user_id), false); 343 | } 344 | if ($tokens == 'refresh' || $tokens == 'both') { 345 | $this->RefreshToken->deleteAll(array('user_id' => $user_id), false); 346 | } 347 | } 348 | 349 | /** 350 | * Fakes the OAuth2.php vendor class extension for variables 351 | * 352 | * @param string $name 353 | * @return mixed 354 | */ 355 | public function __get($name) { 356 | if (isset($this->OAuth2->{$name})) { 357 | try { 358 | return $this->OAuth2->{$name}; 359 | } catch (Exception $e) { 360 | $e->sendHttpResponse(); 361 | } 362 | } 363 | } 364 | 365 | /** 366 | * Fakes the OAuth2.php vendor class extension for methods 367 | * 368 | * @param string $name 369 | * @param mixed $arguments 370 | * @return mixed 371 | * @throws Exception 372 | */ 373 | public function __call($name, $arguments) { 374 | if (method_exists($this->OAuth2, $name)) { 375 | try { 376 | return call_user_func_array(array($this->OAuth2, $name), $arguments); 377 | } catch (Exception $e) { 378 | if (method_exists($e, 'sendHttpResponse')) { 379 | $e->sendHttpResponse(); 380 | } 381 | throw $e; 382 | } 383 | } 384 | } 385 | 386 | /** 387 | * Below are the library interface implementations 388 | * 389 | */ 390 | 391 | /** 392 | * Check client details are valid 393 | * 394 | * @see IOAuth2Storage::checkClientCredentials(). 395 | * 396 | * @param string $client_id 397 | * @param string $client_secret 398 | * @return mixed array of client credentials if valid, false if not 399 | */ 400 | public function checkClientCredentials($client_id, $client_secret = null) { 401 | $conditions = array('client_id' => $client_id); 402 | if ($client_secret) { 403 | $conditions['client_secret'] = $client_secret; 404 | } 405 | $client = $this->Client->find('first', array( 406 | 'conditions' => $conditions, 407 | 'recursive' => -1 408 | )); 409 | if ($client) { 410 | return $client['Client']; 411 | }; 412 | return false; 413 | } 414 | 415 | /** 416 | * Get client details 417 | * 418 | * @see IOAuth2Storage::getClientDetails(). 419 | * 420 | * @param string $client_id 421 | * @return boolean 422 | */ 423 | public function getClientDetails($client_id) { 424 | $client = $this->Client->find('first', array( 425 | 'conditions' => array('client_id' => $client_id), 426 | 'fields' => array('client_id', 'redirect_uri'), 427 | 'recursive' => -1 428 | )); 429 | if ($client) { 430 | return $client['Client']; 431 | } 432 | return false; 433 | } 434 | 435 | /** 436 | * Retrieve access token 437 | * 438 | * @see IOAuth2Storage::getAccessToken(). 439 | * 440 | * @param string $oauth_token 441 | * @return mixed AccessToken array if valid, null if not 442 | */ 443 | public function getAccessToken($oauth_token) { 444 | $accessToken = $this->AccessToken->find('first', array( 445 | 'conditions' => array('oauth_token' => $oauth_token), 446 | 'recursive' => -1, 447 | )); 448 | if ($accessToken) { 449 | return $accessToken['AccessToken']; 450 | } 451 | return null; 452 | } 453 | 454 | /** 455 | * Set access token 456 | * 457 | * @see IOAuth2Storage::setAccessToken(). 458 | * 459 | * @param string $oauth_token 460 | * @param string $client_id 461 | * @param int $user_id 462 | * @param string $expires 463 | * @param string $scope 464 | * @return boolean true if successfull, false if failed 465 | */ 466 | public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null) { 467 | $data = array( 468 | 'oauth_token' => $oauth_token, 469 | 'client_id' => $client_id, 470 | 'user_id' => $user_id, 471 | 'expires' => $expires, 472 | 'scope' => $scope 473 | ); 474 | $this->AccessToken->create(); 475 | return $this->AccessToken->save(array('AccessToken' => $data)); 476 | } 477 | 478 | /** 479 | * Partial implementation, just checks globally avaliable grant types 480 | * 481 | * @see IOAuth2Storage::checkRestrictedGrantType() 482 | * 483 | * @param string $client_id 484 | * @param string $grant_type 485 | * @return boolean If grant type is availiable to client 486 | */ 487 | public function checkRestrictedGrantType($client_id, $grant_type) { 488 | return in_array($grant_type, $this->grantTypes); 489 | } 490 | 491 | /** 492 | * Grant type: refresh_token 493 | * 494 | * @see IOAuth2RefreshTokens::getRefreshToken() 495 | * 496 | * @param string $refresh_token 497 | * @return mixed RefreshToken if valid, null if not 498 | */ 499 | public function getRefreshToken($refresh_token) { 500 | $refreshToken = $this->RefreshToken->find('first', array( 501 | 'conditions' => array('refresh_token' => $refresh_token), 502 | 'recursive' => -1 503 | )); 504 | if ($refreshToken) { 505 | return $refreshToken['RefreshToken']; 506 | } 507 | return null; 508 | } 509 | 510 | /** 511 | * Grant type: refresh_token 512 | * 513 | * @see IOAuth2RefreshTokens::setRefreshToken() 514 | * 515 | * @param string $refresh_token 516 | * @param int $client_id 517 | * @param string $user_id 518 | * @param string $expires 519 | * @param string $scope 520 | * @return boolean true if successfull, false if fail 521 | */ 522 | public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) { 523 | $data = array( 524 | 'refresh_token' => $refresh_token, 525 | 'client_id' => $client_id, 526 | 'user_id' => $user_id, 527 | 'expires' => $expires, 528 | 'scope' => $scope 529 | ); 530 | $this->RefreshToken->create(); 531 | return $this->RefreshToken->save(array('RefreshToken' => $data)); 532 | } 533 | 534 | /** 535 | * Grant type: refresh_token 536 | * 537 | * @see IOAuth2RefreshTokens::unsetRefreshToken() 538 | * 539 | * @param string $refresh_token 540 | * @return boolean true if successfull, false if not 541 | */ 542 | public function unsetRefreshToken($refresh_token) { 543 | return $this->RefreshToken->delete($refresh_token); 544 | } 545 | 546 | /** 547 | * Grant type: user_credentials 548 | * 549 | * @see IOAuth2GrantUser::checkUserCredentials() 550 | * 551 | * @param type $client_id 552 | * @param type $username 553 | * @param type $password 554 | */ 555 | public function checkUserCredentials($client_id, $username, $password) { 556 | $user = $this->User->find('first', array( 557 | 'conditions' => array( 558 | $this->authenticate['fields']['username'] => $username, 559 | $this->authenticate['fields']['password'] => AuthComponent::password($password) 560 | ), 561 | 'recursive' => -1 562 | )); 563 | if ($user) { 564 | return array('user_id' => $user['User'][$this->User->primaryKey]); 565 | } 566 | return false; 567 | } 568 | 569 | /** 570 | * Grant type: authorization_code 571 | * 572 | * @see IOAuth2GrantCode::getAuthCode() 573 | * 574 | * @param string $code 575 | * @return AuthCode if valid, null of not 576 | */ 577 | public function getAuthCode($code) { 578 | $authCode = $this->AuthCode->find('first', array( 579 | 'conditions' => array('code' => $code), 580 | 'recursive' => -1 581 | )); 582 | if ($authCode) { 583 | return $authCode['AuthCode']; 584 | } 585 | return null; 586 | } 587 | 588 | /** 589 | * Grant type: authorization_code 590 | * 591 | * @see IOAuth2GrantCode::setAuthCode(). 592 | * 593 | * @param string $code 594 | * @param string $client_id 595 | * @param int $user_id 596 | * @param string $redirect_uri 597 | * @param string $expires 598 | * @param string $scope 599 | * @return boolean true if successfull, otherwise false 600 | */ 601 | public function setAuthCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null) { 602 | $data = array( 603 | 'code' => $code, 604 | 'client_id' => $client_id, 605 | 'user_id' => $user_id, 606 | 'redirect_uri' => $redirect_uri, 607 | 'expires' => $expires, 608 | 'scope' => $scope 609 | ); 610 | $this->AuthCode->create(); 611 | return $this->AuthCode->save(array('AuthCode' => $data)); 612 | } 613 | } 614 | -------------------------------------------------------------------------------- /Controller/OAuthAppController.php: -------------------------------------------------------------------------------- 1 | 9 | * @see https://github.com/thomseddon/cakephp-oauth-server 10 | * 11 | */ 12 | 13 | App::uses('OAuthAppController', 'OAuth.Controller'); 14 | 15 | /** 16 | * OAuthController 17 | * 18 | */ 19 | class OAuthController extends OAuthAppController { 20 | 21 | public $components = array('OAuth.OAuth', 'Auth', 'Session', 'Security'); 22 | 23 | public $uses = array('Users'); 24 | 25 | public $helpers = array('Form'); 26 | 27 | private $blackHoled = false; 28 | 29 | /** 30 | * beforeFilter 31 | * 32 | */ 33 | public function beforeFilter() { 34 | parent::beforeFilter(); 35 | $this->OAuth->authenticate = array('fields' => array('username' => 'email')); 36 | $this->Auth->allow($this->OAuth->allowedActions); 37 | $this->Security->blackHoleCallback = 'blackHole'; 38 | } 39 | 40 | /** 41 | * Example Authorize Endpoint 42 | * 43 | * Send users here first for authorization_code grant mechanism 44 | * 45 | * Required params (GET or POST): 46 | * - response_type = code 47 | * - client_id 48 | * - redirect_url 49 | * 50 | */ 51 | public function authorize() { 52 | if (!$this->Auth->loggedIn()) { 53 | $this->redirect(array('action' => 'login', '?' => $this->request->query)); 54 | } 55 | 56 | if ($this->request->is('post')) { 57 | $this->validateRequest(); 58 | 59 | $userId = $this->Auth->user('id'); 60 | 61 | if ($this->Session->check('OAuth.logout')) { 62 | $this->Auth->logout(); 63 | $this->Session->delete('OAuth.logout'); 64 | } 65 | 66 | //Did they accept the form? Adjust accordingly 67 | $accepted = $this->request->data['accept'] == 'Yep'; 68 | try { 69 | $this->OAuth->finishClientAuthorization($accepted, $userId, $this->request->data['Authorize']); 70 | } catch (OAuth2RedirectException $e) { 71 | $e->sendHttpResponse(); 72 | } 73 | } 74 | 75 | // Clickjacking prevention (supported by IE8+, FF3.6.9+, Opera10.5+, Safari4+, Chrome 4.1.249.1042+) 76 | $this->response->header('X-Frame-Options: DENY'); 77 | 78 | if ($this->Session->check('OAuth.params')) { 79 | $OAuthParams = $this->Session->read('OAuth.params'); 80 | $this->Session->delete('OAuth.params'); 81 | } else { 82 | try { 83 | $OAuthParams = $this->OAuth->getAuthorizeParams(); 84 | } catch (Exception $e){ 85 | $e->sendHttpResponse(); 86 | } 87 | } 88 | $this->set(compact('OAuthParams')); 89 | } 90 | 91 | /** 92 | * Example Login Action 93 | * 94 | * Users must authorize themselves before granting the app authorization 95 | * Allows login state to be maintained after authorization 96 | * 97 | */ 98 | public function login () { 99 | $OAuthParams = $this->OAuth->getAuthorizeParams(); 100 | if ($this->request->is('post')) { 101 | $this->validateRequest(); 102 | 103 | //Attempted login 104 | if ($this->Auth->login()) { 105 | //Write this to session so we can log them out after authenticating 106 | $this->Session->write('OAuth.logout', true); 107 | 108 | //Write the auth params to the session for later 109 | $this->Session->write('OAuth.params', $OAuthParams); 110 | 111 | //Off we go 112 | $this->redirect(array('action' => 'authorize')); 113 | } else { 114 | $this->Session->setFlash(__('Username or password is incorrect'), 'default', array(), 'auth'); 115 | } 116 | } 117 | $this->set(compact('OAuthParams')); 118 | } 119 | 120 | /** 121 | * Example Token Endpoint - this is where clients can retrieve an access token 122 | * 123 | * Grant types and parameters: 124 | * 1) authorization_code - exchange code for token 125 | * - code 126 | * - client_id 127 | * - client_secret 128 | * 129 | * 2) refresh_token - exchange refresh_token for token 130 | * - refresh_token 131 | * - client_id 132 | * - client_secret 133 | * 134 | * 3) password - exchange raw details for token 135 | * - username 136 | * - password 137 | * - client_id 138 | * - client_secret 139 | * 140 | */ 141 | public function token() { 142 | $this->autoRender = false; 143 | try { 144 | $this->OAuth->grantAccessToken(); 145 | } catch (OAuth2ServerException $e) { 146 | $e->sendHttpResponse(); 147 | } 148 | } 149 | 150 | /** 151 | * Quick and dirty example implementation for protecetd resource 152 | * 153 | * User accesible via $this->OAuth->user(); 154 | * Single fields avaliable via $this->OAuth->user("id"); 155 | * 156 | */ 157 | public function userinfo() { 158 | $this->layout = null; 159 | $user = $this->OAuth->user(); 160 | $this->set(compact('user')); 161 | } 162 | 163 | /** 164 | * Blackhold callback 165 | * 166 | * OAuth requests will fail postValidation, so rather than disabling it completely 167 | * if the request does fail this check we store it in $this->blackHoled and then 168 | * when handling our forms we can use $this->validateRequest() to check if there 169 | * were any errors and handle them with an exception. 170 | * Requests that fail for reasons other than postValidation are handled here immediately 171 | * using the best guess for if it was a form or OAuth 172 | * 173 | * @param string $type 174 | */ 175 | public function blackHole($type) { 176 | $this->blackHoled = $type; 177 | 178 | if ($type != 'auth') { 179 | if (isset($this->request->data['_Token'])) { 180 | //Probably our form 181 | $this->validateRequest(); 182 | } else { 183 | //Probably OAuth 184 | $e = new OAuth2ServerException(OAuth2::HTTP_BAD_REQUEST, OAuth2::ERROR_INVALID_REQUEST, 'Request Invalid.'); 185 | $e->sendHttpResponse(); 186 | } 187 | } 188 | } 189 | 190 | /** 191 | * Check for any Security blackhole errors 192 | * 193 | * @throws BadRequestException 194 | */ 195 | private function validateRequest() { 196 | if ($this->blackHoled) { 197 | //Has been blackholed before - naughty 198 | throw new BadRequestException(__d('OAuth', 'The request has been black-holed')); 199 | } 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2010 Seddon Media 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Model/AccessToken.php: -------------------------------------------------------------------------------- 1 | array( 34 | 'notempty' => array( 35 | 'rule' => array('notempty'), 36 | ), 37 | 'isUnique' => array( 38 | 'rule' => array('isUnique'), 39 | ) 40 | ), 41 | 'client_id' => array( 42 | 'notempty' => array( 43 | 'rule' => array('notempty'), 44 | ), 45 | ), 46 | 'user_id' => array( 47 | 'notempty' => array( 48 | 'rule' => array('notempty'), 49 | ), 50 | ), 51 | 'expires' => array( 52 | 'numeric' => array( 53 | 'rule' => array('numeric'), 54 | ), 55 | ), 56 | ); 57 | 58 | public $actsAs = array( 59 | 'OAuth.HashedField' => array( 60 | 'fields' => 'oauth_token', 61 | ), 62 | ); 63 | 64 | /** 65 | * belongsTo associations 66 | * 67 | * @var array 68 | */ 69 | public $belongsTo = array( 70 | 'Client' => array( 71 | 'className' => 'OAuth.Client', 72 | 'foreignKey' => 'client_id', 73 | 'conditions' => '', 74 | 'fields' => '', 75 | 'order' => '' 76 | ) 77 | ); 78 | 79 | } 80 | -------------------------------------------------------------------------------- /Model/AuthCode.php: -------------------------------------------------------------------------------- 1 | array( 34 | 'notempty' => array( 35 | 'rule' => array('notempty'), 36 | ), 37 | 'isUnique' => array( 38 | 'rule' => array('isUnique'), 39 | ), 40 | ), 41 | 'client_id' => array( 42 | 'notempty' => array( 43 | 'rule' => array('notempty'), 44 | ), 45 | ), 46 | 'user_id' => array( 47 | 'notempty' => array( 48 | 'rule' => array('notempty'), 49 | ), 50 | ), 51 | 'redirect_uri' => array( 52 | 'notempty' => array( 53 | 'rule' => array('notempty'), 54 | ), 55 | ), 56 | 'expires' => array( 57 | 'numeric' => array( 58 | 'rule' => array('numeric'), 59 | ), 60 | ), 61 | ); 62 | 63 | public $actsAs = array( 64 | 'OAuth.HashedField' => array( 65 | 'fields' => 'code', 66 | ), 67 | ); 68 | 69 | /** 70 | * belongsTo associations 71 | * 72 | * @var array 73 | */ 74 | public $belongsTo = array( 75 | 'Client' => array( 76 | 'className' => 'OAuth.Client', 77 | 'foreignKey' => 'client_id', 78 | 'conditions' => '', 79 | 'fields' => '', 80 | 'order' => '' 81 | ), 82 | 'User' => array( 83 | 'className' => 'User', 84 | 'foreignKey' => 'user_id', 85 | 'conditions' => '', 86 | 'fields' => '', 87 | 'order' => '' 88 | ) 89 | ); 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Model/Behavior/HashedFieldBehavior.php: -------------------------------------------------------------------------------- 1 | array(), 16 | ); 17 | 18 | /** 19 | * Setup behavior 20 | */ 21 | public function setup(Model $Model, $config = array()) { 22 | parent::setup($Model, $config); 23 | $this->settings[$Model->name] = Hash::merge($this->_defaults, $config); 24 | } 25 | 26 | /** 27 | * Hash field when present in model data (POSTed data) 28 | */ 29 | public function beforeSave(Model $Model, $options = array()) { 30 | $setting = $this->settings[$Model->name]; 31 | foreach ((array) $setting['fields'] as $field) { 32 | if (array_key_exists($field, $Model->data[$Model->alias])) { 33 | $data = $Model->data[$Model->alias][$field]; 34 | $Model->data[$Model->alias][$field] = Security::hash($data, null, true); 35 | } 36 | } 37 | return true; 38 | } 39 | 40 | /** 41 | * Hash condition when it contains a field specified in setting 42 | */ 43 | public function beforeFind(Model $Model, $queryData) { 44 | $setting = $this->settings[$Model->name]; 45 | $conditions =& $queryData['conditions']; 46 | foreach ((array) $setting['fields'] as $field) { 47 | $escapeField = $Model->escapeField($field); 48 | if (array_key_exists($field, (array)$conditions)) { 49 | $queryField = $field; 50 | } elseif (array_key_exists($escapeField, (array)$conditions)) { 51 | $queryField = $escapeField; 52 | } 53 | if (isset($queryField)) { 54 | $data = $conditions[$queryField]; 55 | $conditions[$queryField] = Security::hash($data, null, true); 56 | } 57 | } 58 | return $queryData; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Model/Client.php: -------------------------------------------------------------------------------- 1 | array( 43 | 'isUnique' => array( 44 | 'rule' => array('isUnique'), 45 | ), 46 | 'notempty' => array( 47 | 'rule' => array('notempty'), 48 | ), 49 | ), 50 | 'redirect_uri' => array( 51 | 'notempty' => array( 52 | 'rule' => array('notempty'), 53 | ), 54 | ), 55 | ); 56 | 57 | public $actsAs = array( 58 | 'OAuth.HashedField' => array( 59 | 'fields' => array( 60 | 'client_secret' 61 | ), 62 | ), 63 | ); 64 | 65 | /** 66 | * hasMany associations 67 | * 68 | * @var array 69 | */ 70 | public $hasMany = array( 71 | 'AccessToken' => array( 72 | 'className' => 'OAuth.AccessToken', 73 | 'foreignKey' => 'client_id', 74 | 'dependent' => false, 75 | 'conditions' => '', 76 | 'fields' => '', 77 | 'order' => '', 78 | 'limit' => '', 79 | 'offset' => '', 80 | 'exclusive' => '', 81 | 'finderQuery' => '', 82 | 'counterQuery' => '' 83 | ), 84 | 'AuthCode' => array( 85 | 'className' => 'OAuth.AuthCode', 86 | 'foreignKey' => 'client_id', 87 | 'dependent' => false, 88 | 'conditions' => '', 89 | 'fields' => '', 90 | 'order' => '', 91 | 'limit' => '', 92 | 'offset' => '', 93 | 'exclusive' => '', 94 | 'finderQuery' => '', 95 | 'counterQuery' => '' 96 | ), 97 | 'RefreshToken' => array( 98 | 'className' => 'OAuth.RefreshToken', 99 | 'foreignKey' => 'client_id', 100 | 'dependent' => false, 101 | 'conditions' => '', 102 | 'fields' => '', 103 | 'order' => '', 104 | 'limit' => '', 105 | 'offset' => '', 106 | 'exclusive' => '', 107 | 'finderQuery' => '', 108 | 'counterQuery' => '' 109 | ) 110 | ); 111 | 112 | /** 113 | * AddClient 114 | * 115 | * Convinience function for adding client, will create a uuid client_id and random secret 116 | * 117 | * @param mixed $data Either an array (e.g. $controller->request->data) or string redirect_uri 118 | * @return booleen Success of failure 119 | */ 120 | public function add($data = null) { 121 | $this->data['Client'] = array(); 122 | 123 | if (is_array($data) && is_array($data['Client']) && array_key_exists('redirect_uri', $data['Client'])) { 124 | $this->data['Client']['redirect_uri'] = $data['Client']['redirect_uri']; 125 | } elseif (is_string($data)) { 126 | $this->data['Client']['redirect_uri'] = $data; 127 | } else { 128 | return false; 129 | } 130 | 131 | /** 132 | * in case you have additional fields in the clients table such as name, description etc 133 | * and you are using $data['Client']['name'], etc to save 134 | **/ 135 | if (is_array($data['Client'])) { 136 | $this->data['Client'] = array_merge($data['Client'], $this->data['Client']); 137 | } 138 | 139 | //You may wish to change this 140 | $this->data['Client']['client_id'] = base64_encode(uniqid() . substr(uniqid(), 11, 2)); // e.g. NGYcZDRjODcxYzFkY2Rk (seems popular format) 141 | //$this->data['Client']['client_id'] = uniqid(); // e.g. 4f3d4c8602346 142 | //$this->data['Client']['client_id'] = str_replace('.', '', uniqid('', true)); // e.g. 4f3d4c860235a529118898 143 | //$this->data['Client']['client_id'] = str_replace('-', '', String::uuid()); // e.g. 4f3d4c80cb204b6a8e580a006f97281a 144 | 145 | $this->addClientSecret = $this->newClientSecret(); 146 | $this->data['Client']['client_secret'] = $this->addClientSecret; 147 | 148 | return $this->save($this->data); 149 | } 150 | 151 | /** 152 | * Create a new, pretty (as in moderately, not beautiful - that can't be guaranteed ;-) random client secret 153 | * 154 | * @return string 155 | */ 156 | public function newClientSecret() { 157 | $length = 40; 158 | $chars = '@#!%*+/-=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 159 | $str = ''; 160 | $count = strlen($chars); 161 | while ($length--) { 162 | $str .= $chars[mt_rand(0, $count - 1)]; 163 | } 164 | return OAuthComponent::hash($str); 165 | } 166 | 167 | public function afterSave($created, $options = array()) { 168 | if ($this->addClientSecret) { 169 | $this->data['Client']['client_secret'] = $this->addClientSecret; 170 | } 171 | return true; 172 | } 173 | 174 | } -------------------------------------------------------------------------------- /Model/OAuthAppModel.php: -------------------------------------------------------------------------------- 1 | array( 34 | 'notempty' => array( 35 | 'rule' => array('notempty'), 36 | ), 37 | 'isUnique' => array( 38 | 'rule' => array('isUnique'), 39 | ) 40 | ), 41 | 'client_id' => array( 42 | 'notempty' => array( 43 | 'rule' => array('notempty'), 44 | ), 45 | ), 46 | 'user_id' => array( 47 | 'notempty' => array( 48 | 'rule' => array('notempty'), 49 | ), 50 | ), 51 | 'expires' => array( 52 | 'numeric' => array( 53 | 'rule' => array('numeric'), 54 | ), 55 | ), 56 | ); 57 | 58 | public $actsAs = array( 59 | 'OAuth.HashedField' => array( 60 | 'fields' => 'refresh_token', 61 | ), 62 | ); 63 | 64 | /** 65 | * belongsTo associations 66 | * 67 | * @var array 68 | */ 69 | public $belongsTo = array( 70 | 'Client' => array( 71 | 'className' => 'OAuth.Client', 72 | 'foreignKey' => 'client_id', 73 | 'conditions' => '', 74 | 'fields' => '', 75 | 'order' => '' 76 | ) 77 | ); 78 | 79 | } 80 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # This project is unmaintained, see https://github.com/uafrica/oauth-server as an alternative 2 | 3 | 4 | ---- 5 | 6 | # CakePHP OAuth2 Server Plugin 7 | 8 | This is a plugin for implementing an OAuth Server/Provider in CakePHP, built on quizlets [oauth2-php library][1] 9 | 10 | ## What's inside? 11 | * A lovely OAuth component that allows cakey access to the oauth library 12 | * The required models with super safe automatic beforeSave token hashing 13 | * AuthComponent'ish interface for action allow/deny's 14 | * Convenience functions for retrieving the current user and adding clients 15 | * An example controller with authorize and token end points 16 | 17 | ## Requirements 18 | [CakePHP 2.x](http://cakephp.org/) 19 | 20 | 21 | A clone of [oauth2-php][1] in your Vendors folder 22 | ### Cloning oauth2-php 23 | ``` 24 | $ git clone git://github.com/quizlet/oauth2-php.git Vendor/oauth2-php 25 | ``` 26 | Or via submodule: 27 | 28 | ``` 29 | $ git submodule add git://github.com/quizlet/oauth2-php.git Vendor/oauth2-php 30 | ``` 31 | 32 | ## Installation 33 | 34 | ### Populate database 35 | First we need to populate the database with the right tables. 36 | 37 | Two ways: use schema.sql or Migrations using [Migrations Plugin from CakeDC][2] 38 | 39 | Go to Config/Schema/schema.sql to grab the tables 40 | 41 | **OR** 42 | 43 | ``` 44 | $ cake Migrations.migration run all --plugin OAuth 45 | ``` 46 | 47 | ### Cloning 48 | Then clone this repo into a "OAuth" folder in your Plugins folder: 49 | 50 | ``` 51 | $ git clone git://github.com/thomseddon/cakephp-oauth-server.git Plugin/OAuth 52 | ``` 53 | Or via submodule: 54 | 55 | ``` 56 | $ git submodule add git://github.com/thomseddon/cakephp-oauth-server.git Plugin/OAuth 57 | ``` 58 | 59 | ### Loading the Plugin 60 | Load the plugin 61 | 62 | ```PHP 63 | CakePlugin::loadAll(); // Loads all plugins at once 64 | CakePlugin::load('OAuth'); //Just load OAuth 65 | ``` 66 | 67 | ### Include component in controller 68 | And include the component in your controller: 69 | 70 | ```PHP 71 | $components = array('OAuth.OAuth'); 72 | ``` 73 | 74 | 75 | ## Getting Started 76 | ### OAuth 77 | **A good understanding of the OAuth protocol should be considered a prerequisite of using this plugin.** 78 | Good documentation explaining various OAuth2 flows is provided by [Google](https://developers.google.com/accounts/docs/OAuth2), [Facebook](http://developers.facebook.com/docs/authentication/) and [in the official spec](http://tools.ietf.org/html/draft-ietf-oauth-v2-23). 79 | For reference, this plugin currently supports the following grant types: 80 | 81 | * [Authorization Code Grant](http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-4.1) 82 | * [Refresh Token Grant](http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-6) 83 | * [Resource Owner Password Credentials Grant](http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-4.3) (requires setup, see below) 84 | 85 | If you need any others please build them into the base [oauth2-php library][1] and let me know :) 86 | 87 | It should be noted here that most OAuth methods support both GET and POST, so you can test your setup straight from the browser. 88 | 89 | ### Controller Setup 90 | To use the "Resource Owner Password Credentials Grant" you need to configure the plugin so it knows where to look for your users username/password combinations. By default it will try a "Users" model with "username" and "password" fields, you can change this in your controllers beforeFilter like so: 91 | 92 | ```PHP 93 | $this->OAuth->authenticate = array( 94 | 'userModel' => 'Members', 95 | 'fields' => array( 96 | 'username' => 'email' 97 | ) 98 | ); 99 | ``` 100 | 101 | You can control what actions can be accessed using an OAuth access token in the same way you control access with the AuthComponent, so for example placing this in a controller's beforeFilter: 102 | 103 | ```PHP 104 | $this->OAuth->allow(array('userinfo', 'example')); 105 | ``` 106 | Would allow access to the "userinfo" and "example" actions. 107 | 108 | ### Adding OAuth Clients 109 | An OAuth client is an application that can access resources on behalf of resource owner, i.e. someone who can use your API. 110 | 111 | This plugin ships with all required models, including the "Clients" model for adding and accessing OAuth clients. 112 | You may wish to handle adding clients yourself, see the tables.sql for the schema, or you can use the convenience method included in the model, like so: 113 | 114 | ```PHP 115 | $client = $this->OAuth->Client->add('http://www.return_url.com') 116 | ``` 117 | Which will generate then client_id and client_secret and return something like: 118 | 119 | ``` 120 | Array( 121 | [client_id] => NGYcZDRjODcxYzFkY2Rk 122 | [client_secret] => 8e7ff3208eed06d101bf3da2473fc92ac1c6d2e7 123 | [redirect_uri] => http://www.return_url.com 124 | ) 125 | ``` 126 | 127 | The method includes various schemes for generating client id's, [pick your favourite](https://github.com/thomseddon/cakephp-oauth-server/blob/master/Model/Client.php#L122). 128 | 129 | **NOTE:** This convenience method will generate a random client secret __and hash it__ for security before storage. Although it will pass back the actual raw client secret when you first add a new client, it is not possible to ever determine this from the hash stored in the database. So if the client forgets their secret, [a new one will have to be issued](https://github.com/thomseddon/cakephp-oauth-server/blob/master/Model/Client.php#L139). 130 | 131 | 132 | ### Included Endpoints 133 | This plugin ships with an example controller that provides the necessary endpoints to generate access tokens. Routes are also included to give you sexy URL's like: "/oauth/token", you can fire them up by placing this in your bootstrap.php: 134 | 135 | ```PHP 136 | CakePlugin::loadAll(array( 137 | 'OAuth' => array('routes' => true) 138 | )); 139 | ``` 140 | 141 | 142 | As an example, once you have registered a client, you could then use the Authorization Code Grant like so: 143 | 144 | 1. Get an Authorization code 145 | * `/oauth/authorize?response_type=code&client_id=xxxx&redirect_url=http%3a%2f%2flocalhost` 146 | * (note the URL encoding on the redirect_uri) 147 | 2. Swap code for access token 148 | * `/oauth/token?grant_type=authorization_code&code=from_above&client_id=xxxx&client_secret=xxxx` 149 | 3. Use access token 150 | * `/oauth/userinfo?access_token=from_above` 151 | 152 | 153 | There is quite a bit of documentation through the code, so dive in, get your hands dirty and submit any issues here! 154 | 155 | 156 | [1]: https://github.com/quizlet/oauth2-php 157 | [2]: https://github.com/CakeDC/migrations 158 | -------------------------------------------------------------------------------- /Test/Case/AllOAuthTestsTest.php: -------------------------------------------------------------------------------- 1 | addTestDirectory($path . 'Model' . DS . 'Behavior'); 9 | return $suite; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /Test/Case/Model/Behavior/HashedFieldBehaviorTest.php: -------------------------------------------------------------------------------- 1 | AccessToken = ClassRegistry::init('OAuth.AccessToken'); 17 | $this->AccessToken->recursive = -1; 18 | } 19 | 20 | protected function _createToken() { 21 | $this->AccessToken->create(array( 22 | 'client_id' => $this->_clientId, 23 | 'oauth_token' => $this->_token, 24 | 'user_id' => 69, 25 | 'expires' => time() + 86400, 26 | )); 27 | $this->AccessToken->save(); 28 | } 29 | 30 | public function testBeforeSave() { 31 | $this->_createToken(); 32 | 33 | $result = $this->AccessToken->findByClientId($this->_clientId); 34 | $expected = Security::hash($this->_token, null, true); 35 | $this->assertEquals($expected, $result['AccessToken']['oauth_token']); 36 | } 37 | 38 | public function testBeforeFind() { 39 | $this->_createToken(); 40 | 41 | $result = $this->AccessToken->find('first', array( 42 | 'conditions' => array( 43 | 'oauth_token' => $this->_token, 44 | ), 45 | )); 46 | $this->assertEquals($this->_clientId, $result['AccessToken']['client_id']); 47 | $this->assertNotEquals($this->_token, $result['AccessToken']['oauth_token']); 48 | } 49 | 50 | /** 51 | * test saving with missing oauth_token in POSTed data does not corrupt value 52 | */ 53 | public function testSavingWithMissingToken() { 54 | $this->_createToken(); 55 | 56 | $baseline = $this->AccessToken->find('first'); 57 | $this->assertNull($baseline['AccessToken']['scope']); 58 | 59 | $updated = $baseline; 60 | $updated['AccessToken']['scope'] = 'all'; 61 | unset($updated['AccessToken']['oauth_token']); 62 | 63 | $this->assertFalse(array_key_exists('oauth_token', $updated)); 64 | $this->AccessToken->save($updated); 65 | 66 | $result = $this->AccessToken->findByClientId($baseline['AccessToken']['client_id']); 67 | 68 | $this->assertEquals('all', $result['AccessToken']['scope']); 69 | $expected = Security::hash($this->_token, null, true); 70 | $this->assertEquals($expected, $result['AccessToken']['oauth_token']); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Test/Fixture/AccessTokenFixture.php: -------------------------------------------------------------------------------- 1 | array('type' => 'string', 'null' => false, 'default' => null, 'length' => 40, 'key' => 'primary', 'collate' => 'utf8_general_ci', 'charset' => 'utf8'), 15 | 'client_id' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 36, 'collate' => 'utf8_general_ci', 'charset' => 'utf8'), 16 | 'user_id' => array('type' => 'integer', 'null' => false, 'default' => null), 17 | 'expires' => array('type' => 'integer', 'null' => false, 'default' => null), 18 | 'scope' => array('type' => 'string', 'null' => true, 'default' => null, 'collate' => 'utf8_general_ci', 'charset' => 'utf8'), 19 | 'indexes' => array( 20 | 'PRIMARY' => array('column' => 'oauth_token', 'unique' => 1) 21 | ), 22 | 'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'MyISAM') 23 | ); 24 | 25 | /** 26 | * Records 27 | * 28 | * @var array 29 | */ 30 | public $records = array( 31 | ); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /View/OAuth/authorize.ctp: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | Form->create('Authorize'); 12 | 13 | foreach ($OAuthParams as $key => $value) { 14 | echo $this->Form->hidden(h($key), array('value' => h($value))); 15 | } 16 | ?> 17 | 18 | Do you authorize the app to do its thing? 19 | 20 | Form->submit('Yep', array('name' => 'accept')); 22 | echo $this->Form->submit('Nope', array('name' => 'accept')); 23 | echo $this->Form->end(); 24 | ?> 25 | -------------------------------------------------------------------------------- /View/OAuth/login.ctp: -------------------------------------------------------------------------------- 1 | Session->flash('auth'); 4 | 5 | echo $this->Form->create('User'); 6 | 7 | foreach ($OAuthParams as $key => $value) { 8 | echo $this->Form->hidden(h($key), array('value' => h($value))); 9 | } 10 | 11 | ?> 12 | 13 | Please login 14 | 15 | Form->input('email'); 17 | echo $this->Form->input('password'); 18 | 19 | echo $this->Form->end('submit'); 20 | 21 | ?> 22 | -------------------------------------------------------------------------------- /View/OAuth/userinfo.ctp: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "thomseddon/cakephp-oauth-server", 3 | "description" : "CakePHP OAuth2 Server Plugin", 4 | "type" : "cakephp-plugin", 5 | "authors" : [{ 6 | "name" : "Thom Seddon", 7 | "homepage" : "http://www.thomseddon.co.uk/", 8 | "role" : "Author" 9 | }, { 10 | "name" : "CakePHP Community", 11 | "homepage" : "https://github.com/thomseddon/cakephp-oauth-server/graphs/contributors" 12 | } 13 | ], 14 | "keywords" : [ 15 | "cakephp", 16 | "oauth", 17 | "server" 18 | ], 19 | "homepage" : "https://github.com/thomseddon/cakephp-oauth-server", 20 | "license" : [ 21 | "MIT" 22 | ], 23 | "require" : { 24 | "composer/installers" : "*", 25 | "php" : ">=5.3.0", 26 | "thomseddon/oauth2-php": "dev-master" 27 | }, 28 | "abandoned": true, 29 | "support" : { 30 | "forum" : "http://stackoverflow.com/tags/cakephp", 31 | "source" : "https://github.com/thomseddon/cakephp-oauth-server", 32 | "issues" : "https://github.com/thomseddon/cakephp-oauth-server/issues", 33 | "irc" : "irc://irc.freenode.org/cakephp" 34 | }, 35 | "extra" : { 36 | "installer-name" : "OAuth" 37 | } 38 | } 39 | --------------------------------------------------------------------------------