Loading...

SSO Authentication with Laravel 5.3

Laravel authentication based on JSON response from a SSO Restful service

posted on Jan 30, 2017   

There are times when a simple login page is not enough anymore, when your company requests for something more, when you run different web apps all connected between each others. And in those times, you really need a common login page sharing cookies, sessions, roles and permissions and you come up with a single-sign-on need.

I know there are some out-of-the-box solutions out there and Laravel OAuth2 is something suitable for my needs but due to the strong customization required in this project I had to start with something on my own by scratch.

Well, I struggled a lot and for long to find a good solution, something completly managed  by a central web application to handle all the applications, users, roles and permissions for each application. So far, I finally got what I needed. And I had to build it all by myself as per the hard customization requested by the client, the company I work for in this scenario.

All the web applications already in place have been developed with different languages, from .NET MVC 5 to Java Spring/Hibernate to the most powerful PHP framework (just my opinion) Laravel 5.1/5.3. Databases used in all the applications are SQLServer 2008 and Oracle 11g Enterprise

Before all, I just want to aware all of you reading these lines I'm a passionate ideas developer, an enthusiastic Laravel developer and I love learning and sharing when I get some spare time. Now, the one you are going to read is the solution I came up with for the scenario I had faced and I implemented it all by myself, without grabbing code all over the internet and for this reason, if you don't like the code or the solution I'm goint to share, please, don't be rude to me. I'm just trying to help. 

Ok, now that I explained a bit the scenario, let's dig into the solution.

So the main requirement from stakeholders was to have a centrilized login page, something like Google SSO. To accomplish that, I started analyzing how all the current web applications were managing the login sessions and came up with a solution. One login page based on company Enterprise Active Directory to access the main Global Intranet developer with Laravel 5.1. Once logged in with owns domain account credentials, users can choose which web application to use and click for it. The link to each application is created with an internal referral and a random generated token for each application in order to check authenticity of the request itself.

Once a link is clicked by the user, so to speak the web application the user wants to be granted for, a request is sent to the RESTful SSO that consolidates the request into the database linking the token to the user for that application in that moment with an expiration time for that request, checks for user permissions on that application and gather roles and permissions for that application and user, sending back the request through to the final web application.  

The final web application check back with the SSO REST service if the token is the one related to that user requesting access and if the SSO find the right information in the requests databases in the time-frame defined, the SSO app send back a JSON response with the following structure:

{ id: 1, user: "hushhush", email: "hushus@hushush.com", name: "hushush" ,{role: { id: 1, name: "admin", permission: {[id: 1, name : "read"], [id:2, name: "write"]}}}}

The final web application, the requested one by the user, finally grant access to the user based on roles and permissions received. This handshake between the SSO rest service and the final web application ensure that the final result won't be compromised. If the handshake fails, a 403 page is shown to the user. All the web applications are accessible only through this mechanism and no login page is available at all other than the Global Intranet.

I developed the SSO application using Laravel 5.1 at that time and one of the final web application in Laravel 5.3. So I'm going to share a bit of code for the Laravel final part, the one is getting the JSON request from the SSO, reading the JSON structure and granting access to the user based on that JSON without persisting any user information into the database. Furthermore, all the applications miss the user table into their owned database schema.

The final web application, I mean, the web app that receives the JSON response, must implement the following:

1. Middleware to authenticate each request if the user is not yet authenticated and persisted to the session. The middleware will explode the querystring to search for the token, the user and the application.

https://yourfinalwebapp.dev/api/username/c71b77aae5ec42971131c5a96b90b5f100e42965ebb015ca392f43f7d49bf81bf0b82ec577858b9b265cbdee807a87f07d57a513ac9448e54b16457114ad59bd/12

    public function handle($request, Closure $next, $guard = null)
    {
        list($api, $useraccount, $token, $appid) = explode('/', $request->path()); 
        $credentials['useraccount'] = $useraccount;
        $credentials['token'] = $token;
        $credentials['appid'] = $appid;
 
        if ($user = Auth::guard($guard)->attempt($credentials, false)) {
           
            if ($user && isset($user->Error))
                return abort(401, $user->Error);
            return $this->sendLoginResponse($request, $guard);
        }
        throw new RuntimeException('Not authorized');
    }
 

2. Implement a route in the web route file, the only available access point for the final application.

        Route::get('/api/{useraccount}/{token}/{appid}', function(){})->middleware('auth.token');

All the other routes are grouped under the auth middleware in order to protect unauthorized access.

3. Create a new UserProvider to manage User sessions. You can make it simple by issueing a new provider in the AuthServiceProvider like so:

        $this->app['auth']->provider('custom', function(){
            return new CustomUserProvider( $this->app['hash'], $this->app['config']['auth.providers.custom']['model'], $this->app['session.store']);
        });

The CustomUserProvider will take care of the user. It will store the user session and retrive the user authenticated session. Instead of querying eloquent or database, it will query the session to retrieve the user session.

4. Create new Guard for authentication, session management and so on.... You can accomplish that by extending the current auth guard in the AuthServiceProvider.

      $this->app['auth']->extend('GuardToken', function(){
            $provider = $this->app['auth']->createUserProvider($this->app['config']['auth.guards.web']['provider']);
            $guard = new CheckTokenGuard($provider, $this->app['session.store']);
           .....
          return $guard;
      });

The CheckTokenGuard will take care of handling the whole process of user authentication using the CustomUserProvider and the session. This object will do basically all the same of the original Guard provided by Laravel. Instead of handling user using the attempt method will check the token and the user by asking the SSO Restful service if they are valid. If valid, it will get a JSON response as the one already shown above.

    public function attempt(array $credentials = [], $remember = false, $login = true)
    {
        $this->fireAttemptEvent($credentials, $remember, $login);
        foreach ($credentials as $key => $value) {
            if (! Str::contains($key, 'useraccount') &&
                ! Str::contains($key, 'token') &&
                ! Str::contains($key, 'appid') ) {
                return false;
            }
        }
        $this->lastAttempted =
                  $user = $this->provider->checkByToken(
                                            $credentials['useraccount'],
                                            $credentials['token'],$credentials['appid']
                                          );
       
        if ($user && isset($user->Error))
            return $user;
        if (! $user) {
            $this->fireFailedEvent($user, $credentials);
            return false;
        }
        if ($login) {
            $this->login($user, $remember);
            return $this->user;
        }
        if ($login) {
            $this->fireFailedEvent($user, $credentials);
        }
        return false;
    }

 

The CustomGuard will check for the token through the CustomUserProvider in order to create the user model and roles and permissions models like so:

    public function checkByToken($useraccount, $token, $appid)
    {
        $tokenManager = new TokenManager($useraccount, $token, $appid);
        $response = $tokenManager->checkToken();
        if ($response->getStatusCode() !== 200)
            return $response->getReasonPhrase();
        $body_resp = json_decode($response->getBody());
        if (isset($body_resp->Error))
            return $body_resp;
        $this->setResponseUser($body_resp);
        return $this->user = $this->createUser($this->createModel(), $body_resp);
    }

The TokenManager is a self defined class to manage the curl request to the SSO service and get the result back. Something as easy as this:

    public function checkToken()
    {
        $uri = $this->user.'/'.$this->token.'/'.$this->appid;
        $client = new GuzzleClient(['base_uri' => $this->checkendpoint]);
        $res = $client->request('GET', $uri, [
            'curl' => [
                CURLOPT_SSL_VERIFYPEER => true
            ]
        ]);
        return $res;
    }

 Then the CustomUserProvider will take care of creating the User model and whatever.....

    protected function createUser($model, $body_resp)
    {
        $model->id = $body_resp->id;
        $model->domainaccount = $body_resp->username;
        $model->email = '';
        $this->setUser($model);
        $this->setRoles($model, $body_resp);
        return $model;
    }
    protected function setRoles($model, $body)
    {
        $this->session->set('roles', $this->createRoles($model, $body));
        return $this;
    }
    protected function createRoles($user, $body)
    {
        return json_encode([
            'id' => $body->role->id,
            'user_id' => $user->id,
            'name' => $body->role->rolename,
        ]);
    }

Then, when using Auth::check() and Auth facade in general in the application, it will always look for the user in the current session and it works like the user was stored in the database.

Now you can use your User model as always used, creating the relations to roles and permissions model. In the Role model remember to decode the Json early created during the authentication process in the CustomUserProvider. Then, you're done.

Pro: Centrilized applications list, users, roles and permissions, and more.....

Remember to change your auth config file with your custom providers.

I hope it could help all those fellows around the globe that are trying to make something similar using Laravel and are still struggling with it.

Hope to hear from you, write to me if you want to share your results or you need some extra help. I won't answer promptly for sure, but when I'll find a chair where to sit and rest, I will try to read all of you.

Have a great time on the most powerful and enjoyable PHP framework ever developed up until now....

Thanks to Taylor Otwell for his effort and to Jeffrey Way for his great lessons on Laracasts, I think, guys, if you want start learning Laravel, you must give it a run.