Implementing HTTP Basic Auth in RESTful Web Service APIs

I’m trying to implement an RESTful app in a Yii2 web application. First of all I need to authenticate a user in my Restful api, via HTTP basic Auth. Only login/logout, not needing to manage user registration and all the complex authentication logic so far.

After few days (yes, I’m not really a fox) I’m stuck

DB User table




  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,

  `role_id` int(10) unsigned NOT NULL,

  `status` tinyint(4) NOT NULL,

  `email` varchar(255) NOT NULL,

  `new_email` varchar(255) DEFAULT NULL,

  `username` varchar(255) DEFAULT NULL,

  `password` varchar(255) DEFAULT NULL,

  `auth_key` varchar(255) DEFAULT NULL,

  `api_key` varchar(255) DEFAULT NULL,

  `login_ip` varchar(45) DEFAULT NULL,

  `login_time` timestamp NULL DEFAULT NULL,

  `create_ip` varchar(45) DEFAULT NULL,

  `create_time` timestamp NULL DEFAULT NULL,

  `update_time` timestamp NULL DEFAULT NULL,

  `ban_time` timestamp NULL DEFAULT NULL,

  `ban_reason` varchar(255) DEFAULT NULL,



This is the complete directory structure




.

├── api

├── assets

├── commands

├── config

├── controllers

├── js

├── mail

├── messages

├── models

├── runtime

├── tests

├── vendor

├── views

└── www



The restful app is separate from web app. It is located in api directory, with this tree




api

├── config

│   └── api.php

├── index.php

├── modules

│   └── v1

│   	├── controllers

│   	│   └── UserController.php

│   	├── models

│   	│   └── User.php

│   	└── Module.php

└── robots.txt



Edit: I cut the parts exceeded. See after

I changed User Controller and User Model, at least to check if I can authenticate a user, but without luck. Postman (basic auth with username and password) still return




"name": "Unauthorized",

"message": "You are requesting with an invalid credential.",

"code": 0,

"status": 401,

"type": "yii\\web\\UnauthorizedHttpException"



This is my actual api config




<?php

 

//$db 	= require(__DIR__ . '/../../config/db.php');

$params = require(__DIR__ . '/../../config/params.php');

 

$config = [

	'id' => 'basic',

	'name' => 'test',

	// Need to get one level up:

	'basePath' => dirname(__DIR__).'/..',

	'bootstrap' => ['log'],

	'language' => 'it-IT', // da fare: ottenere il valore dal browser

	'timeZone' => 'Europe/Rome', 

	'components' => [

    	'request' => [

        	'class' => '\yii\web\Request',

        	'enableCookieValidation' => false,

        	'parsers' => [

            	'application/json' => 'yii\web\JsonParser',

        	],

    	],

    	'user' => [

          	'identityClass' => 'app\api\modules\v1\models\User', 

          	'enableSession' => false,

    	],

    	'log' => [

        	'traceLevel' => YII_DEBUG ? 3 : 0,

        	'targets' => [

            	[

                	'class' => 'yii\log\FileTarget',

                	'levels' => ['error', 'warning'],

            	],

        	],

    	],

    	'db' => require(__DIR__ . '/../../config/db.php'),

    	'urlManager' => [

        	'enablePrettyUrl' => true,

        	'enableStrictParsing' => true,

        	'showScriptName' => false,

        	'rules' => [

            	['class' => 'yii\rest\UrlRule', 'controller' => ['v1/user']],

            	/*'OPTIONS v1/user/login' => 'v1/user/login',

            	'POST v1/user/login' => 'v1/user/login',*/

        	],

    	], 

	],

	'modules' => [

    	'v1' => [

        	'class' => 'app\api\modules\v1\Module',

    	],

	],

	'params' => $params,

];

 

return $config;



This User.php model:




<?php

namespace app\api\modules\v1\models;

use Yii;

use yii\db\ActiveRecord;

use yii\web\IdentityInterface;

use yii\base\NotSupportedException;


/**

 * This is the model class for table "user".

 *

 * @property string $id

 * @property string $role_id

 * @property integer $status

 * @property string $email

 * @property string $new_email

 * @property string $username

 * @property string $password

 * @property string $auth_key

 * @property string $api_key

 * @property string $login_ip

 * @property string $login_time

 * @property string $create_ip

 * @property string $create_time

 * @property string $update_time

 * @property string $ban_time

 * @property string $ban_reason

 */

class User extends ActiveRecord implements IdentityInterface

{


	public static function tableName()

	{

    	return 'user';

	}


	public static function findIdentity($id)

	{

    	return static::findOne($id);

	}


	public static function findIdentityByAccessToken($token, $type = null)

	{

    	return static::findOne(['api_key' => $token]);

	}


	public function getId()

	{

    	return $this->id;

	}


	public function getAuthKey()

	{

    	return $this->auth_key;

	}


	public function validateAuthKey($authKey)

	{

    	return $this->auth_Key === $authKey;

	}


}



And finally my UserController.php:




<?php

namespace app\api\modules\v1\controllers;

use Yii;

use yii\rest\ActiveController;

use yii\filters\auth\HttpBasicAuth;

use yii\helpers\ArrayHelper;

use yii\base\NotSupportedException;

use yii\web\Response;

use yii\filters\ContentNegotiator;

use app\api\modules\v1\models\User;


class UserController extends ActiveController

   {

  	public $modelClass = 'app\api\modules\v1\models\User';


public function behaviors()

{

	$behaviors = parent::behaviors();

	$behaviors['authenticator'] = [

      	'class' => HttpBasicAuth::className(),

      	'auth' => [$this, 'auth']

    	];

	return $behaviors;

}




public function auth($username, $password) {

	return User::findOne([

    	'username' => $username,

    	'password' => $password,

	]);

}



I read the official documentation and every blog that I found around the web, also in Russian and Chinese, but I just can not solve the problem.

With this controller I can list all the users record in db via postman get, so I think the configuration works fine, but I still have a problem to understand how to do authentication via rest




<?php

namespace app\api\modules\v1\controllers;

use Yii;

use yii\rest\ActiveController;

use yii\filters\auth\HttpBasicAuth;

use yii\helpers\ArrayHelper;

use yii\base\NotSupportedException;

use yii\web\Response;

use yii\filters\ContentNegotiator;

use app\api\modules\v1\models\User;


class UserController extends ActiveController

   {

  	public $modelClass = 'app\api\modules\v1\models\User';

}



Sorry if I insist, but I’m really in trouble and need help. I’d love it if someone could at least put me on the right track, because I do not know if what I have written in the second post is not working because it contains errors (likely), or because it needs to implement other parts of a login process or, even, because a bug https://github.com/y...ii2/issues/7409

I solved it this way:

In models\User.php create this method:




public static function findByUsernameAndPassword($username, $password)

    {

        foreach (self::$users as $user) {

            if (strcasecmp($user['username'], $username) === 0) {

				if(strcasecmp($user['password'], $password) === 0) {

					return new static($user);

				}

            }

        }


        return null;

    }




In the controller:




public function behaviors()

	{

		$behaviors = parent::behaviors();

		$behaviors['basicAuth'] = [

			'class' => HttpBasicAuth::className(),

			'auth' => function($username, $password){

				return User::findByUsernameAndPassword($username,$password);

			},

		];

		return $behaviors;

	}



I hope you help.

I left this problem back, because I had more urgent things to solve. But soon I’ll try. I will tell you…

I would like to know how to do this too.

I do not understand at first place, why findIdentityByAccessToken() is triggered when you use HttpBasicAuth. As far as I understand, when you are authenticating using basic auth you are doing it by username and password, not by token.

So mu behaviours() method looks like this:




    public function behaviors()

    {

        $behaviors = parent::behaviors();

        

        $behaviors['contentNegotiator'] = [

            'class' => 'yii\filters\ContentNegotiator',

            'formats' => [

                'application/json' => Response::FORMAT_JSON,

            ]

        ];

        

        $behaviors['authenticator'] = [

            'class' => HttpBasicAuth::className(),

        ];


        return $behaviors;

    }



So when I trigger "localhost/api/users/1", I get response with:

"message": "\"findIdentityByAccessToken\" is not implemented."

How to validate using token when I have username and password, this just doesn’t make sense to me.

Even if I find some way to tell yii not to use this findIdentityByAccessToken(), what is next?

We create some method that is doing something like this ?


return User::findOne([

    'username' => $username,

    'password' => $password,

])



Do we have to collect $username and $password from request using request component ? Like this:


Yii::$app->request->post('username');

And then what ? Even if we manage to validate that user is good, what to do with that information ? We do all of this in controller action and if everything is OK, just continue executing rest of the code in that action, in this case actionView() ?

[color="#006400"]/* Moved from "General Discussions" to "REST APIs" */[/color]

What I did is set the behaviors like this




        $behaviors['authenticator'] = [

            'class' => HttpBasicAuth::className(),

            'auth'  => 'app\models\Users::getUserIdentity',

        ];



The key is auth, if that is null it will default findIdentityByAccessToken

In the User model add the function




public static function getUserIdentity($username, $password)

{

   //find and authenticate user here


   return $user;

}