I am trying to log a user in using CakePHP 3 right after registration, but I have not been successful. This is what I am doing:
function register(){
// ....
if($result = $this->Users->save($user)){
// Retrieves corresponding user that was just saved
$authUser = $this->Users->get($result->id);
// Log user in using Auth
$this->Auth->setUser($authUser);
// Redirect user
$this->redirect('/users/account');
}
}
I guess posting this question opened my eyes to a fix. This is what I did to get it to work... if there is better way, I would be glad to change it...
function register(){
// .... Default CakePHP generated code
if($result = $this->Users->save($user)){
// Retrieve user from DB
$authUser = $this->Users->get($result->id)->toArray();
// Log user in using Auth
$this->Auth->setUser($authUser);
// Redirect user
$this->redirect(['action' => 'account']);
}
}
CakePHP 3.8 × cakephp/authentication update.
Any place you were calling AuthComponent::setUser(), you should now use setIdentity():
// Assume you need to read a user by access token
$user = $this->Users->find('byToken', ['token' => $token])->first();
// Persist the user into configured authenticators.
$this->Authentication->setIdentity($user);
Source: /authentication/1/en/migration-from-the-authcomponent.html#checking-identities
I am trying to use authentication to save profile but GAE Endpoint does not give 401 error even if user is null.
Instead Endpoint is responding with 200 ok and giving default results.
BTW this is part of the Udacity Course.
/**
* Creates or updates a Profile object associated with the given user
* object.
*
* #param user
* A User object injected by the cloud endpoints.
* #param profileForm
* A ProfileForm object sent from the client form.
* #return Profile object just created.
* #throws UnauthorizedException
* when the User object is null.
*/
// Declare this method as a method available externally through Endpoints
#ApiMethod(name = "saveProfile", path = "profile", httpMethod = HttpMethod.POST)
// The request that invokes this method should provide data that
// conforms to the fields defined in ProfileForm
// TODO 1 Pass the ProfileForm parameter
// TODO 2 Pass the User parameter
public Profile saveProfile(final User user) throws UnauthorizedException {
String userId = null;
String mainEmail = null;
String displayName = "Your name will go here";
TeeShirtSize teeShirtSize = TeeShirtSize.NOT_SPECIFIED;
// TODO 2
// If the user is not logged in, throw an UnauthorizedException
if(user== null){
throw new UnauthorizedException("Authorization Required!");
}
// TODO 1
// Set the teeShirtSize to the value sent by the ProfileForm, if sent
// otherwise leave it as the default value
// TODO 1
// Set the displayName to the value sent by the ProfileForm, if sent
// otherwise set it to null
// TODO 2
// Get the userId and mainEmail
// TODO 2
// If the displayName is null, set it to default value based on the user's email
// by calling extractDefaultDisplayNameFromEmail(...)
// Create a new Profile entity from the
// userId, displayName, mainEmail and teeShirtSize
Profile profile = new Profile(userId, displayName, mainEmail, teeShirtSize);
// TODO 3 (In Lesson 3)
// Save the Profile entity in the datastore
// Return the profile
return profile;
}
Unfortunately its a known issue. Go "Star" this issue to get updates on when it's fixed. There are a couple workarounds in the first link, heres a quote from it:
There are two workarounds (1) save the user and read back from the
store, if it refers to a valid account the user id will be populated
(this sucks because you pay the saving / loading / deletion cost for
each API access that is authenticated even if it is tiny, and
obviously some performance cost) and (2) you could use the google+ ID
but that is NOT the same as the user id.
In my CakePHP application I have multi-tenancy which is provided through isolated databases (each tenant has their own, tenant-specific database).
There is also a 'global' database which contains users and tenancy information. The 'tenants' table contains the name of which database the particular tenant occupies. Each user contains a single tenant_id.
Structure:
global_db:
users (contains tenant_id foreign key)
tenants (contains tenant-specific database name, ie: 'isolated_tenant1_db')
isolated_tenant1_db:
orders
jobs
customers
isolated_tenant2_db:
orders
jobs
customers
This system works correctly when the user is logged in via forms / sessions. When they login through /Users/login their tenancy is verified, stored in Session, and database parameters are loaded so their own 'isolated' models can use this dynamic connection.
However, issues arise when the user tries to login via Basic Auth, and directly request the controller function they want to access. For example /Orders/view/1.xml.
In this case, CakePHP attempts to construct the 'Order' Model before the user has been logged in, and therefore before any tenancy information is available - which means it has no idea what database to connect to in order to access orders.
From putting debug() statements around the place I can see that the order in which models / controllers / auth are constructed / executed is as follows (when executing /Orders/view/1.xml):
Model __construct: User
Controller __construct: OrdersController
Model __construct: Permission
Model __construct: Order
function: OrdersController/beforeFilter
AuthComponent __startup
Model __construct: Models related to Order
My problem is that AuthComponent::_startup is executed after Order Model has been constructed. I need to attempt to login the user (and get their database information) before this 'Order' model is constructed.
Questions:
What causes the User model to be constructed before anything else? (I also have the default CakePHP ACL enabled)
Where in the App can I put a call to Auth->login() to attempt login if the request contains BasicAuth headers, that will be executed prior to trying to load tenant-specific models? I assume putting this inside User __construct is a very bad idea.
== UPDATE 01/05/2014 ==
Inserting code samples.
bootstrap.php:
Checks whether the request is being made to api. subdomain:
// Determine whether the request is coming from the api.* subdomain, and if so set the API_REQUEST define to true.
if (preg_match('/^api\./i',$_SERVER['HTTP_HOST']))
{
define('API_REQUEST',true);
// Any links generated (in emails etc), will contain the full base url. If a cron job logged in via the API is generating
// those e-mails, then users will receive links to api.mydomain, instead of just mydomain.
$full_base_url = Router::fullBaseUrl();
$new_full_base_url = preg_replace('/\/\/api\./i', '//', $full_base_url);
Router::fullBaseUrl($new_full_base_url);
CakeLog::write('auth_base_url_debug', 'modified fullbaseurl from ' . $full_base_url . ' to ' . $new_full_base_url);
}
else
{
define('API_REQUEST',false);
}
AppController.php:
public $components = array(
'Security',
'Session',
'Acl',
'Auth' => array(
'className' => 'ExtendedAuth',
'authenticate' => array(
'FormAlias',
),
'authorize' => array(
'Actions' => array('actionPath' => 'controllers')
),
'loginRedirect' => array('controller' => 'Consignments', 'action' => 'index'),
'logoutRedirect' => array('controller' => 'Users', 'action' => 'login'),
),
//'Users.RememberMe',
);
function beforeFilter()
{
// Reroute all requests to API subdomain (ie: api.mydomain) to api_ prefixed actions.
// Also, enable Basic Authentication if the user is accessing via api.*
// If login fails, return 401 error instead of 302 redirect to login page.
if(API_REQUEST == true)
{
$this->params['action'] = 'api_'.$this->params['action']; // prefix the actions with api_
$this->Auth->authenticate = array('BasicAlias'); // Switch to using Basic Authentication
if($this->Auth->login() == false) // Attempt Basic Auth Login
{ // Login failed
CakeLog::write('auth_api', 'Unauthorized API request to: ' . $this->params['action']);
header("HTTP/1.0 401 Unauthorized"); // Force returning an Unauthorized header (401)
exit; // MUST BE CALLED TO PREVENT 302 BEING SENT!
}
}
}
It is important to note that BasicAlias Auth Component is not included in the $components within AppController, but used dynamically if the request is to the api.* subdomain. However, the order in which classes are constructed has no effect whether BasicAlias AuthComponent is included in $components, or used dynamically as shown above.
AppModel:
function __construct($id = false, $table = null, $ds = null)
{
if(($ds == null) && ($this->use_tenant_database == true))
{
// Create a connection to the tenants database and configure model to use this connection.
$Tenant = ClassRegistry::init('Tenant');
$db_name = $Tenant->checkAndCreateTenantDatabaseConnectionForCurrentUser();
if($db_name == false)
{
header("HTTP/1.0 500 Server Error"); // Force returning a Server Error Header (500)
debug('AppModel::$db_name = false, unable to proceed');
CakeLog::write('tenant_error', 'db_name = false, unable to connect.');
exit; // MUST BE CALLED TO PREVENT 302 BEING SENT!
}
// Point model to the tenant database connection:
$this->useDbConfig = $db_name;
}
parent::__construct($id, $table, $ds);
}
And then within any models which use a specific tenant database:
class Order extends AppModel
{
var $use_tenant_database = true;
...
}
Tenant.php:
/**
* Check whether a connection to the current users tenant database has already been created and if so, return its name.
* Otherwise, create the connection and return its name.
*
* #return boolean|Ambigous <mixed, multitype:, NULL, array, boolean>
*/
public function checkAndCreateTenantDatabaseConnectionForCurrentUser()
{
// Check whether we have the tenants database connection information available in the Configure variable:
if(Configure::check('Tenant.db_name') == true)
{ // the db_config is available in configure, use it!
$db_name = Configure::read('Tenant.db_name');
}
else
{ // The tenants db_name has not been set in the configure variable, we need to create a database connection and then
// set the configure variable.
$tenant_id = $this->getCurrentUserTenantId();
if($tenant_id == null)
{ // Unable to resolve the tenant_id, instead, connect to the default database.
debug('TRIED TO CONSTRUCT MODEL WITHOUT KNOWING TENANT DATABASE!!');
exit;
}
$db_name = $this->TenantDatabase->createConnection($tenant_id);
if($db_name == false)
{ // The database connection could not be created.
CakeLog::write('tenant_error', 'unable to find the database name for tenant_id: ' . $tenant_id);
return false;
}
Configure::write('Tenant.db_name', $db_name);
}
return $db_name;
}
So, if the user requests a URL for example:
http://api.mydomain.com/Orders/getAllPendingOrders
Where they have supplied BASIC auth credentials along with the request, then what happens is that classes are constructed / executed in the following order:
Model __construct: User
Controller __construct: OrdersController
Model __construct: Permission
Model __construct: Order
Model __construct: Tenant
Model __construct: TenantDatabase
function: OrdersController/beforeFilter
AuthComponent __startup --> This then performs the login.
Model __construct: other models.
The problem is: Order.php is being constructed the user has been logged in, which means when the code in AppModel.php is executed:
$db_name = $Tenant->checkAndCreateTenantDatabaseConnectionForCurrentUser();
It is unable to determine the users current tenancy.
I need to find out a workaround for this, either by somehow performing the login BEFORE Order.php is constructed, or hacking it so that if you attempt to construct a model which has $use_tenant_database = true, and the user is not logged in, then BasicAuth is performed at this point to try and login the user.. however this feels wrong to me.
You might want to have a look at Authorization (who’s allowed to access what) portion in Cake's documentation. Specifically look at the isAuthorized function and how it works.
You might need something like this in your Orders controller:
// app/Controller/OrdersController.php
public function isAuthorized($user) {
// All registered users can add posts
if ($this->action === 'add') {
return true;
}
// The owner of an order can edit and delete it
if (in_array($this->action, array('edit', 'delete'))) {
$orderId = (int) $this->request->params['pass'][0];
if ($this->Order->isOwnedBy($orderId, $user['id'])) {
return true;
}
}
return parent::isAuthorized($user);
}
Implement your logic in before filter Request Life-cycle callback in the app controller.
Controller::beforeFilter() :
This function is executed before every action in the controller. It’s a handy place to check for an active session or inspect user permissions.
http://book.cakephp.org/2.0/en/controllers.html
It turns out these models were being constructed by the 'Search.Prg' plugin, a CakeDC plugin for handling search / filtering of results. The initialize() function within the component was being executed and causing the model to be constructed prior to the user being logged in.
The way in which this was solved was to move the Basic Auth check / login process from AppController beforeFilter to ExtendedAuthComponent (my own custom authenciation component) initialize function.
The end code was this:
ExtendedAuthComponent.php
public function initialize(Controller $controller)
{
parent::initialize($controller); // Call parent initialization first, this sets up request and response variables.
$this->controller = $controller;
// Reroute all requests to API subdomain (ie: api.rms.roving.net.au) to api_ prefixed actions.
// Also, enable Basic Authentication if the user is accessing via api.*
// If login fails, return 401 error instead of 302 redirect to login page.
if(API_REQUEST == true)
{
$controller->params['action'] = 'api_'.$controller->params['action']; // prefix the actions with api_
if($this->loggedIn() == false) // Attempt Basic Auth Login
{ // Login failed
$this->authenticate = array('BasicAlias'); // Switch to using Basic Authentication
if($this->login() == false)
{
CakeLog::write('auth_api', 'Unauthorized API request to: ' . $this->params['action']);
header("HTTP/1.0 401 Unauthorized"); // Force returning an Unauthorized header (401)
exit; // MUST BE CALLED TO PREVENT 302 BEING SENT!
}
}
}
}
This causes the user to be logged in via Basic Auth before the Search.Prg components initialize() function is run, which means the users tenancy is determined before the model(s) are constructed, solving the problem.
Is there a way to bind an auth error message to the scope condition of the auth component?
For instance say I have:
'Auth' => array('authenticate' => array('Blowfish' => array('scope' => array('User.activated' => 1))));
I would like set an error if the scope condition fails. I need to be able to differentiate that from the error that is presented if the user/pass is incorrect.
Is this possible?
Right, I've had a root around CakePHP's source code and I've come to the conclusion that it doesn't do anything fancy when using scoped conditions, it simply appends them as additional query conditions. It will either find the user which matches the username/password combination and any scope conditions, or it doesn't.
One possible solution would be to manually log users in and check for the activated field like so:
public function login()
{
if ($this->request->is('post')) {
if ($this->Auth->login($this->data['User'])) {
// check activated field
if ($this->Auth->user('activated') == 1) {
// user is activated
$this->redirect(...);
} else {
// user is not activated
// log the user out
$this->Auth->logout();
// redirect to an error page for inactive users
$this->redirect(..);
}
}
// redirect to an error page for wrong username/password
$this->redirect(..);
}
}
I should clarify that you should not specify scope conditions in configuring the authentication component.
I hope this helps!
======================================== EDIT ====================================
Per charles suggestion, I accomplished the Offline/Online feature using the following code, based on Charles code:
<?php
Class AppController extends Controller{
// prevents unauthorized access
public $components = array('Auth');
// the name of the model storing site_offline boolean
public $uses = array('Configuration');
// callback invoked before every controller action
public function beforeFilter() {
// returns your site_offline status assuming 0 is offline
if ($this->Configuration->get_site_status() == 1) {
$this->Auth->allow('*');
}else {
if(($this->Configuration->get_site_status() == 0) and (!$this->Auth->user() == null)){
// I set it up like this for now to allow access to any authenticated user,
//but later will change it to only allow admins access thru a login form
$this->Auth->allow('*');
}else{
//If site is offline and user is not authenticated, sent them to
// the a screen using the OFFLINE layout and provide a screen for login.
$this->layout = 'offline';
$this->setFlash('Maintenance Mode. Check back shortly.');
$this->Auth->deny('*');
}
}
}
}
?>
Then I used jQuery to hide my login form. An admin clicks on the message to show the login form. This is an attempt to prevent any login tryouts.
============================ END EDIT ==========================================
I would like to know what is the best way to create a "site offline/online" feature in CakePHP. Basically, I would like to allow an administrator to turn off access to the site to everyone registered or not. The offline page should have a login access thru which only admins can login.
The idea I have is to create some kind of dashboard controller, where as soon as the administrator is logged in he/she will be redirected to this dashboard from where he can access the other controller actions (admin_edit, etc). This dashboard and all admin actions (admin_delete, etc) should use the admin layout.
Is this a good approach? For the offline/online feature should I create a settings table with a site_offline field that can be turned on or off? Where in app_controller and what code should I use to check for it before allowing or not access to the site?
Thanks a lot for your help,
first add a config in the core.config
/*
* This is the site maintenance
* The built in defaults are:
*
* - '1' - Site works
* - '0' - site down for maintenance.
*/
Configure::write('Site.status', 1);
in the AppController you'll check it in the beforeRender function
if (Configure::read('Site.status') == 0) {
$this->layout = 'maintenance';
$this->set('title_for_layout', __('Site_down_for_maintenance_title'));
} else {
// do something
}
i'm here load a separate layout form the maintenance to let me add whatever layout i want
If you were going to save site_offline boolean value in a database table you should be able to easily do this with a callback in AppController and the Auth component.
<?php
AppController extends Object {
// prevents unauthorized access
public $components = array('Auth');
// the name of the model storing site_offline boolean
public $uses = array('NameOfModel');
// callback invoked before every controller action
public function beforeFilter() {
// returns your site_offline status assuming 0 is offline
if ($this->NameOfModel->get_status() === 0) {
$this->Auth->deny('*');
} else {
$this->Auth->allow('*');
}
}
}
I've always liked the idea of the DashboardsController for admin functions. That's actually the exact name of the class I use and the same general idea.