Implemented stateless login for Auth #1169

Merged
merged 1 commit into from Mar 13, 2013
View
11 lib/Cake/Controller/Component/Auth/BaseAuthenticate.php
@@ -155,4 +155,15 @@ public function getUser(CakeRequest $request) {
return false;
}
+/**
+ * Handle unauthenticated access attempt.
@lorenzo
CakePHP member
lorenzo added a note Mar 6, 2013

Can you somehow specify that only the last one in the chain is called?

@ADmad
CakePHP member
ADmad added a note Mar 6, 2013

Yup I will review and improve the docblocks wherever needed and add more tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ *
+ * @param CakeRequest $request A request object.
+ * @param CakeResponse $response A response object.
+ * @return mixed Either true to indicate the unauthenticated request has been
+ * dealt with and no more action is required by AuthComponent or void (default).
+ */
+ public function unauthenticated(CakeRequest $request, CakeResponse $response) {
+ }
+
}
View
28 lib/Cake/Controller/Component/Auth/BasicAuthenticate.php
@@ -82,23 +82,15 @@ public function __construct(ComponentCollection $collection, $settings) {
}
/**
- * Authenticate a user using basic HTTP auth. Will use the configured User model and attempt a
- * login using basic HTTP auth.
+ * Authenticate a user using HTTP auth. Will use the configured User model and attempt a
+ * login using HTTP auth.
*
* @param CakeRequest $request The request to authenticate with.
* @param CakeResponse $response The response to add headers to.
* @return mixed Either false on failure, or an array of user data on success.
*/
public function authenticate(CakeRequest $request, CakeResponse $response) {
- $result = $this->getUser($request);
-
- if (empty($result)) {
- $response->header($this->loginHeaders());
- $response->statusCode(401);
- $response->send();
- return false;
- }
- return $result;
+ return $this->getUser($request);
}
/**
@@ -118,6 +110,20 @@ public function getUser(CakeRequest $request) {
}
/**
+ * Handles an unauthenticated access attempt by sending appropriate login headers
+ *
+ * @param CakeRequest $request A request object.
+ * @param CakeResponse $response A response object.
+ * @return boolean True
+ */
+ public function unauthenticated(CakeRequest $request, CakeResponse $response) {
+ $response->header($this->loginHeaders());
+ $response->statusCode(401);
+ $response->send();
+ return true;
+ }
+
+/**
* Generate the login headers
*
* @return string Headers for logging in.
View
27 lib/Cake/Controller/Component/Auth/DigestAuthenticate.php
@@ -14,7 +14,7 @@
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
-App::uses('BaseAuthenticate', 'Controller/Component/Auth');
+App::uses('BasicAuthenticate', 'Controller/Component/Auth');
/**
* Digest Authentication adapter for AuthComponent.
@@ -55,7 +55,7 @@
* @package Cake.Controller.Component.Auth
* @since 2.0
*/
-class DigestAuthenticate extends BaseAuthenticate {
+class DigestAuthenticate extends BasicAuthenticate {
/**
* Settings for this object.
@@ -97,9 +97,6 @@ class DigestAuthenticate extends BaseAuthenticate {
*/
public function __construct(ComponentCollection $collection, $settings) {
parent::__construct($collection, $settings);
- if (empty($this->settings['realm'])) {
- $this->settings['realm'] = env('SERVER_NAME');
- }
@markstory
CakePHP member

Are you sure we don't need the realm anymore? I thought it was required to properly generate digest hashes.

@ADmad
CakePHP member
ADmad added a note Mar 5, 2013

It is required but now DigestAuthenticate extends BasicAuthenticate and its constructor already sets the realm.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
if (empty($this->settings['nonce'])) {
$this->settings['nonce'] = uniqid('');
}
@@ -109,26 +106,6 @@ public function __construct(ComponentCollection $collection, $settings) {
}
/**
- * Authenticate a user using Digest HTTP auth. Will use the configured User model and attempt a
- * login using Digest HTTP auth.
- *
- * @param CakeRequest $request The request to authenticate with.
- * @param CakeResponse $response The response to add headers to.
- * @return mixed Either false on failure, or an array of user data on success.
- */
- public function authenticate(CakeRequest $request, CakeResponse $response) {
- $user = $this->getUser($request);
-
- if (empty($user)) {
- $response->header($this->loginHeaders());
- $response->statusCode(401);
- $response->send();
- return false;
- }
- return $user;
- }
-
-/**
* Get a user based on information in the request. Used by cookie-less auth for stateless clients.
*
* @param CakeRequest $request Request object.
View
98 lib/Cake/Controller/Component/AuthComponent.php
@@ -157,8 +157,9 @@ class AuthComponent extends Component {
);
/**
- * The session key name where the record of the current user is stored. If
- * unspecified, it will be "Auth.User".
+ * The session key name where the record of the current user is stored. Default
+ * key is "Auth.User". If you are using only stateless authenticators set this
+ * to false to ensure session is not started.
*
* @var string
*/
@@ -188,7 +189,7 @@ class AuthComponent extends Component {
* Normally, if a user is redirected to the $loginAction page, the location they
* were redirected from will be stored in the session so that they can be
* redirected back after a successful login. If this session value is not
- * set, the user will be redirected to the page specified in $loginRedirect.
+ * set, redirectUrl() method will return the url specified in $loginRedirect.
*
* @var mixed
* @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#AuthComponent::$loginRedirect
@@ -312,44 +313,43 @@ public function startup(Controller $controller) {
/**
* Checks whether current action is accessible without authentication.
- * If current action is login action referrer url is saved in session which is
- * later accessible using AuthComponent::redirectUrl().
*
* @param Controller $controller A reference to the instantiating controller object
* @return boolean True if action is accessible without authentication else false
*/
protected function _isAllowed(Controller $controller) {
$action = strtolower($controller->request->params['action']);
-
- $url = '';
- if (isset($controller->request->url)) {
- $url = $controller->request->url;
- }
- $url = Router::normalize($url);
- $loginAction = Router::normalize($this->loginAction);
-
- if ($loginAction != $url && in_array($action, array_map('strtolower', $this->allowedActions))) {
- return true;
- }
-
- if ($loginAction == $url) {
- if (empty($controller->request->data)) {
- if (!$this->Session->check('Auth.redirect') && !$this->loginRedirect && env('HTTP_REFERER')) {
- $this->Session->write('Auth.redirect', $controller->referer(null, true));
- }
- }
+ if (in_array($action, array_map('strtolower', $this->allowedActions))) {
return true;
}
return false;
}
/**
- * Handle unauthenticated access attempt.
+ * Handles unauthenticated access attempt. First the `unathenticated()` method
+ * of the last authenticator in the chain will be called. The authenticator can
+ * handle sending response or redirection as appropriate and return `true` to
+ * indicate no furthur action is necessary. If authenticator returns null this
+ * method redirects user to login action. If it's an ajax request and
+ * $ajaxLogin is specified that element is rendered else a 403 http status code
+ * is returned.
*
- * @param Controller $controller A reference to the controller object
- * @return boolean Returns false
+ * @param Controller $controller A reference to the controller object.
+ * @return boolean True if current action is login action else false.
*/
protected function _unauthenticated(Controller $controller) {
+ if (empty($this->_authenticateObjects)) {
+ $this->constructAuthenticate();
+ }
+ $auth = $this->_authenticateObjects[count($this->_authenticateObjects) - 1];
+ if ($auth->unauthenticated($this->request, $this->response)) {
+ return false;
+ }
+
+ if ($this->_isLoginAction($controller)) {
+ return true;
+ }
+
if (!$controller->request->is('ajax')) {
$this->flash($this->authError);
$this->Session->write('Auth.redirect', $controller->request->here());
@@ -367,11 +367,39 @@ protected function _unauthenticated(Controller $controller) {
}
/**
+ * Normalizes $loginAction and checks if current request url is same as login
+ * action. If current url is same as login action, referrer url is saved in session
+ * which is later accessible using redirectUrl().
+ *
+ * @param Controller $controller A reference to the controller object.
+ * @return boolean True if current action is login action else false.
+ */
+ protected function _isLoginAction(Controller $controller) {
+ $url = '';
+ if (isset($controller->request->url)) {
+ $url = $controller->request->url;
+ }
+ $url = Router::normalize($url);
+ $loginAction = Router::normalize($this->loginAction);
+
+ if ($loginAction == $url) {
+ if (empty($controller->request->data)) {
+ if (!$this->Session->check('Auth.redirect') && !$this->loginRedirect && env('HTTP_REFERER')) {
+ $this->Session->write('Auth.redirect', $controller->referer(null, true));
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+/**
* Handle unauthorized access attempt
*
* @param Controller $controller A reference to the controller object
* @return boolean Returns false
* @throws ForbiddenException
+ * @see AuthComponent::$unauthorizedRedirect
*/
protected function _unauthorized(Controller $controller) {
if ($this->unauthorizedRedirect === false) {
@@ -395,7 +423,7 @@ protected function _unauthorized(Controller $controller) {
/**
* Attempts to introspect the correct values for object properties.
*
- * @return boolean
+ * @return boolean True
*/
protected function _setDefaults() {
$defaults = array(
@@ -619,13 +647,12 @@ public function logout() {
* @link http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#accessing-the-logged-in-user
*/
public static function user($key = null) {
- if (empty(self::$_user) && !CakeSession::check(self::$sessionKey)) {
- return null;
- }
if (!empty(self::$_user)) {
$user = self::$_user;
- } else {
+ } elseif (self::$sessionKey && CakeSession::check(self::$sessionKey)) {
$user = CakeSession::read(self::$sessionKey);
+ } else {
+ return null;
}
if ($key === null) {
return $user;
@@ -640,10 +667,6 @@ public static function user($key = null) {
* @return boolean true if a user can be found, false if one cannot.
*/
protected function _getUser() {
- $user = $this->user();
- if ($user) {
- return true;
- }
if (empty($this->_authenticateObjects)) {
$this->constructAuthenticate();
}
@@ -654,6 +677,11 @@ protected function _getUser() {
return true;
}
}
+
+ $user = $this->user();
+ if ($user) {
+ return true;
+ }
return false;
}
View
23 lib/Cake/Test/Case/Controller/Component/Auth/BasicAuthenticateTest.php
@@ -80,11 +80,10 @@ public function testConstructor() {
public function testAuthenticateNoData() {
$request = new CakeRequest('posts/index', false);
- $this->response->expects($this->once())
- ->method('header')
- ->with('WWW-Authenticate: Basic realm="localhost"');
+ $this->response->expects($this->never())
+ ->method('header');
- $this->assertFalse($this->auth->authenticate($request, $this->response));
+ $this->assertFalse($this->auth->getUser($request));
}
/**
@@ -96,10 +95,6 @@ public function testAuthenticateNoUsername() {
$request = new CakeRequest('posts/index', false);
$_SERVER['PHP_AUTH_PW'] = 'foobar';
- $this->response->expects($this->once())
- ->method('header')
- ->with('WWW-Authenticate: Basic realm="localhost"');
-
$this->assertFalse($this->auth->authenticate($request, $this->response));
}
@@ -113,10 +108,6 @@ public function testAuthenticateNoPassword() {
$_SERVER['PHP_AUTH_USER'] = 'mariano';
$_SERVER['PHP_AUTH_PW'] = null;
- $this->response->expects($this->once())
- ->method('header')
- ->with('WWW-Authenticate: Basic realm="localhost"');
@markstory
CakePHP member

Why don't these get set anymore?

@ADmad
CakePHP member
ADmad added a note Mar 5, 2013

They are no longer set when calling authenticate() but rather when unathenticated() is called. AuthComponent::_getUser() is now called for all requests which in turn calls getUser() of all athenticators. If none of the authenticators return a user record then AuthComponent::_unauthenticate() calls unauthenticated() which is what would trigger the headers if basic/digest is the last authenticator in the list.

@ADmad
CakePHP member
ADmad added a note Mar 5, 2013

BasicAuthenticate::authenticate() is redundant now. Because if required credentials were passed BasicAuthenticate::getUser() would already return the user and if not BasicAuthenticate::unauthenticated() would send the headers asking for credentials.

@markstory
CakePHP member

Ah ok. Thanks for explaining it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
-
$this->assertFalse($this->auth->authenticate($request, $this->response));
}
@@ -132,6 +123,8 @@ public function testAuthenticateInjection() {
$_SERVER['PHP_AUTH_USER'] = '> 1';
$_SERVER['PHP_AUTH_PW'] = "' OR 1 = 1";
+ $this->assertFalse($this->auth->getUser($request));
+
$this->assertFalse($this->auth->authenticate($request, $this->response));
}
@@ -151,8 +144,8 @@ public function testAuthenticateChallenge() {
$this->response->expects($this->at(1))
->method('send');
- $result = $this->auth->authenticate($request, $this->response);
- $this->assertFalse($result);
+ $result = $this->auth->unauthenticated($request, $this->response);
+ $this->assertTrue($result);
}
/**
@@ -201,7 +194,7 @@ public function testAuthenticateFailReChallenge() {
$this->response->expects($this->at(2))
->method('send');
- $this->assertFalse($this->auth->authenticate($request, $this->response));
+ $this->assertTrue($this->auth->unauthenticated($request, $this->response));
}
}
View
15 lib/Cake/Test/Case/Controller/Component/Auth/DigestAuthenticateTest.php
@@ -94,11 +94,10 @@ public function testConstructor() {
public function testAuthenticateNoData() {
$request = new CakeRequest('posts/index', false);
- $this->response->expects($this->once())
- ->method('header')
- ->with('WWW-Authenticate: Digest realm="localhost",qop="auth",nonce="123",opaque="123abc"');
+ $this->response->expects($this->never())
+ ->method('header');
- $this->assertFalse($this->auth->authenticate($request, $this->response));
+ $this->assertFalse($this->auth->getUser($request, $this->response));
}
/**
@@ -133,7 +132,7 @@ public function testAuthenticateWrongUsername() {
$this->response->expects($this->at(2))
->method('send');
- $this->assertFalse($this->auth->authenticate($request, $this->response));
+ $this->assertTrue($this->auth->unauthenticated($request, $this->response));
}
/**
@@ -156,8 +155,8 @@ public function testAuthenticateChallenge() {
$this->response->expects($this->at(2))
->method('send');
- $result = $this->auth->authenticate($request, $this->response);
- $this->assertFalse($result);
+ $result = $this->auth->unauthenticated($request, $this->response);
+ $this->assertTrue($result);
}
/**
@@ -224,7 +223,7 @@ public function testAuthenticateFailReChallenge() {
$this->response->expects($this->at(2))
->method('send');
- $this->assertFalse($this->auth->authenticate($request, $this->response));
+ $this->assertTrue($this->auth->unauthenticated($request, $this->response));
}
/**
View
74 lib/Cake/Test/Case/Controller/Component/AuthComponentTest.php
@@ -1348,4 +1348,78 @@ public function testUser() {
$result = $this->Auth->user('is_admin');
$this->assertFalse($result);
}
+
+/**
+ * testStatelessAuthNoRedirect method
+ *
+ * @return void
+ */
+ public function testStatelessAuthNoRedirect() {
+ if (CakeSession::id()) {
+ session_destroy();
+ CakeSession::$id = null;
+ }
+ $_SESSION = null;
+
+ AuthComponent::$sessionKey = false;
+ $this->Auth->authenticate = array('Basic');
+ $this->Controller->request['action'] = 'admin_add';
+
+ $this->Auth->response->expects($this->once())
+ ->method('statusCode')
+ ->with(401);
+
+ $this->Auth->response->expects($this->once())
+ ->method('send');
+
+ $result = $this->Auth->startup($this->Controller);
+ $this->assertFalse($result);
+
+ $this->assertNull($this->Controller->testUrl);
+ $this->assertNull(CakeSession::id());
+ }
+
+/**
+ * testStatelessAuthNoSessionStart method
+ *
+ * @return void
+ */
+ public function testStatelessAuthNoSessionStart() {
+ if (CakeSession::id()) {
+ session_destroy();
+ CakeSession::$id = null;
+ }
+ $_SESSION = null;
+
+ $_SERVER['PHP_AUTH_USER'] = 'mariano';
+ $_SERVER['PHP_AUTH_PW'] = 'cake';
+
+ $this->Auth->authenticate = array(
+ 'Basic' => array('userModel' => 'AuthUser')
+ );
+ $this->Controller->request['action'] = 'admin_add';
+
+ $result = $this->Auth->startup($this->Controller);
+ $this->assertTrue($result);
+
+ $this->assertNull(CakeSession::id());
+ }
+
+/**
+ * testStatelessAuthRedirect method
+ *
+ * @return void
+ */
+ public function testStatelessFollowedByStatefulAuth() {
+ $this->Auth->authenticate = array('Basic', 'Form');
+ $this->Controller->request['action'] = 'admin_add';
+
+ $this->Auth->response->expects($this->never())->method('statusCode');
+ $this->Auth->response->expects($this->never())->method('send');
+
+ $result = $this->Auth->startup($this->Controller);
+ $this->assertFalse($result);
+
+ $this->assertEquals('/users/login', $this->Controller->testUrl);
+ }
}