9

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

code-8
  • 54,650
  • 106
  • 352
  • 604
  • You can [extend the Laravel authentication system](http://laravel.com/docs/5.1/authentication#adding-custom-authentication-drivers) by creating your own user provider that handles the login validation and user details, and setting that as the auth driver. – Bogdan Oct 25 '15 at 15:54
  • If you find that the documentation is not detailed enough, then you can have a look at the [`Illuminate\Auth\DatabaseUserProvider`](https://github.com/laravel/framework/blob/5.1/src/Illuminate/Auth/DatabaseUserProvider.php) source to see how it handles a database stored user and apply the logic to your remote API. – Bogdan Oct 25 '15 at 15:59
  • If you would provide some code showcasing how you connect to the API, and what requests you're making to authenticate and fetch the user information, then I would probably be able to provide an answer on how to integrate the API calls into a Laravel Auth driver. – Bogdan Oct 29 '15 at 13:38
  • So authentication would be done by checking the credentials the user inputs with the ones returned by the cURL request? – Bogdan Oct 29 '15 at 13:48
  • Yes sir. I know it's suck, but it is just the starter of the project. It's for Demo purpose only. – code-8 Oct 29 '15 at 13:49
  • Is there any reason you use `shell_exec` instead of [`curl_exec`](http://php.net/manual/en/function.curl-exec.php)? I'm just asking, because I could provide an answer with a better approach to the API calls. – Bogdan Oct 29 '15 at 13:51
  • No. I used `shell_exec` because I didn't know anything about `curl_exec`. Checking it out now .... – code-8 Oct 29 '15 at 13:53

1 Answers1

18

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.

Bogdan
  • 43,166
  • 12
  • 128
  • 129
  • I just realize that I connect to the wrong URL. I suppose to make a request to : `/vse/accounts` instead. I hope you don't mind tweak your answer on `step 1`. I'm sorry for this. I just adjusting it myself, but it break. This is the first time, I'm dealing with driver. – code-8 Oct 29 '15 at 19:16
  • So it should be `'/vse/accounts/user/' . $username`? – Bogdan Oct 29 '15 at 19:31
  • `$username` is not part of the rule anymore. Just `/vse/accounts` and that's it. – code-8 Oct 29 '15 at 19:45
  • So if I get this correctly, you get all accounts via the API call and have to check against the entire list to match the credentials? – Bogdan Oct 29 '15 at 19:52
  • Correct. I have to loop through them all, and check if the user input is match the existing `email/password`. – code-8 Oct 29 '15 at 19:53
  • I've updated the `getUserDetails` method to reflect the changes you posted. – Bogdan Oct 29 '15 at 20:16
  • 1
    I tested it. It works perfectly. One last minor issue that I have here is I didn't seem to be able to grab anything at all out of my `Auth::user()` instance. – code-8 Oct 29 '15 at 20:26
  • Because I tested fetching the `Auth::user()` data within the same request, I missed the fact it was actually needed to also implement the `retrieveById` method, which is used by the [`Illuminate\Auth\Guard`](http://laravel.com/api/5.1/Illuminate/Auth/Guard.html) class to get the user details from the session via the ID for separate requests (i.e. after the login redirect). I've updated my answer with the necessary fixes to the `ApiUserProvider` class in order to make `Auth::user()` work (which involved adding a few extra helper methods). – Bogdan Oct 29 '15 at 20:53
  • Man, I can't thank you enough for this help, I already accept the answer. – code-8 Oct 29 '15 at 22:31
  • As a side note, I do feed obligated to stress the fact the API you're using needs to be improved because you're leaving yourself open to [man-in-the-middle attacks](https://www.owasp.org/index.php/Man-in-the-middle_attack), not to mention that it's **huge** security risk to store passwords as plain text. So if you're not the one maintaining the API code, I strongly suggest you urge the people responsible to improve it. – Bogdan Oct 29 '15 at 22:45
  • I keep getting `Call to undefined function App\Auth\curl_init()` on my staging server. I was hoping you can provide me a little hint. Local works perfectly. – code-8 Nov 02 '15 at 17:31
  • 1
    The cURL extension needs to be installed for those methods to be available. To check that, you can simply run this command on your staging server `php -m | grep curl`. It should output _"curl"_ if the extension is installed, otherwise it won't output anything. – Bogdan Nov 02 '15 at 20:52
  • Thanks, and you're right. I need to install cURL on my server. – code-8 Nov 02 '15 at 21:55
  • @Bogdan can you please tell me what methods I should implement to use Auth::check? – Raghavendra N Feb 03 '16 at 23:58
  • @RaghavendraN For `Auth::check` to work it needs the `retrieveById` method to be implemented on the user provider class. – Bogdan Feb 04 '16 at 14:25
  • @Bogdan may I ask something, how can I implement "register User" function from here? i can add "register method" at class ApiUser, but then how do i link that "register method" to the actual execution in class "ApiUserProvider" ? - Thanks! – AnD Oct 16 '16 at 16:50
  • 1
    There are several lines that say 'driver', in auth.php. Showing a single line with no context is the exact opposite of helpful. – Aaron Hill Mar 26 '18 at 03:08
  • @AaronHill If you took the time to check the question tags you'd have seen that the question was asked for Laravel 5.1, and in version 5.1 the `auth.php` file had [one and only one line](https://github.com/laravel/laravel/blob/5.1/config/auth.php#L18) for defining the driver (since there wasn't any API or other database settings back then) so the code snippet was more than clear. But apparently it's easier to make snide remarks. – Bogdan Mar 26 '18 at 09:54
  • I am getting error "Call to undefined method App\Auth\ApiUserProvider::check()" on, "vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php". I am using laravel 5.8.38. – Shuvo Joseph Mar 08 '21 at 13:58