├── Controller └── Component │ └── AuthsomeComponent.php └── README.md /Controller/Component/AuthsomeComponent.php: -------------------------------------------------------------------------------- 1 | 'User', 41 | 'configureKey' => null, 42 | 'sessionKey' => null, 43 | 'cookieKey' => null, 44 | ); 45 | 46 | private $__userModel; 47 | 48 | /** 49 | * Constructor. 50 | * 51 | * @param ComponentCollection $collection 52 | * @param array $settings 53 | */ 54 | public function initialize(Controller $controller) { 55 | Authsome::instance($this); 56 | 57 | // Use the model name as the key everywhere by default 58 | $keys = array('configureKey', 'sessionKey', 'cookieKey'); 59 | foreach ($keys as $key) { 60 | if (empty($this->settings[$key])) { 61 | $this->settings[$key] = $this->settings['model']; 62 | } 63 | } 64 | 65 | parent::initialize($controller); 66 | } 67 | 68 | public function get($field = null) { 69 | $user = $this->__getActiveUser(); 70 | 71 | if (empty($field)) { 72 | return $user; 73 | } 74 | 75 | if (strpos($field, '.') === false) { 76 | if (in_array($field, array_keys($user))) { 77 | return $user[$field]; 78 | } 79 | $field = $this->settings['model'].'.'.$field; 80 | } 81 | 82 | return Set::extract($user, $field); 83 | } 84 | 85 | public function set($fields = null, $value = null) { 86 | if ($fields === null) { 87 | return false; 88 | } 89 | 90 | if (!is_array($fields)) { 91 | $fields = array($fields => $value); 92 | } 93 | 94 | $user = $this->Session->read($this->settings['sessionKey']); 95 | if (empty($user)) { 96 | $user = array(); 97 | } 98 | 99 | foreach ($fields as $field => $value) { 100 | if (strstr($field, '.') === false) { 101 | $user[$this->settings['model']][$field] = $value; 102 | } else { 103 | $user = Set::insert($user, $field, $value); 104 | } 105 | } 106 | 107 | $this->Session->write($this->settings['sessionKey'], $user); 108 | Configure::write($this->settings['sessionKey'], $user); 109 | return true; 110 | } 111 | 112 | public function delete($fields = null) { 113 | if ($fields === null) { 114 | return false; 115 | } 116 | 117 | if (!is_array($fields)) { 118 | $fields = (array) $fields; 119 | } 120 | 121 | $user = $this->Session->read($this->settings['sessionKey']); 122 | if (!$user) { 123 | return true; 124 | } 125 | 126 | foreach ($fields as $field) { 127 | if (strstr($field, '.') !== false) { 128 | $user = Set::remove($user, $field, $value); 129 | } else if (isset($user[$this->settings['model']][$field])) { 130 | unset($user[$this->settings['model']][$field]); 131 | } 132 | } 133 | 134 | $this->Session->write($this->settings['sessionKey'], $user); 135 | Configure::write($this->settings['sessionKey'], $user); 136 | return true; 137 | } 138 | 139 | public function login($type = 'credentials', $credentials = null) { 140 | $userModel = $this->__getUserModel(); 141 | 142 | $args = func_get_args(); 143 | if (!method_exists($userModel, 'authsomeLogin')) { 144 | throw new Exception( 145 | $userModel->alias.'::authsomeLogin() is not implemented!' 146 | ); 147 | } 148 | 149 | if (!is_string($type) && is_null($credentials)) { 150 | $credentials = $type; 151 | $type = 'credentials'; 152 | } 153 | 154 | $user = $userModel->authsomeLogin($type, $credentials); 155 | 156 | Configure::write($this->settings['configureKey'], $user); 157 | $this->Session->write($this->settings['sessionKey'], $user); 158 | return $user; 159 | } 160 | 161 | public function logout() { 162 | Configure::write($this->settings['configureKey'], array()); 163 | $this->Session->write($this->settings['sessionKey'], array()); 164 | if (!empty($this->settings['cookieKey'])) { 165 | $this->Cookie->write($this->settings['cookieKey'], ''); 166 | } 167 | return true; 168 | } 169 | 170 | public function persist($duration = '2 weeks') { 171 | $userModel = $this->__getUserModel(); 172 | 173 | if (!method_exists($userModel, 'authsomePersist')) { 174 | throw new Exception( 175 | $userModel->alias.'::authsomePersist() is not implemented!' 176 | ); 177 | } 178 | 179 | $token = $userModel->authsomePersist(Authsome::get(), $duration); 180 | $token = $token.':'.$duration; 181 | 182 | if (empty($this->settings['cookieKey'])) { 183 | return false; 184 | } 185 | 186 | return $this->Cookie->write( 187 | $this->settings['cookieKey'], 188 | $token, 189 | true, // encrypt = true 190 | $duration 191 | ); 192 | } 193 | 194 | public function hash($password) { 195 | return Authsome::hash($password); 196 | } 197 | 198 | private function __getUserModel() { 199 | if ($this->__userModel) { 200 | return $this->__userModel; 201 | } 202 | 203 | return $this->__userModel = ClassRegistry::init( 204 | $this->settings['model'] 205 | ); 206 | } 207 | 208 | private function __getActiveUser() { 209 | $user = Configure::read($this->settings['configureKey']); 210 | if (!empty($user)) { 211 | return $user; 212 | } 213 | 214 | $this->__useSession() || 215 | $this->__useCookieToken() || 216 | $this->__useGuestAccount(); 217 | 218 | $user = Configure::read($this->settings['configureKey']); 219 | if (is_null($user)) { 220 | throw new Exception( 221 | 'Unable to initilize user' 222 | ); 223 | } 224 | 225 | return $user; 226 | } 227 | 228 | private function __useSession() { 229 | $user = $this->Session->read($this->settings['sessionKey']); 230 | if (!$user) { 231 | return false; 232 | } 233 | 234 | Configure::write($this->settings['configureKey'], $user); 235 | return true; 236 | } 237 | 238 | private function __useCookieToken() { 239 | if (empty($this->settings['cookieKey'])) { 240 | return false; 241 | } 242 | $token = $this->Cookie->read($this->settings['cookieKey']); 243 | if (!$token || !is_string($token)) { 244 | return false; 245 | } 246 | 247 | // Extract the duration appendix from the token 248 | $tokenParts = split(':', $token); 249 | $duration = array_pop($tokenParts); 250 | $token = join(':', $tokenParts); 251 | 252 | $user = $this->login('cookie', compact('token', 'duration')); 253 | 254 | // Delete the cookie once its been used 255 | $this->Cookie->delete($this->settings['cookieKey']); 256 | 257 | if (!$user) { 258 | return; 259 | } 260 | 261 | $this->persist($duration); 262 | 263 | return (bool)$user; 264 | } 265 | 266 | private function __useGuestAccount() { 267 | return $this->login('guest'); 268 | } 269 | 270 | } 271 | 272 | // Static Authsomeness 273 | class Authsome{ 274 | static function instance($setInstance = null) { 275 | static $instance; 276 | 277 | if ($setInstance) { 278 | $instance = $setInstance; 279 | } 280 | 281 | if (!$instance) { 282 | throw new Exception( 283 | 'AuthsomeComponent not initialized properly!' 284 | ); 285 | } 286 | 287 | return $instance; 288 | } 289 | 290 | public static function get($field = null) { 291 | return self::instance()->get($field); 292 | } 293 | 294 | public static function set($field = null, $value = null) { 295 | return self::instance()->set($field, $value); 296 | } 297 | 298 | public static function delete($field = null, $value = null) { 299 | return self::instance()->delete($field, $value); 300 | } 301 | 302 | public static function login($type = 'credentials', $credentials = null) { 303 | return self::instance()->login($type, $credentials); 304 | } 305 | 306 | public static function logout() { 307 | return self::instance()->logout(); 308 | } 309 | 310 | public static function persist($duration = '2 weeks') { 311 | return self::instance()->persist($duration); 312 | } 313 | 314 | public static function hash($password, $method = 'sha1', $salt = true) { 315 | return Security::hash($password, $method, $salt); 316 | } 317 | 318 | } 319 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Authsome Plugin 2 | 3 | Auth for people who hate the Auth component 4 | 5 | ## Background 6 | 7 | Authsome is a CakePHP 2.x plugin that makes authentication a pleasure to work with by following a few simple rules: 8 | 9 | **Assume nothing:** Authsome requires that you have some kind of user model, but that's it. It doesn't care if you use a database, passwords or religious ceremonies for verifying your member logins. 10 | 11 | **Touch nothing:** Authsome does not interact with your application at all. No login redirects, no permissions checks, nothing. You never have to worry about the underlaying magic, it will never get into your way. 12 | 13 | **Always available:** Authsome is there for you when you need it. You can do stuff like `Authsome::get('id')` from anywhere in your project. If you have MVC OCD, you can also use Authsome as a regular component: `$this->Authsome->get('id')` 14 | 15 | ## Requirements 16 | 17 | * PHP 5.2.8+ 18 | * CakePHP 2.x 19 | 20 | ## Installation 21 | 22 | For 1.3 support, please see the [1.3 branch](https://github.com/felixge/cakephp-authsome/tree/1.3); 23 | 24 | _[Manual]_ 25 | 26 | * Download this: [https://github.com/felixge/cakephp-authsome/zipball/master](https://github.com/felixge/cakephp-authsome/zipball/master) 27 | * Unzip that download. 28 | * Copy the resulting folder to `app/Plugin` 29 | * Rename the folder you just copied to `Authsome` 30 | 31 | _[GIT Submodule]_ 32 | 33 | In your app directory type: 34 | 35 | git submodule add git://github.com/felixge/cakephp-authsome.git Plugin/Authsome 36 | git submodule init 37 | git submodule update 38 | 39 | _[GIT Clone]_ 40 | 41 | In your plugin directory type 42 | 43 | git clone git://github.com/felixge/cakephp-authsome.git Authsome 44 | 45 | ### Enable plugin 46 | 47 | In 2.0 you need to enable the plugin your `app/Config/bootstrap.php` file: 48 | 49 | CakePlugin::load('Authsome'); 50 | 51 | If you are already using `CakePlugin::loadAll();`, then this is not necessary. 52 | 53 | ## Usage 54 | 55 | Once installed, load authsome in your AppController and specify the name of your user model: 56 | 57 | class AppController extends Controller { 58 | public $components = array( 59 | 'Authsome.Authsome' => array( 60 | 'model' => 'User' 61 | ) 62 | ); 63 | } 64 | 65 | Implement authsomeLogin in your user model (must return a non-null value): 66 | 67 | class User extends AppModel{ 68 | public function authsomeLogin($type, $credentials = array()) { 69 | switch ($type) { 70 | case 'guest': 71 | // You can return any non-null value here, if you don't 72 | // have a guest account, just return an empty array 73 | return array('it' => 'works'); 74 | case 'credentials': 75 | $password = Authsome::hash($credentials['password']); 76 | 77 | // This is the logic for validating the login 78 | $conditions = array( 79 | 'User.email' => $credentials['email'], 80 | 'User.password' => $password, 81 | ); 82 | break; 83 | default: 84 | return null; 85 | } 86 | 87 | return $this->find('first', compact('conditions')); 88 | } 89 | } 90 | 91 | Almost done! Check if you did everything right so far by putting this in one of your controllers: 92 | 93 | $guest = Authsome::get(); 94 | debug($guest); 95 | 96 | If this returns `Array([it] => works)`, you can go ahead and implement a simple login function: 97 | 98 | class UsersController extends AppController{ 99 | public function login() { 100 | if (empty($this->data)) { 101 | return; 102 | } 103 | 104 | $user = Authsome::login($this->data['User']); 105 | 106 | if (!$user) { 107 | $this->Session->setFlash('Unknown user or wrong password'); 108 | return; 109 | } 110 | 111 | $user = Authsome::get(); 112 | debug($user); 113 | } 114 | } 115 | 116 | And add a app/views/users/login.ctp file like this: 117 | 118 |

pageTitle = 'Login'; ?>

119 | create('User', array('action' => $this->action)); 121 | echo $form->input('email', array('label' => 'Email')); 122 | echo $form->input('password', array('label' => "Password")); 123 | echo $form->submit('Login'); 124 | echo $form->end(); 125 | ?> 126 | 127 | 128 | The array passed into `Authsome::login()` gets passed directly to your `authsomeLogin` function, so you really pass any kind of credentials. You can even come up with your own authentication types by doing `Authsome::login('voodoo_auth', $chickenBones)`. 129 | 130 | ### Cookies 131 | 132 | Any login created by `Authsome::login()` will only last as long as your CakePHP session itself. However, you might want to offer one of those nifty "Remember me for 2 weeks" buttons. `Authsome::persist()` comes to rescue! 133 | 134 | First of all change your login action like this: 135 | 136 | public function login() { 137 | if (empty($this->data)) { 138 | return; 139 | } 140 | 141 | $user = Authsome::login($this->data['User']); 142 | 143 | if (!$user) { 144 | $this->Session->setFlash('Unknown user or wrong password'); 145 | return; 146 | } 147 | 148 | $remember = (!empty($this->data['User']['remember'])); 149 | if ($remember) { 150 | Authsome::persist('2 weeks'); 151 | } 152 | } 153 | 154 | Also add a checkbox like this to your form: 155 | 156 | echo $form->input('remember', array( 157 | 'label' => "Remember me for 2 weeks", 158 | 'type' => "checkbox" 159 | )); 160 | 161 | Authsome itself does not care how you manage your cookie login tokens for auth persistence, but I highly recommend following [Charles' Receipe](http://fishbowl.pastiche.org/2004/01/19/persistent_login_cookie_best_practice/) for this. Charles recommends to create a table that maps `user_id`s and login tokens, here is what I use: 162 | 163 | CREATE TABLE `login_tokens` ( 164 | `id` int(11) NOT NULL auto_increment, 165 | `user_id` int(11) NOT NULL, 166 | `token` char(32) NOT NULL, 167 | `duration` varchar(32) NOT NULL, 168 | `used` tinyint(1) NOT NULL default '0', 169 | `created` datetime NOT NULL, 170 | `expires` datetime NOT NULL, 171 | PRIMARY KEY (`id`) 172 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 173 | 174 | Don't forget to create an empty model in app/models/login_token.php for this: 175 | 176 | class LoginToken extends AppModel{ 177 | } 178 | 179 | Next you'll need to implement `authsomePersist` in your user model, which creates and stores a unique login token when `Authsome::persist()` is called: 180 | 181 | public $hasMany = array('LoginToken'); 182 | 183 | public function authsomePersist($user, $duration) { 184 | $token = md5(uniqid(mt_rand(), true)); 185 | $userId = $user['User']['id']; 186 | 187 | $this->LoginToken->create(array( 188 | 'user_id' => $userId, 189 | 'token' => $token, 190 | 'duration' => $duration, 191 | 'expires' => date('Y-m-d H:i:s', strtotime($duration)), 192 | )); 193 | $this->LoginToken->save(); 194 | 195 | return "${token}:${userId}"; 196 | } 197 | 198 | So far so good. If you are still on track, you should now be able to see new records showing up in your `login_tokens` table if you log in with the remember checkbox checked. 199 | 200 | If so, proceed to the next step and add the `'cookie'` login `$type` to your authsomeLogin function: 201 | 202 | public function authsomeLogin($type, $credentials = array()) { 203 | switch ($type) { 204 | case 'guest': 205 | // You can return any non-null value here, if you don't 206 | // have a guest account, just return an empty array 207 | return array('it' => 'works'); 208 | case 'credentials': 209 | $password = Authsome::hash($credentials['password']); 210 | 211 | // This is the logic for validating the login 212 | $conditions = array( 213 | 'User.email' => $credentials['email'], 214 | 'User.password' => $password, 215 | ); 216 | break; 217 | case 'cookie': 218 | list($token, $userId) = split(':', $credentials['token']); 219 | $duration = $credentials['duration']; 220 | 221 | $loginToken = $this->LoginToken->find('first', array( 222 | 'conditions' => array( 223 | 'user_id' => $userId, 224 | 'token' => $token, 225 | 'duration' => $duration, 226 | 'used' => false, 227 | 'expires <=' => date('Y-m-d H:i:s', strtotime($duration)), 228 | ), 229 | 'contain' => false 230 | )); 231 | 232 | if (!$loginToken) { 233 | return false; 234 | } 235 | 236 | $loginToken['LoginToken']['used'] = true; 237 | $this->LoginToken->save($loginToken); 238 | 239 | $conditions = array( 240 | 'User.id' => $loginToken['LoginToken']['user_id'] 241 | ); 242 | break; 243 | default: 244 | return null; 245 | } 246 | 247 | return $this->find('first', compact('conditions')); 248 | } 249 | 250 | Let's go over this real quick. First we are checking the db for a matching token. If none is found, we return false. If we find a valid token, we invalidate it and set the conditions for finding the user that belongs to the token. 251 | 252 | Pretty simple! You could also do this entirely different. For example you could skip having a `login_tokens` table all together and instead give out tokens that are signed with a secret and a timestamp. However, the drawback with those tokens is that they could be used multiple times which makes cookie theft a more severe problem. 253 | 254 | **Security Advisory:** You should require users to re-authenticate using an alternative login method in case of the following: 255 | 256 | * Changing the user's password 257 | * Changing the user's email address (especially if email-based password recovery is used) 258 | * Any access to the user's address, payment details or financial information 259 | * Any ability to make a purchase 260 | 261 | This can easily be done by tweaking the end of your authsomeLogin function like this: 262 | 263 | $user = $this->find('first', compact('conditions')); 264 | if (!$user) { 265 | return false; 266 | } 267 | $user['User']['loginType'] = $type; 268 | return $user; 269 | 270 | Then deny access to any of the functionality mentioned above like this: 271 | 272 | if (Authsome::get('loginType') === 'cookie') { 273 | Authsome::logout(); 274 | $this->redirect(array( 275 | 'controller' => 'users', 276 | 'action' => 'login', 277 | )) 278 | } 279 | 280 | ## Under the hood 281 | 282 | Authsome builds on a fairly simple logic. The first time you call `Authsome::get()`, it tries to find out who the active user it. This is done as follows: 283 | 284 | 1. Check if Configure::read($this->settings['configureKey']) for a user record 285 | 2. Check $this->Session->read($this->settings['sessionKey']) for a user record 286 | 3. Check $this->Cookie->read($this->settings['cookieKey']) for a token 287 | 288 | If all 3 of those checks do not produce a valid user record, authsome calls the user models `authsomeLogin('guest')` function and takes the record returned from that. If even that fails, authsome will throw an exception and bring your app to a crashing halt. 289 | 290 | ## Options 291 | 292 | ### AuthsomeComponent::initialize($controller, $settings) 293 | 294 | Initializes the AuthsomeComponent with the given settings. This method is called for you when including Authsome in your AppController: 295 | 296 | public $components = array( 297 | 'Authsome.Authsome' => array( 298 | 'model' => 'User' 299 | ) 300 | ); 301 | 302 | Available `$settings` and their defaults: 303 | 304 | 'model' => 'User', 305 | // Those all default to $settings['model'] if not set explicitly 306 | 'configureKey' => null, 307 | 'sessionKey' => null, 308 | 'cookieKey' => null, 309 | 310 | ### AuthsomeComponent::get($field = null) 311 | 312 | Returns the current user record. If `$field` is given, the records sub-field for the main model is extracted. The following two calls are identical: 313 | 314 | $this->Authsome->get('id'); 315 | $this->Authsome->get('User.id'); 316 | 317 | However, you could can also access any associations you may habe returned from your user models `authsomeLogin` function: 318 | 319 | $this->Authsome->get('Role.name'); 320 | 321 | ### AuthsomeComponent::login($type = 'credentials', $credentials = null) 322 | 323 | Passes the given `$type` and `$credentials` to your user model `authsomeLogin` function. Returns false on failure, or the user record on success. 324 | 325 | If you skip the `$type` parameter, the default will be `'credentials'`. This means the following two calls are identical: 326 | 327 | $user = $this->Authsome->login('credentials', $this->data); 328 | $user = $this->Authsome->login($this->data); 329 | 330 | ### AuthsomeComponent::logout() 331 | 332 | Destroys the current authsome session and also deletes any authsome cookies. 333 | 334 | ### AuthsomeComponent::persist($duration = '2 weeks') 335 | 336 | Calls the user models `authsomePersist` function to get a login token and stores it in a cookie. `$duration` must be a relative time string that can be parsed by `strtotime()` and must not be an absolute date or timestamp. 337 | 338 | When performing a cookie login, authsome will automatically renew the login cookie for the given `$duration` again. 339 | 340 | ### AuthsomeComponent::hash($passwords) 341 | 342 | Takes the given `$passwords` and returns the sha1 hash for it using core.php's `'Security.salt'` setting. The following two lines are identical: 343 | 344 | $hashedPw = $this->Authsome->hash('foobar'); 345 | $hashedPw = Security::hash('foobar', 'sha1', true); 346 | 347 | This is a convenience function. It is not used by Authsome internally, you are free to use any password hashing schema you desire. 348 | 349 | ### Static convenience functions 350 | 351 | The following static shortcuts exist for your convenience: 352 | 353 | Authsome::get() 354 | Authsome::login() 355 | Authsome::logout() 356 | Authsome::persist() 357 | Authsome::hash() 358 | 359 | They are identical to calling the AuthsomeComponent in your controller, but allow you to access Authsome anywhere in your app (models, views, etc.). If you suffer from MVC OCD, do not use these functions. 360 | 361 | ## Sponsors 362 | 363 | The initial development of Authsome was paid for by [ThreeLeaf Creative](http://threeleaf.tv/), the makers of a fantastic CakePHP CMS system. 364 | 365 | Authsome is developed by [Debuggable Ltd](http://debuggable.com/). Get in touch if you need help making your next project an authsome one! 366 | 367 | ## License 368 | 369 | Copyright (c) 2009-2012 Felix Geisendörfer 370 | 371 | Permission is hereby granted, free of charge, to any person obtaining a copy 372 | of this software and associated documentation files (the "Software"), to deal 373 | in the Software without restriction, including without limitation the rights 374 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 375 | copies of the Software, and to permit persons to whom the Software is 376 | furnished to do so, subject to the following conditions: 377 | 378 | The above copyright notice and this permission notice shall be included in 379 | all copies or substantial portions of the Software. 380 | 381 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 382 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 383 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 384 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 385 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 386 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 387 | THE SOFTWARE. --------------------------------------------------------------------------------