Yii2 REST Simplify BasicAuth - php

I'm impressed with how simple it was to create a REST api in Yii2. However, i'm having a little trouble understanding the Basic Authentication. My needs are utterly simple and i'd like my solution to follow suit.
I need Basic token authentication here. I'm not even against hardcoding it for now, but here's what i've done thus far.
I have database table to hold my singular token ApiAccess(id, access_token)
ApiAccess.php - Model - NOTE: IDE shows syntax error on this first line
class ApiAccess extends base\ApiAccessBase implements IdentityInterface
{
public static function findIdentityByAccessToken($token, $type = null)
{
return static::findOne(['access_token' => $token]);
}
}
Module.php - in init() function
\Yii::$app->user->enableSession = false;
I made an ApiController that each subsequent noun extends
ApiController.php
use yii\rest\ActiveController;
use yii\filters\auth\HttpBasicAuth;
use app\models\db\ApiAccess;
class ApiController extends ActiveController
{
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => HttpBasicAuth::className(),
];
return $behaviors;
}
}
As it stands, accessing an api endpoint in the browser prompts for a username and password. Request via REST Client displays access error.
How do I properly tie HttpBasicAuth to my ApiAccess model?
OR
How do I hardcode an api access token? (First option is obviously best)

Let's watch and try to understand "yii" way basic auth for REST.
1st. When you adding behavior to your REST controller, you enabling basic auth:
$behaviors['authenticator'] = [
'class' => HttpBasicAuth::className(),
];
As you did. What does it mean? It means that your application will parse your authorization header. It looks like:
Authorization : Basic base64(user:password)
Here is a trick for yii2. If you look at code more carefully, you will see that yii uses access_token from user field, so your header should look like:
Authorization : Basic base64(access_token:)
You can parse this header by your own, if you want to change this behavior:
$behaviors['authenticator'] = [
'class' => HttpBasicAuth::className(),
'auth' => [$this, 'auth']
];
....
public function auth($username, $password)
{
return \app\models\User::findOne(['login' => $username, 'password' => $password]);
}
2nd thing to do. You must implement findIdentityByAccessToken() function from identityInterface.
Why your IDE complaining?
class User extends ActiveRecord implements IdentityInterface
Here's how your user class declaration should look.
From your implementation and structure:
public static function findIdentityByAccessToken($token, $type = null)
{
return static::findOne(['access_token' => $token]);
}
you not returning object of class which implements identity interface.
How to make it properly?
Add column access_token to your users table, and return back your user model (you can look how it must look here - https://github.com/yiisoft/yii2-app-advanced/blob/master/common/models/User.php)
If you do this - default code will work with your findIdentityByAccessToken() implementation.
If you don't want to add field to users table - make new one with user_id,access_token fields. Then your implementation should look like:
public static function findIdentityByAccessToken($token, $type = null)
{
$apiUser = ApiAccess::find()
->where(['access_token' => $token])
->one();
return static::findOne(['id' => $apiUser->user_id, 'status' => self::STATUS_ACTIVE]);
}
Hope i could cover all of your questions.

Related

Why does my Laravel policy always return false?

The code for the policy is here:
class userOwnedClassPolicy
{
use HandlesAuthorization;
...
public function create(User $user)
{
return ($user->userType == 'teacher');
}
...
}
This policy is registered thusly in the AuthServiceProvider.php file:
class AuthServiceProvider extends ServiceProvider
{
//Map models to authorization policies.
protected $policies = [
App\Models\classMember::class => App\Policies\classMemberPolicy::class,
App\Models\evaluation::class => App\Policies\evaluationPolicy::class,
App\Models\group::class => App\Policies\groupPolicy::class,
App\Models\groupMember::class => App\Policies\groupMemberPolicy::class,
App\Models\sharedClass::class => App\Policies\sharedClassPolicy::class,
App\Models\slg::class => App\Policies\slgPolicy::class,
App\Models\spreadsheet::class => App\Policies\spreadsheetPolicy::class,
App\Models\spreadsheetValue::class => App\Policies\spreadsheetValuePolicy::class,
App\Models\teacher::class => App\Policies\teacherPolicy::class,
App\Models\test::class => App\Policies\testPolicy::class,
App\Models\userOwnedClass::class => App\Policies\userOwnedClassPolicy::class
];
public function boot()
{
$this->registerPolicies();
}
}
(I have tried registering the policies using strings of the file paths as well, but this accomplishes nothing.)
The relevant section of controller code is here:
class ClassController extends Controller
{
...
public function store(Request $postReq)
{
$this->authorize('create', Auth::user());
userOwnedClass::create([
'name' => $postReq->input('className'),
'ownerId' => Auth::user()->id
]);
}
...
}
I have tried substituting the code in the policy's create method with return true, but even that fails. What have I done wrong, and why does the controller always return a 403 error when called?
As you created policy userOwnedClassPolicy and set it for userOwnedClass model in AuthServiceProvider here:
App\Models\userOwnedClass::class => App\Policies\userOwnedClassPolicy::class
you cannot just run policy method:
$this->authorize('create', Auth::user());
When you run this line above, you tell - check create method for policy for \App\Models\User object, but you don't have any policy created for this model.
So in this case you should run it like so:
$this->authorize('create', \App\Models\userOwnedClass::class);
Then Laravel will know that it should run create method from userOwnedClassPolicy policy and it will automatically pass currently authenticated user into $user variable in policy method.

Specify different set of rules based on which function makes the request

I wish to create a Request in a Laravel 5.1 application which has a specific set of rules based on the function that calls it.
For example, say, there are two functions in UsersController namely, login() and register(). The login() function requires only two inputs- username and password, whereas the register() function requires three inputs- username, password and email among other constraints. How can I create a single Request, call it UserRequest, that can handle the rules corresponding to both the above functions based on whichever makes the Request?
I am not sure if what I'm attempting could be done, or whether it is a good practice. Please suggest regarding the same as well.
You have a lot of flexibility since you can do whatever in rules() method, you just need a way to distinguish who is using the Request.
For this example, i would simply use the route() method(it tells you what route was called), you can do something like this:
class MyRequest extends Request {
/*
* Request rules
*/
protected $rules = [
// login rules
'login_route' => [
'login' => 'required',
],
// register rules
'register_route' => [
'login' => 'sometimes',
],
// depends if you need it
'default' => [
'login' => 'sometimes',
]
];
public function authorize()
{
return true; // or whatever
}
public function rules()
{
// where did this request come from?
$route = $this->route();
if(array_key_exists($route, $this->rules))
return $this->rules[$route];
return $this->rules['default'];
}
}
There are other ways, it depends on your problem, you could check the request method (GET, POST..) using getMethod() or check a segment, or instanciate an object on construct (dependency injection) to check if a user is logged in or not for example, really, it depends.
However, if the use case is complex, it is better to seperate in two requests.
Hope this helps.
You mean form requests. Before your method will be excecuted, the validator checks the rules. If not valid, the not-valid messages array will be returned with http status 422. See example below from https://mattstauffer.co/blog/laravel-5.0-form-requests
<?php namespace App\Http\Controllers;
use App\Http\Requests\FriendFormRequest;
use Illuminate\Routing\Controller;
use Response;
use View;
class FriendsController extends Controller
{
public function getAddFriend()
{
return view('friends.add');
}
public function postAddFriend(FriendFormRequest $request)
{
return Response::make('Friend added!');
}
}
And then your form request class:
<?php namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Response;
class FriendFormRequest extends FormRequest
{
public function rules()
{
return [
'first_name' => 'required',
'email_address' => 'required|email'
];
}
public function authorize()
{
// Only allow logged in users
// return \Auth::check();
// Allows all users in
return true;
}
// OPTIONAL OVERRIDE
public function forbiddenResponse()
{
// Optionally, send a custom response on authorize failure
// (default is to just redirect to initial page with errors)
//
// Can return a response, a view, a redirect, or whatever else
return Response::make('Permission denied foo!', 403);
}
// OPTIONAL OVERRIDE
public function response()
{
// If you want to customize what happens on a failed validation,
// override this method.
// See what it does natively here:
}
}
You can make different 'standalone' form requests and use them for your controller methods.
Official documentation here: http://laravel.com/docs/master/validation#form-request-validation

Getting the controller action before behaviour code runs in Yii2

I'm trying to execute some code inside a Yii2 controller as I need some code from the model to be accessible within the behaviors section so I can pass the model as a parameter and avoid running duplicate queries; however I also need to be able to find out what action is being called, but I am not having much luck.
I have tried using beforeAction but it seems this gets run AFTER the behaviours code runs, so that doesn't help me.
I then tried using init, but it seems the action isn't available via $this->action->id at that point.
Some example code:
class MyController extends Controller {
public $defaultAction = 'view';
public function init() {
// $this->action not available in here
}
public function beforeAction() {
// This is of no use as this runs *after* the 'behaviors' method
}
public function behaviors() {
return [
'access' => [
'class' => NewAccessControl::className(),
'only' => ['view','example1','example2'],
'rules' => [
[
'allow' => false,
'authManager' => [
'model' => $this->model,
'other_param' => $foo,
'other_param' => $bar,
],
'actions' => ['view'],
],
// everything else is denied
],
],
];
}
public function viewAction() {
// This is how it is currently instantiated, but we want to instantiate *before* the behavior code is run so we don't need to instantiate it twice
// but to be able to do that we need to know the action so we can pass in the correct scenario
$model = new exampleModel(['scenario' => 'view']);
}
}
authManager is simply a reference to a member variable inside an extension of the AccessRule class.
Is there anyway I can do this?
Well, if I get you right, you are looking for something like this:
public function behaviors()
{
$model = MyModel::find()->someQuery();
$action = Yii::$app->controller->action->id;
return [
'someBehavior' => [
'class' => 'behavior/namespace/class',
'callback' => function() use ($model, $action) {
//some logic here
}
]
];
}
Because behaviors() is just a method, you can declare any variables and add any logic that you want in it, the only one convention that you must follow - is that return type must be an array.
If you use your custom behavior, you are able to use events() method where you can bind your behavior's methods to certain events. E.g.
class MyBehavior extends Behavior
{
public function events()
{
return [
\yii\web\User::EVENT_AFTER_LOGIN => 'myAfterLoginEvent',
];
}
public function myAfterLoginEvent($event)
{
//dealing with event
}
}
In this example myAfterLoginEvent will be executed after user successfully login into application. $event variable will be passed by framework and depending of event type it will contain different data. Read about event object
UPDATE:
As I can see now my answer was more generic about events and behaviors. And now when you added code, I can suggest to you to override behavior's beforeAction($action) method with the following code:
public function beforeAction($action)
{
$actionID = $action->id;
/* #var $rule AccessRule */
foreach ($this->rules as &$rule) {
$model = &$rule->authManager['model'];
//now set model scenario maybe like this
$model->scenario = $actionID;
}
//now call parent implementation
parent::beforeAction($action);
}
Also take a look at AccessControl implementation of beforeAction method, it invokes for each rule allows method with passing current action to it as a parameter. So if you have class that extends AccessRule, you can either override allows($action, $user, $request) method or matchCustom($action) method to set appropriate model scenario. Hope this will help.
One more alternative:
override controller's runAction($id, $params = []) method. Here $id is actionID - exactly what you need. Check id, set appropriate model scenario and call parent::runAction($id, $params);

Laravel 4 - Hardcoded authentication

I want to create authentication mechanism without need for database where only one person (admin) who knows right username and password (which I would hardcode) would be able to login. I still want to use Auth::attempt(), Auth::check() and other functions.
I found out that I could create my own User driver, but it seems to me that there should be something simpler.
Maybe it is not very nice solution, but I want as simple as possible website.
It may only seem there should be something simpler, but in fact that's as simple as you can get if you want to extend the authentication system. All the methods you're using through the Auth facade (like attempt, check, etc.), are implemented within the Illuminate\Auth\Guard class. This class needs a UserProviderInterface implementation to be injected into the constructor in order to work. Which means that in order to use the Auth facade you either need to use the already implemented DatabaseUserProvider or EloquentUserProvider, or implement your own provider that handles the simple login you want.
Although the article you linked to may look lengthy, to achieve what you need you might get away with much less code in the provider than you might think. Here's what I think is what you need:
1. In your app/config/auth.php change the driver to simple and append the desired login credentials:
'driver' => 'simple',
'credentials' => array(
'email' => 'user#email.com',
'password' => 'yourpassword'
)
2. Create a file in your app directory called SimpleUserProvider.php that has this code:
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\GenericUser;
use Illuminate\Auth\UserProviderInterface;
class SimpleUserProvider implements UserProviderInterface {
protected $user;
public function __construct(array $credentials)
{
$this->user = new GenericUser(array_merge($credentials, array('id' => null)));
}
// If you only need to login via credentials the following 3 methods
// don't need to be implemented, they just need to be defined
public function retrieveById($identifier) { }
public function retrieveByToken($identifier, $token) { }
public function updateRememberToken(UserInterface $user, $token) { }
public function retrieveByCredentials(array $credentials)
{
return $this->user;
}
public function validateCredentials(UserInterface $user, array $credentials)
{
return $credentials['email'] == $user->email && $credentials['password'] == $user->password;
}
}
3. Lastly you'll need to register the new provider with the authentication system. You can append this to the app/start/global.php file:
Auth::extend('simple', function($app)
{
return new SimpleUserProvider($app['config']['auth.credentials']);
});
This should give you a simple (no database) user authentication while still being able to use Laravel's facades.
The accepted answer did not work for me. Every time I logged in, the login was successful but when on the /home page, I was redirected to the login page again.
I found out that this was due to the user not being stored in the session as authenticated user. To fix this, I had to implement the getAuthIdentifier method in the User model class and also implement the retrieveById method .
I've also adjusted my solution to support multiple hard coded users (it presumes, that the email is unique, so we can also use it as id for the user):
1. In app/config/auth.php:
'providers' => [
'users' => [
'driver' => 'array',
],
],
'credentials' => [
'userA#email.com' => 'passA',
'userB#email.com' => 'passB',
]
2. The UserProvider:
use \Illuminate\Auth\GenericUser;
use \Illuminate\Contracts\Auth\UserProvider;
use \Illuminate\Contracts\Auth\Authenticatable;
class ArrayUserProvider implements UserProvider
{
private $credential_store;
public function __construct(array $credentials_array)
{
$this->credential_store = $credentials_array;
}
// IMPORTANT: Also implement this method!
public function retrieveById($identifier) {
$username = $identifier;
$password = $this->credential_store[$username];
return new User([
'email' => $username,
'password' => $password,
]);
}
public function retrieveByToken($identifier, $token) { }
public function updateRememberToken(Authenticatable $user, $token) { }
public function retrieveByCredentials(array $credentials)
{
$username = $credentials['email'];
// Check if user even exists
if (!isset($this->credential_store[$username])) {
return null;
}
$password = $this->credential_store[$username];
return new GenericUser([
'email' => $username,
'password' => $password,
'id' => null,
]);
}
public function validateCredentials(Authenticatable $user, array $credentials)
{
return $credentials['email'] == $user->email && $credentials['password'] == $user->getAuthPassword();
}
}
3. And in app/Providers/AuthServiceProvider:
use Illuminate\Support\Facades\Auth;
class AuthServiceProvider extends ServiceProvider
{
...
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
Auth::provider('array', function($app, array $config) {
// Return an instance of Illuminate\Contracts\Auth\UserProvider...
return new ArrayUserProvider($app['config']['auth.credentials']);
});
}
}
4. In User.php (model):
class User extends Authenticatable
{
...
public function getAuthIdentifier()
{
return $this->email;
}
}
More Information:
For everyone who is interested, why there need to be the above stated additions:
When you login, the method login in Illuminate\Auth\SessionGuard is called. In this method you will find, that the identifier of the user is stored in the session with $this->updateSession($user->getAuthIdentifier()). Therefore we need to implement this method in our user model.
When you add $this->middleware('auth') in your controller, then the method authenticate() in Illuminate\Auth\Middleware\Authenticate is called. This again calls $this->auth->guard($guard)->check() to check, whether a user is authenticated. The check() method only tests, that there exists a user in the session (see Illuminate\Auth\GuardHelpers). It does this by calling the user() method of the guard, which in this case is again SessionGuard. In the user() method, the user is retrieved by taking the id stored in the session and calling retrieveById to get the user.

Override laravel 4's authentication methods to use custom hasing function

I have a table in my database with users. Their password are generated with my own custom hashing function.
How do i override the Authentication methods in laravel 4 to use my own hash class?
This is what I have been trying to do:
class CustomUserProvider implements Illuminate\Auth\UserProviderInterface {
public function retrieveByID($identifier)
{
return $this->createModel()->newQuery()->find($identifier);
}
public function retrieveByCredentials(array $credentials)
{
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a user, return it in a
// Eloquent User "model" that will be utilized by the Guard instances.
$query = $this->createModel()->newQuery();
foreach ($credentials as $key => $value)
{
if ( ! str_contains($key, 'password')) $query->where($key, $value);
}
return $query->first();
}
public function validateCredentials(Illuminate\Auth\UserInterface $user, array $credentials)
{
$plain = $credentials['password'];
return $this->hasher->check($plain, $user->getAuthPassword());
}
}
class CodeIgniter extends Illuminate\Auth\Guard {
}
App::bind('Illuminate\Auth\UserProviderInterface', 'CustomUserProvider');
Auth::extend('codeigniter', function()
{
return new CodeIgniter( App::make('CustomUserProvider'), App::make('session'));
});
When I run the Auth::attempt method I get this error:
ErrorException: Warning: Illegal offset type in isset or empty in G:\Dropbox\Workspaces\www\video\vendor\laravel\framework\src\Illuminate\Foundation\Application.php line 352
This is how ended up solving the problem:
libraries\CustomHasherServiceProvider.php
use Illuminate\Support\ServiceProvider;
class CustomHasherServiceProvider extends ServiceProvider {
public function register()
{
$this->app->bind('hash', function()
{
return new CustomHasher;
});
}
}
libraries\CustomHasher.php
class CustomHasher implements Illuminate\Hashing\HasherInterface {
private $NUMBER_OF_ROUNDS = '$5$rounds=7331$';
public function make($value, array $options = array())
{
$salt = uniqid();
$hash = crypt($password, $this->NUMBER_OF_ROUNDS . $salt);
return substr($hash, 15);
}
public function check($value, $hashedValue, array $options = array())
{
return $this->NUMBER_OF_ROUNDS . $hashedValue === crypt($value, $this->NUMBER_OF_ROUNDS . $hashedValue);
}
}
And then I replaced 'Illuminate\Hashing\HashServiceProvider' with 'CustomHasherServiceProvider' in the providers array in app/config/app.php
and added "app/libraries" to autoload classmap in composer.json
#vFragosop was on the right path with extending Auth.
There are a couple of ways to skin the cat and here is how I would do that without replacing the default Hasher class:
Include in your app/routes.php or wherever:
use Illuminate\Auth\Guard;
Auth::extend("eloquent", function() {
return new Guard(
new \Illuminate\Auth\EloquentUserProvider(new CustomHasher(), "User"),
App::make('session.store')
);
});
Create and autoload a CustomHasher class (i.e., app/libraries/CustomHasher.php):
class CustomHasher extends Illuminate\Hashing\BcryptHasher {
public function make($value, array $options = array())
{
...
}
public function check($value, $hashedValue, array $options = array())
{
...
}
}
That's it.
Warning: I can't ensure this is works out of the box and there may be a few gotchas here and there. Keep in mind Laravel 4 is still on development. Wish I could provide a more precise answer, but codebase is still going through many changes and not everything is properly documented. Anyway, you are looking for something like this:
// on config/auth.php
'driver' => 'custom'
// on start/global.php
Auth::extend('custom', function() {
// CustomUserProvider is your custom driver and should
// implement Illuminate\Auth\UserProviderInterface;
return new Guard(new CustomUserProvider, App::make('session'));
});
If this doesn't give you enough information to start, you should be able to figure it out by taking a look at those classes below:
EloquentUserProvider and DatabaseUserProvider
These classes are the currently supported authentication drivers. They should guide you on how to create your CustomUserProvider (or any name you like really).
Manager
This is the base class for anything that accepts custom drivers (including the AuthManager). It provides the methods for registering them like you do in Laravel 3.
This was the top result on Google, but these answers are insufficient for anyone on Laravel 5. Even the documentation doesn't suffice.
I've successfully replaced the Hasher for only the UserProvider. The rest of my application continues to use the very nice BcryptHasher, while user authentication uses a custom hasher. To do this, I had to study these answers, the documentation, and Laravel's source code itself. Here's what I found. Hopefully I can save someone else's full head of hair. Feel free to crosspost this to a question about Laravel 5.
First, create your custom hasher, if you haven't already. Place it wherever you'd like.
class MyCustomHasher implements Hasher {
public function make($value, array $options = []) {
return md5( $value ); // PLEASE DON'T USE MD5!
}
public function check($value, $hashedValue, array $options = []) {
if (strlen($hashedValue) === 0) {
return false;
}
return $hashedValue === $this->make($value);
}
public function needsRehash($hashedValue, array $options = []) {
return false;
}
}
Edit any registered ServiceProvider as follows...
class AppServiceProvider extends ServiceProvider {
public function boot() {
Auth::provider('eloquentCustom', function ($app, $config) {
return new EloquentUserProvider(new MyCustomHasher(), $config['model']);
});
}
}
You can replace 'eloquentCustom' with whatever you'd prefer.
Finally, edit your config/auth.php to use your custom provider. Here are the relevant parts...
return [
// ...
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
// ...
],
// ...
'providers' => [
'users' => [
'driver' => 'eloquentCustom', // <--- This is the only change
'model' => App\User::class,
],
// ...
],
// ...
];
Here's a little explanation, because I can't believe how obscure this was.
As you may expect, authentication is configured with config/auth.php. There are two key parts: Guards and Providers. I haven't yet bothered to learn exactly what guards do, but they seem to enforce authentication requirements. Providers are responsible for providing the necessary information to the guards. Therefore, a Guard requires a Provider. You can see that, in the default configuration, guards.web.provider is mapped to providers.users.
Laravel provides two implementations of UserProvider by default: EloquentUserProvider and DatabaseUserProvider. These correspond to the two possible values for providers.users.driver: eloquent and database, respectively. Normally, the eloquent option is chosen. EloquentUserProvider needs a Hasher, so Laravel gives it whatever the standard implementation is (ie. BcryptHasher). We override this behavior by creating our own "driver" for instantiating the Provider.
Auth is our friendly neighborhood facade. It is backed by the AuthManager. The often suggested Auth::extend() method expects a Guard (contrary to what the documentation might suggest). We have no need to mess with the Guard. Instead, we can use Auth::provider() which basically does the same thing as extend(), except it expects a Provider. So we provide a function to create our own instance of a EloquentUserProvider, giving it our custom Hasher (eg. MyCustomHasher). We also include a driver "name" that can be used in the config file.
Now back to the config file. That driver name that we just created is now a valid value for providers.users.driver. Set it there and you're good to go!
I hope this all makes sense and is useful for someone!

Categories