Debugging user-related issues can be tricky.
As a developer, I'm often tasked with debugging a particular error that a user may encounter. Naturally, the first step is to reproduce that issue. There are many possible solutions to logging in as another users. You could simply set the password hash on all accounts to be the same as your own known password, but this procedure will have to be repeated any time you re-import your production database into development.
You can also add temporary code into your authentication controller such as this:
Auth::loginById(42);
While this is effective, it's extremely dangerous because of the possibility that this debugging code will make it's way into production. The best solution is to allow a user or group of users with a specific role or privilege to temporarily log in as another user, entirely through the interface of the app itself. This will have the advantage of also allowing developers to reproduce issues in the production environment as a specific user.
I built this solution in Laravel 5.4, but the concepts are general, and thus should work in any Laravel 5 app.
Step 1: Determine who has the ability to switch users
There are so many ways you can do this. For my application, I've chosen to create a Developer role, and whoever has that role can switch users. You may want to create a can_switch_users permission and then attach that to the role of your choice. I'm using Laratrust to manage my roles and permissions, so the code examples here are specific to that package.
I think that this is a task that is best suited to put into a console command, so the first thing we're going to do is to set up the command to accomplish this:
php artisan make:command SetupDeveloperRole
Here's my thought process on how to do this.
- Make sure the role does not already exist.
- Create the new role
- Check if the permission you need already exists
- Create that permission
- Attach the permission to the role
- Attach the role to a specified user
Here's the console command I created that follows the above steps in the handle() method (don't forget to add the class to app/Console/Kernel.php).
<?php namespace App\Console\Commands; use App\Permission; use App\Role; use App\User; use Illuminate\Console\Command; use Illuminate\Database\Eloquent\ModelNotFoundException; class SetupDeveloperRole extends Command { protected $signature = 'setup:developerrole'; protected $description = 'Sets up developer role with log in as another user permission.'; public function __construct() { parent::__construct(); } public function handle() { try { $developer = Role::findOrFail(Role::DEVELOPER_ID); $this->info("The developer role already exists."); } catch (ModelNotFoundException $e) { $developer = new Role(); $developer->name = "Developer"; $developer->save(); $this->info("Developer role created."); } try { $loginAsOtherUsers = Permission::findOrFail(Permission::LOG_IN_AS_OTHER_USERS_ID); $this->info("Log in as other users permission already exists."); } catch (ModelNotFoundException $e){ $loginAsOtherUsers = new Permission(); $loginAsOtherUsers->name = "log_in_as_other_users"; $loginAsOtherUsers->display_name = "Log in As Other Users"; $loginAsOtherUsers->save(); $this->info("Log in as other users permission created."); } $developer->attachPermission($loginAsOtherUsers); $this->info("Log in as Other User permission attached to Developer Role."); $admin = User::where('email', config('admin.primary.email'))->firstOrFail(); $admin->attachRole($developer); $this->info("Developer role attached to primary admin."); } }
Step 2: Create the Controller to handle the actual User switching
I'm a fan of using the artisan:make commands even for something as simple as a controller, so:
php artisan make:controller UserSwitchController
So our steps here are as follows:
- Create a method to handle switching to another user
- Create a method for restoring the original user
- Create middleware that will prevent unauthorized access to these methods
In the first method, we store the existing user id in session as well as a "user_is_switched" variable. I suppose that's redundant, but by adding the separate "user_is_switched" variable, the checks later on just make more semantic sense. Feel free to omit it if you like.
public function switchUser(Request $request){ $request->session()->put('existing_user_id', Auth::user()->id); $request->session()->put('user_is_switched', true); $newuserId = $request->input('new_user_id'); Auth::loginUsingId($newuserId); return redirect()->to('/'); }
Next, we need to make our method for restoring the original user.
public function restoreUser(Request $request) { $oldUserId = $request->session()->get('existing_user_id'); Auth::loginUsingId($oldUserId); $request->session()->forget('existing_user_id'); $request->session()->forget('user_is_switched'); return redirect()->back(); }
For sake of completeness (and the "use" statements at the top), here's the entire controller:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class UserSwitchController extends Controller { public function __construct() { $this->middleware('is_developer_or_switched'); } public function switchUser(Request $request){ $request->session()->put('existing_user_id', Auth::user()->id); $request->session()->put('user_is_switched', true); $newuserId = $request->input('new_user_id'); Auth::loginUsingId($newuserId); return redirect()->to('/'); } public function restoreUser(Request $request) { $oldUserId = $request->session()->get('existing_user_id'); Auth::loginUsingId($oldUserId); $request->session()->forget('existing_user_id'); $request->session()->forget('user_is_switched'); return redirect()->back(); } }
Let's generate the middleware to protect these methods
php artisan make:middleware IsDeveloperOrSwitched
Again, as a reminder, we're using Laratrust to manage roles and permissions here. If you're using a different package, your implementation may be different. Notice that all we're doing is checking to see if either the user has the Developer role or if the user_is_switched variable has been set into session. This is important since it's likely that the user you'r switching to does not have the Developer role. Here's our entire Middleware file. Notice that we're pulling the currently authorized user from the Guard passed to the constructor. This is one of those cool things that Laravel does for us so we don't have to worry about it.
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Contracts\Auth\Guard; class IsDeveloperOrSwitched { protected $user; public function __construct(Guard $auth) { $this->user = $auth->user(); } public function handle($request, Closure $next) { if($this->user->hasRole('Developer') || $request->session()->get('user_is_switched')){ return $next($request); } return redirect()->to('/'); } }
Again, don't forget to register this to the $routeMiddleware array in app/Http/Kernel.php
'is_developer_or_switched' => \App\Http\Middleware\IsDeveloperOrSwitched::class,
next, let's add that to our constructor method:
public function __construct() { $this->middleware('is_developer_or_switched'); }
Notice in my example, I'm intentionally overriding the parent constructor. You may or may not want to do that, so be careful here.
Step 3: Make the methods accessible via routes
This is an easy one. I'm going to make the switchUser method accessible via POST only because we're sending data. The restoreUser method only pulls it's check from session, so I'm going to set that one to GET (it'll also make it simpler to make that a simple link to click on).
Add this to routes/web.php (if you're prior to Laravel 5.3, you're still in app/Http/routes.php):
Route::post('switchuser', 'UserSwitchController@switchUser')->name('user.switch'); Route::get('restoreuser', 'UserSwitchController@restoreUser')->name('user.restore');
Notice that I name the routes here. I love the new Laravel 5.4 syntax for this. If you're not naming all of your routes, you really should. It's one of those things that will just make life easier and your code cleaner rather than having to hard code URLs all over the place.
Step 4: Create the user switching mechanism in your views
Most apps will have a user index page that hopefully is already only accessible to certain users. In this example, I have a standard table that lists all users. Note the use of the Laratrust blade function to check for a specific role.
<table class="table table-striped table-hover table-bordered"> <thead> <tr> <th>User ID</th> <th>Name</th> <th>Email</th> <th>Phone</th> <th>Edit</th> @role('Developer') <th>Login As</th> @endrole </tr> </thead> <tbody> @foreach ($users as $user) <tr> <td>{{ $user->id }}</td> <td>{{ $user->name }}</td> <td>{{ $user->email }}</td> <td>{{ $user->phone }}</td> <td>{{ link_to_route('users.edit', 'Edit', array($user->id), array('class' => 'btn btn-info')) }}</td> @role('Developer') <td><form action="{{ route('user.switch') }}" method="POST"> <input type="hidden" name="new_user_id" value="{{ $user->id }}"> {{ csrf_field() }} <button type="submit" class="btn btn-danger">Log in as {{ $user->email }}</button> </form></td> @endrole </tr> @endforeach </tbody> </table>
Now that we've got that set up, we need some sort of notification that lets us know that we are in fact logged in as a different user and provide us with a link to restore our original login. I chose to do this in my layouts/header.php file. Since I'm using bootstrap, this was all I needed to add:
@if(session('user_is_switched')) <div class="alert alert-warning"> You are currently logged in as a different user. <a href="{{ route('user.restore') }}">Click here</a> to restore your login. </div> @endif
I could have gotten fancy and added the original user name and the new user name there, but that would have required me to add more information to sesssion. That's not a bad thing, I just didn't feel like doing it in this particular case.
Take the time to do it right
You can see that creating a system like this did not take all that long and really isn't that complicated. It also forces you to think about certain issues of authorization and how to best address them. Of course, you'll want to tailor this to the needs of your app, but I hope this provides you with a framework that you can build on.
Have any questions? Hit me up on twitter @rorycmcdaniel or comment below.
No comments:
Post a Comment