phplaravelauthenticationlaravel-5.1

Custom user authentication base on the response of an API call


Description:

I have been using Laravel for a bunch of project now. Implementing User Authentication is simple in Laravel. Now, the structure that I am dealing with is a little different - I don't have a database or a users table locally. I have to make an API call to query what I need.


I've tried

public function postSignIn(){

    $username     = strtolower(Input::get('username'));
    $password_api = VSE::user('password',$username); // abc <-----
    $password     = Input::get('password'); // abc <-----


    if ( $password == $password_api ) {
        //Log user in
        $auth = Auth::attempt(); // Stuck here <----
    }

    if ($auth) {
      return Redirect::to('/dashboard')->with('success', 'Hi '. $username .' ! You have been successfully logged in.');
    }
    else {
      return Redirect::to('/')->with('error', 'Username/Password Wrong')->withInput(Request::except('password'))->with('username', $username);
    }
  }

Updated

I connect to the API using a simple shell_exec command in my VSE class

public static function user($attr, $username) {

        $data = shell_exec('curl '.env('API_HOST').'vse/accounts');
        $raw = json_decode($data,true);
        $array =  $raw['data'];
        return $array[$attr];
    }

I wish I can show that to you here, But it is on the VM on my local machine so please stay with me here. Basically, It

Execute

curl http://172.16.67.137:1234/vse/accounts <--- updated

Response

Object
data:Array[2]

0:Object
DBA:""
account_id:111
account_type:"admin"
address1:"111 Park Ave"
address2:"Floor 4"
address3:"Suite 4011"
city:"New York"
customer_type:2
display_name:"BobJ"
email_address:"bob@xyzcorp.com"
first_name:"Bob"
last_name:"Jones"
last_updated_utc_in_secs:200200300
middle_names:"X."
name_prefix:"Mr"
name_suffix:"Jr."
nation_code:"USA"
non_person_name:false
password:"abc"
phone1:"212-555-1212"
phone2:""
phone3:""
postal_code:"10022"
state:"NY"
time_zone_offset_from_utc:-5

1:Object
DBA:""
account_id:112
account_type:"mbn"
address1:"112 Park Ave"
address2:"Floor 3"
address3:"Suite 3011"
city:"New York"
customer_type:2
display_name:"TomS"
email_address:"tom@xyzcorp.com"
first_name:"Tom"
last_name:"Smith"
last_updated_utc_in_secs:200200300
middle_names:"Z."
name_prefix:"Mr"
name_suffix:"Sr."
nation_code:"USA"
non_person_name:false
password:"abd"
phone1:"212-555-2323"
phone2:""
phone3:""
postal_code:"10022"
state:"NY"
time_zone_offset_from_utc:-5
message:"Success"
status:200

As you can see the password for Bob is abc and for Tom is abd


Solution

  • By following the steps below, you can setup your own authentication driver that handles fetching and validating the user credentials using your API call:

    1. Create your own custom user provider in app/Auth/ApiUserProvider.php with the following contents:

    namespace App\Auth;
    
    use Illuminate\Contracts\Auth\UserProvider;
    use Illuminate\Contracts\Auth\Authenticatable as UserContract;
    
    class ApiUserProvider implements UserProvider
    {
        /**
         * Retrieve a user by the given credentials.
         *
         * @param  array  $credentials
         * @return \Illuminate\Contracts\Auth\Authenticatable|null
         */
        public function retrieveByCredentials(array $credentials)
        {
            $user = $this->getUserByUsername($credentials['username']);
    
            return $this->getApiUser($user);
        }
    
        /**
         * Retrieve a user by their unique identifier.
         *
         * @param  mixed  $identifier
         * @return \Illuminate\Contracts\Auth\Authenticatable|null
         */
        public function retrieveById($identifier)
        {
            $user = $this->getUserById($identifier);
    
            return $this->getApiUser($user);
        }
    
        /**
         * Validate a user against the given credentials.
         *
         * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
         * @param  array  $credentials
         * @return bool
         */
        public function validateCredentials(UserContract $user, array $credentials)
        {
            return $user->getAuthPassword() == $credentials['password'];
        }
    
        /**
         * Get the api user.
         *
         * @param  mixed  $user
         * @return \App\Auth\ApiUser|null
         */
        protected function getApiUser($user)
        {
            if ($user !== null) {
                return new ApiUser($user);
            }
        }
    
        /**
         * Get the use details from your API.
         *
         * @param  string  $username
         * @return array|null
         */
        protected function getUsers()
        {
            $ch = curl_init();
    
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_URL, env('API_HOST') . 'vse/accounts');
    
            $response = curl_exec($ch);
            $response = json_decode($response, true);
    
            curl_close($ch);
    
            return $response['data'];
        }
    
        protected function getUserById($id)
        {
            $user = [];
    
            foreach ($this->getUsers() as $item) {
                if ($item['account_id'] == $id) {
                    $user = $item;
    
                    break;
                }
            }
    
            return $user ?: null;
        }
    
        protected function getUserByUsername($username)
        {
            $user = [];
    
            foreach ($this->getUsers() as $item) {
                if ($item['email_address'] == $username) {
                    $user = $item;
    
                    break;
                }
            }
    
            return $user ?: null;
        }
    
        // The methods below need to be defined because of the Authenticatable contract
        // but need no implementation for 'Auth::attempt' to work and can be implemented
        // if you need their functionality
        public function retrieveByToken($identifier, $token) { }
        public function updateRememberToken(UserContract $user, $token) { }
    }
    

    2. Also create a user class that extends the default GenericUser offered by the authentication system in app/Auth/ApiUser.php with the following contents:

    namespace App\Auth;
    
    use Illuminate\Auth\GenericUser;
    use Illuminate\Contracts\Auth\Authenticatable as UserContract;
    
    class ApiUser extends GenericUser implements UserContract
    {
        public function getAuthIdentifier()
        {
            return $this->attributes['account_id'];
        }
    }
    

    3. In your app/Providers/AuthServiceProvider.php file's boot method, register the new driver user provider:

    public function boot(GateContract $gate)
    {
        $this->registerPolicies($gate);
    
        // The code below sets up the 'api' driver
        $this->app['auth']->extend('api', function() {
            return new \App\Auth\ApiUserProvider();
        });
    }
    

    4. Finally in your config/auth.php file set the driver to your custom one:

        'driver' => 'api',
    

    You can now do the following in your controller action:

    public function postSignIn()
    {
        $username = strtolower(Input::get('username'));
        $password = Input::get('password');
    
        if (Auth::attempt(['username' => $username, 'password' => $password])) {
            return Redirect::to('/dashboard')->with('success', 'Hi '. $username .'! You have been successfully logged in.');
        } else {
            return Redirect::to('/')->with('error', 'Username/Password Wrong')->withInput(Request::except('password'))->with('username', $username);
        }
    }
    

    Calling Auth::user() to get user details after a successful login, will return an ApiUser instance containing the attributes fetched from the remote API and would look something like this:

    ApiUser {#143 ▼
      #attributes: array:10 [▼
        "DBA" => ""
        "account_id" => 111
        "account_type" => "admin"
        "display_name" => "BobJ"
        "email_address" => "bob@xyzcorp.com"
        "first_name" => "Bob"
        "last_name" => "Jones"
        "password" => "abc"
        "message" => "Success"
        "status" => 200
      ]
    }
    

    Since you haven't posted a sample of the response that you get when there's no match in the API for the user email, I setup the condition in the getUserDetails method, to determine that there's no match and return null if the response doesn't contain a data property or if the data property is empty. You can change that condition according to your needs.


    The code above was tested using a mocked response that returns the data structure you posted in your question and it works very well.

    As a final note: you should strongly consider modifying the API to handle the user authentication sooner rather than later (perhaps using a Oauth implementation), because having the password sent over (and even more worryingly as plain text) is not something you want to postpone doing.