zen of coding

Getting Auth and Admin Routing to play nicely together

Here I’m going to show how to use CakePHP’s built-in Auth component and Admin Routing to easily build a protected, administrative area for your application.

I guess this post is somewhat on an intermediate level and I hope you know a little about Auth (or maybe even here) and Admin/Prefix Routing already…

Generally speaking, Auth is often used to allow one to create a web application with multiple user profiles (or accounts… registration, etc.).

However, sometimes, you could have a site, where no user registration is required, but a site owner (admin) needs to have their own control panel (or administrative area) where she can manage some aspects of the site.
This is not to be misunderstood that the two are mutually exclusive, but in this post I’m just going to focus on how to, as always, very quickly (thanks to cake ;)) to build an admin area.

Let’s imagine some web site where there is an Administrator who can add articles to her blog, browse a DB of contacts who’ve submitted questions or comments via some form on the site and do some other, generic, administrative duties.

Even though we won’t have any registered users on the site, to make life just a little bit easier we’ll still create users table, User Model, and Users Controller to handle our admin’s login, logout and homepage. (By default Auth works nicely with the convention of User/users model, controller, table, etc.)

Keeping in mind our Prefix/Admin routing… which we’ve already enabled in core.php Configure::write(‘Routing.admin’, ‘admin’);… we create a Users controller that looks something like this:

<?php
    class UsersController extends AppController {

        var $name ='Users';

        function admin_index() {
            // home page code... could be something as simple as 'Welcome Home' view.
        }

        function admin_login() {
            if($this->Auth->user()) {
                $this->redirect(array('controller'=>'users', 'action'=>'admin_index'));
            }
        }

        function admin_logout() {
           $this->Session->del('Auth.User');
           $this->redirect(array('controller'=>'pages', 'action'=>'display', 'home', 'admin'=>false));
        }
    }
?>

Looks rather simple…
Some might say where’s your beforeFilter() and Auth component? … we’ll handle that in the app_controller.php (just a little later).

Let’s take a look, first, at admin_login().
It could be an empty action, really, but I prefer to override default login() mechanism and basically say, that if the admin has been authenticated, aka $this->Auth->user() returns true, let’s direct her to the home page.

Now what about admin_logout()?
The most common way to build a logout() action is to do something like this:

function logout() {
   $this->redirect($this->Auth->logout());
}

Doing something like this is completely fine, but for my specific need and idea (plus an example of an alternative option to think about) I would rather redirect the admin back to the main page of the site.

Since we cannot rely on the automagic of $this->Auth->logout(), we need to “properly” log out the admin. In other words, we make sure that the session with the default key of ‘Auth.User’ is gone… hence: $this->Session->del(‘Auth.User’);. Then we simply redirect to the default homepage…
Quickly note the ‘admin’=>false in the redirect code… that simply gets rid of the /admin/ prefix in the URL (again one of the lovely cake’s prefix routing tricks).

Alright, so we’ve got the admin login, homepage and logout handled nicely…

Let’s do an amazingly simple Auth component setup in App Controller (app_controller.php)

<?php
class AppController extends Controller {

    var $components = array('Auth');

    function beforeFilter () {
        $this->Auth->autoRedirect = false;
    }

    function beforeRender () {
        $admin = Configure::read('Routing.admin');

        if (isset($this->params[$admin]) && $this->params[$admin]) {
            $this->layout = 'admin';
        }
    }
?>

Well, let’s see… we’ve included our Auth component and added a simple beforeFilter().
As mentioned before, I really didn’t want the admin to be redirected anywhere, except the actual administrator homepage upon login. Therefore, I override the default Auth behavior to redirect to the previously accessed page (aka $this->referer()) by using $this->Auth->autoRedirect = false;

Why?
Imagine a blog… and an admin decided to login from a link in the footer of the site from some random blog article. Obviously the admin wants to get to her admin area and not just get redirected back to the article. (Of course this is highly dependent on the application, but in my case that was a requirement and this apporach worked out quite nicely).

To be honest I’m not quite sure about the beforeRender() method, but it did the trick to switch the layout from ‘default’ to ‘admin’ based on the action name (again, as you see, ‘admin_’ is a built-in part of cake’s Prefix/Admin routing).
(Thanks for a nice solution provided by Mark Story for the layout switching).

Well, that’s all lovely, but what’s next?
As a matter of fact we’re pretty much done. Our app is ready to easily setup any administrative functions for any of your controllers.

Alright, let’s say we’ve got a Posts controller and we need to make one of our actions (let’s imagine /admin/posts/add) for administrator only…

All that needs to be done is the following:

<?php
   class PostsController extends AppController {

       var $name ='Posts';

       function beforeFilter() {
          parent::beforeFilter();

          $this->Auth->allow('index', 'view');
        }

       function index() {
         //something here
       }

       function view() {
       //something here too
       }

        function admin_add() {
            if(!empty($this->data)) {
                $this->Post->save($this->data);
            }
        }
    }
?>

First, we inherit our App Controller’s settings by using parent::beforeFilter();, then we ensure that regular/non-registered users can see (access) index() and view() actions.

Of course, our admin_add() action is nicely protected and would only be accessed if the admin had authenticated.

Now what do you do if you needed an administrative action in the Contacts Controller, that allows an admin to ‘browse’ contacts?…

Yep, you simply add admin_browse() to your Contacts Controller. Of course, as in the example above, any actions of this controller that need to be seen by regular users would need to be allowed in beforeFilter() as we just did.

(I guess there is no need to mention that all admin actions should be added to the admin layout or element as links for convenient navigation ;))

P.S. How do you add an admin account to the users table with a properly hashed password?
The easiest way, unless you wish to build an add_admin() action is to temporarily enable scaffolding, which will let you easily add new users (admins).

  • in beforeRender() you could also use:

    $admin = Configure::read(‘Routing.admin’);
    if (isset($this->params[$admin]) && $this->params[$admin]) {
    $this->layout = ‘admin’;
    }

  • Phally

    Another great everyday example! Good to see it published. It really is something easy, when you know how to use it. If you don’t it will take you hours of routing and redirecting.
    Good work!

  • @Phally and Mark Story

    Phally, as mentioned thanks for reminding about that important, little detail ;) And you are right it took me hours staring at random code and quite a few tries, before I got it all narrowed down… so, like snowboarding, it’s very easy once you know how to do it, but you’ll get your neck twisted by the time you get there :)

    Thanks, Mark… ctrl+c/ctrl+v… here I come! :)

  • Javier

    You could avoid having to allow actions in every controller by adding this to your AppController’s beforeFilter():

    $admin = Configure::read(‘Routing.admin’);
    if (isset($this->params[$admin]) && $this->params[$admin]){
    $this->layout = ‘admin’;
    }
    else {
    $this->Auth->allow();
    }

    I’ve used the Auth + Admin Routing system in many projects and I find it very useful. The only thing I feel that is not so good is that I can’t find a way to keep it DRY. I mean, every action has to start with “admin_”, and every view file name must start with “admin_” as well. Suppose I want to change the prefix to “administration”. I’ve got to change things everywhere!

  • Pingback: CakePHP : signets remarquables du 08/01/2009 au 13/01/2009 | Cherry on the...()

  • Your timing could _not_ have been better. I started trying to use the Auth component yesterday and was definitely, as you put it, getting my neck twisted. All I was looking for is a simple boolean-esque login and you nailed it. I’ll be digging deeper into some of what you’re doing here, but this answered at least one lingering question immediately.

    Thanks.

  • @Rob Wilkerson

    Well, I’m glad it helped out.
    You might also want to check some of my earlier tutorials on Auth, which cover some of the gotchas, tricks and tips.

  • @Javier

    Very good idea (and thanks for sharing), but in my particular case I had to be a little more explicitly strict due to some other aspects of the application.

  • Pingback: links for 2009-01-13 « Richard@Home()

  • To add to Mark Story’s comment, I usually require a bit more logic for admin only and non-admin only sections, so I do the following:

    function beforeFilter()
    {
    if ($this->isAdmin()) {
    $this->beforeAdminFilter();
    } else {
    $this->beforeFrontFilter();
    }
    }

    function isAdmin()
    {
    $admin = Configure::read(’Routing.admin’);
    return (isset($this->params[$admin]) && $this->params[$admin]) ? true : false;
    }

    I’ll do the same for beforeRender. Just an easy way for me to separate the admin logic from the frontend logic.

  • @Steve

    Sounds very good. Thanks for sharing it.

  • redfox26

    hi

    i start with cakeview and i search to do an admin section…

    does somebody can say me what i suppose to put in admin_index function?

    direct html?

    i don’t really understand the “Welcome Home’ view. ”

    thanks

  • @redfox26

    Imagine that you are an admin… what would you like to see on that page?
    A summary of some stats, a naked girl, and maybe a joke of the day…

    Bottom line, anything that you feel would be worthwhile for the admin is good to go ;)

  • redfox26

    so we put directely html code there?

  • @redfox26: It would work the same way your frontend home page looks like. Some people make it just html with a welcome message, others may want to put in different kind of “widgets” pulling data from different models as a portal to other sections.

    Another thing you can do is simply redirect to particular action if you don’t want a default home page.

  • @Steve

    Thank you for explaining it a little better than I did :)

    @redfox26

    Yes, or follow Steve’s suggestion on how to handle it otherwise.

  • Chris

    Awesome article. I’m building my first real cake application, and this is perfect.

    However, I’m running into a problem with your code.

    Your users/admin_login method is pretty cool, but I think this line
    $this->redirect(array(‘controller’=>’users’, ‘action’=>’admin_index’));

    needs to be rewritten to
    $this->redirect(array(‘controller’=>’users’, ‘action’=>’index’, ‘admin’=>TRUE));

    Again, I’m pretty new to cake, so I could be wrong, but this seems more correct.
    Thanks.

  • @Chris

    It works either way, but I do have to say that the way you’ve done it is a more proper approach. Thanks for sharing.

  • luke aka boobyWomack

    nice page teknoid. I found this quite illuminating.

    I am doing a site where I want to use admin routing and I also have regular users, as you allude to in your post. This is what I have done – not sure if it is a good way or not, but seems to work for me, in allowing users to get access to non-admin pages, guests to access all guest-accessible-pages and admins to the admin_ pages.

    function beforeFilter(){
    $this->Auth->loginAction = ‘/users/login’ ;
    $this->Auth->allowedActions = array(‘display’);

    //in case we have an admin page requested:
    if ( isset($this->params[Configure::read(‘Routing.admin’)]) && $this->params[Configure::read(‘Routing.admin’)]) {
    if ($this->isAdmin($this->Auth->user())) {
    //$this->layout = ‘admin’;
    var_dump(‘admin layout’);
    }
    else {
    $this->Auth->deny();
    }
    }

    }

    function isAdmin($authuser){
    // var_dump(Set::extract($authuser, ‘User.group_id’)) ;
    return (Set::extract($authuser, ‘User.group_id’) === “1”) ? true : false ;
    }

  • luke aka boobyWomack

    oh, i put it in my app_controller. I only put it in once too ;) unlike here!

  • luke aka boobyWomack

    hmm, it doesnt work for the users who are logged in but not admins – it still shows the page but not the layout. I think the Auth->deny () gets a over-ride later. I shall adjust and return shame-faced to my drawing board.

  • @luke

    Hi, thanks for sharing. The only thing I’m not sure about is var_dump(‘admin_logout’); … probably a left over debug statement?
    Also, it’s best to write ‘/users/login/’ in cake-like array notation.

    Other than that, seems fine to me.

    p.s. I’ve updated your comment to remove the “extra” code.

  • luke aka boobyWomack

    hey teknoid – yes, I usually remove them once I’m happy with something working (textmate find all and then zap)

    good point about the array notation.

  • @luke

    Just a quick fyi… if you use cake’s pr(), which is essentially a wrapper for print_r() then it won’t produce any output while you are in debug = 0 mode (production). So if you do accidentally forget to remove one pr() here or there in production it’s a safer bet then var_dump().

  • Chirayu

    How can we approach if we have registered users as well with administrator. Auth component will fail in that case ?

  • @Chirayu

    Why would it fail?
    You should probably have a ‘role’ field in your users table, you can then do a check such as:
    if($this->Auth->user(‘role’)) { …

    This is partially covered in the manual.

  • foldi

    In both Mark’s and Steve’s comments, why is the comparison of $this->params[$admin] repeated?

    isset($this->params[$admin] && $this->params[$admin])

    This is throwing a php error for me. However, when I express it like this

    isset($this->params[$admin])

    it works fine.

  • @foldi

    It’s not really “repeated”… one is checking if the param is actually set and the other one is checking if it’s set to ‘true’. That being said, there seems to be a missing or misplaced ‘)’ in the if()

  • It should be if (isset($this->params[$admin]) && $this->params[$admin]). The closing ) for isset() is at the end of the line.

  • @darthmalis

    You are correct, I stopped being lazy and fixed up the code in the preceding comments ;)
    Thanks.

  • james

    great article, exactly what i was looking for…going to try it now

  • @james

    Thanks, hopefully it gets you started on the right track.

  • kicaj

    Hi, i have problem, i get message from my browser (Firefox) like as “Redirect Loop Error”, why? Sorry, but my english it’s not perfect, and lot of things i can’t understand…

  • @kicaj

    I’ve seen the problem happen once in a while, but when exactly does it take place? When you try to login/logout… or all the time when you visit homepage, for example?

  • kicaj

    When I set in app_controller in components ‘Auth’, and use $this->Auth->allow(‘some’, ‘functions’); or $this->Auth->allowedActions()

    I think, I can’t use $this-Auth… in app_controller

  • @kicaj

    You certainly can use it in App Controller.
    Usually this happens when you try to login/logout and leads you back to the page you came from, which creates a infinite loop.

    That being said, it’s best to specify which actions you are “allowing” in the specific controller.

  • kicaj

    I have next problem, hehe again:p

    When i add ‘language’ to my url (eg. in form action) i can’t loggin to admin area, why?
    I use app_helper to add language, it’t very simple…

  • kicaj
  • Chirayu

    Hi techno,

    I went through your tutorial. I had a query on using auth component for Registered users as well as Admin users. You replied that its possible if your user table has the role field.

    I am using table structure which has Many to Many relations with User and Role and maintained into separate table called users_roles. Typically I have followed the structure posted here : http://www.studiocanaria.com/articles/cakephp_auth_component_users_gro

    can you suggest in brief what should be the approach, or some code example.

    • bendo01

      @Chirayu : i’m using the same technique, but the problem is if an admin trying to login using admin params it’s always shows flash messages ‘is not authorize’ , can you help? :)

  • @Chirayu

    The approach would be the same, you simply need to check the role of the currently logged-in user.

  • It`s old school:
    $this->Auth->allow(‘index’, ‘view’);

    can we dissallow /admin/ prefix automatically?

  • @Gediminas

    Everything is disallowed by default.

  • I’d like to add my thanks for this great article.

    I had hoped to construct my app. so that I had one login form /users/login/ and then to use logic in the login action to decide if the user was an administrator and redirect them to /admin/users/whatever/.

    Is there a way to do this? All my attempts came to nought so for now I’ve used your method of having a separate admin login /admin/users/login/

  • First of all, thanks for the article, very simple concept and I really like it :).

    Just a quick tip for anyone out there who require your user authentication as well as admin and want to deny access to any user trying to use any admin prefixed link.

    1) You need to create a field in your users table called ‘role’. Then either make a user an ‘admin’ or ‘user’ (Or use whatever you prefer.).

    2) In your AppController::beforeFilter() add this bit of code at the bottom

    $admin = Configure::read(‘Routing.admin’);
    if (isset($this->params[$admin]) && $this->params[$admin] && $this->Auth->user(‘role’) != ‘admin’)
    {
    $this->Session->setFlash(‘You are not authorized.’, ‘default’, array(‘class’ => ‘error’));
    $this->redirect(‘/’);
    }

    Basically what this does is check to see if the users role is admin and redirects to the home page if they are not. For added security remove the flash message and redirect and serve up a 404 error so the user doesn’t know they found an admin link.

  • @Cody

    Great addition. Thanks for sharing.

    p.s. I’ve made corrections per your request.

  • tricky999

    I had some problems with your code in cakePHP 1.3

    However, with a bit of head scratching, I got your beforeRender() function to work by doing the following:

    function beforeRender () {
    // $admin = Configure::read(‘Routing.admin’);
    //This is no longer required, but in 1.3 it should read $admin = Configure::read(‘Routing.prefixes’, array(‘admin’))

    //It appears that as long as you have set up admin routing in the config/core.php file, this code works….I used Bake to create mine :)

    if (isset($this->params[‘admin’]) && $this->params[‘admin’]) {
    $this->layout = ‘admin’;
    }
    }

    I’m finding the changes from 1.2 to 1.3 rather interesting at present, especially as a noob, so hopefully this will save someone else hours of frustration!

  • @tricky999

    That’s true… 1.3 only has prefix routing. So unless you manually add Configure::write(‘Routing.admin’); (WHICH YOU REALLY SHOULD NOT) the above is not going to work on 1.3

    Prefix routing is the way to go in 1.3. Thanks for sharing.

  • Thomas

    Thanks for this great article! It helped me exactly with what I needed for a simple CMS for personal use =]

  • Pingback: Tweets that mention Getting Auth and Admin Routing to play nicely together | nuts and bolts of cakephp -- Topsy.com()

  • Sidra

    Thanks alot :) it works fine for me

  • teknoid

    @Sidra

    You are welcome.

  • Haris

    Great post!!! I am actually on my first CakePHP app and this article has cleared up certain things. I really don’t want to get off of the subject; however, i can not seem to find anywhere how to set up my routes the right way and this article is as close as i have been able to find.

    I have setup my PostsController index action in my routes file to be my home page e.g. http://www.exampleSite.com

    The probelm i am having now is that my urls are duplicated.
    http://www.exampleSite.com & http://www.exampleSite.com/posts are the same.

    Is there anyway to reset the routes and completely take control over them or am i missing something simple because i am really new to programming in general.

    Thanks to everyone in advance.

  • Benko

    Any chance of an updated version of this article for CakePHP 2.0? Doesn’t seem to work with the new version.

  • teknoid

    @Benko

    Unlikely, the concepts are similar… syntax is different, so it’s a little more involved than copy/paste. (but that’s a good thing).

%d bloggers like this: