Symfony 6 json_login don't authenticate user - json

I have running a simple symfony/skeleton.
Basically I followed the documentation on "json_login", however Symfony does not authenticate the user when the login route is called. I test via Thunder Client in VSC.
I can call the /login route method, but the user object is basically NULL. In the meantime I have also tried to use my own authenticator, however this has not brought me any further in all variations. I have first tested with Symfony 6.1., last on version 6.2..
I would be very grateful for a tip or a link to a working tutorial. Thanks
I have already created a user entity via make:user which has the following structure:
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
private ?string $email = null;
#[ORM\Column]
private array $roles = [];
/**
* #var string The hashed password
*/
#[ORM\Column]
private ?string $password = null;
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->email;
}
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #see PasswordAuthenticatedUserInterface
*/
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
}
Then I created a controller for the /login and /logout routes with the following structure:
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Entity\User;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
class UserLoginController extends AbstractController
{
#[Route('/login', name: 'app_login')]
public function index(#[CurrentUser] ?User $user): Response
{
if (null === $user) {
return $this->json([
'message' => 'missing credentials',
], Response::HTTP_UNAUTHORIZED);
}
return $this->json([
'message' => 'Welcome to your new controller!',
'path' => 'src/Controller/ApiLoginController.php',
'user' => $user->getUserIdentifier()
]);
}
}
And my configuration for it looks like this:
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: "auto"
providers:
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
json_login:
check_path: /app_login
username_path: email
password_path: password
lazy: true
provider: app_user_provider
#custom_authenticator: App\Security\UserAuthenticator
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }

Related

yii2 disable inbuilt authentication

I have written my own Users model which extends ActiveRecord and implements IdentityInterface and also defined tablename. and implemented all methods.
Users.php
public static function tableName()
{
return 'users';
}
// other methods are also present like rules() , getId() etc.
UserController.php
public function actionLogin()
{
$this->layout = 'blank';
if (!Yii::$app->myuser->isGuest) {
return 'hello';
}
$model = new UserLoginForm();
if ($model->load(Yii::$app->request->post()) && $model->login()) {
return $this->redirect(['user/view',
'id' => $model->getUser()->id,
]);
} else {
$model->password = '';
return $this->render('login', [
'model' => $model,
]);
}
}
UserLoginForm.php
<?php
namespace backend\models;
use Yii;
use yii\base\Model;
class UserLoginForm extends Model
{
public $username;
public $password;
public $rememberMe = true;
private $_user;
/**
* {#inheritdoc}
*/
public function rules()
{
return [
// username and password are both required
[['username', 'password'], 'required'],
// rememberMe must be a boolean value
['rememberMe', 'boolean'],
// password is validated by validatePassword()
['password', 'validatePassword'],
];
}
/**
* Validates the password.
* This method serves as the inline validation for password.
*
* #param string $attribute the attribute currently being validated
* #param array $params the additional name-value pairs given in the rule
*/
public function validatePassword($attribute, $params)
{
if (!$this->hasErrors()) {
$user = $this->getUser();
if (!$user || !$user->validatePassword($this->password)) {
$this->addError($attribute, 'Incorrect username or password.');
}
}
}
/**
* Logs in a user using the provided username and password.
*
* #return bool whether the user is logged in successfully
*/
public function login()
{
if ($this->validate()) {
return Yii::$app->myuser->login($this->getUser());
}
return false;
}
/**
* Finds user by [[username]]
*
* #return Users|null
*/
public function getUser()
{
if ($this->_user === null) {
$this->_user = Users::findByUsername($this->username);
}
return $this->_user;
}
}
And in backend/config/main.php
'myuser' => [
'class' => 'yii\web\User',
'identityClass' => 'backend\models\Users',
'enableAutoLogin' => true,
'identityCookie' => ['name' => '_identity-backend_user', 'httpOnly' => true],
],
But after successful login, i get following error
The table does not exist: {{%user}}
I found that it is calling common/models/User.php class which by default present in advanced template. But why is it calling this class ? I want to use my own Users.php model. Please somone help me to fix this problem.
The class used for authentication is determined by the user application component, according to the authentication section of the Yii2 guide:
The user application component manages the user authentication status. It requires you to specify an identity class which contains the actual authentication logic. In the following application configuration, the identity class for user is configured to be app\models\User whose implementation is explained in the next subsection:
When you configure a new myuser component, the framework still uses the user component as it is configured by default, update your code, so it overwrites the user component, instead of creating a new myuser one.
'user' => [
// Points to your custom class
'identityClass' => 'backend\models\Users',
'enableAutoLogin' => true,
'identityCookie' => ['name' => '_identity-backend', 'httpOnly' => true],
],

How to rename a datatable column name and value to island names that is generated from infyom laravel generator?

I have two models for Island and Fisher. I want to use datatable to display island name instead of (island_id) fisher_first_name and fisher_last_name
As displayed in the datatable
Island Id Fisher First Name Fisher Last Name
1 Dovecot Imap
2 Jon Tim
These are my two models relationships
public function fishers(){
return $this->hasMany(Fisher::Class);
}
public function island(){
return $this->belongsTo(Island::Class,'island_id');
}
This is the getColumns fuction from FisherDatatable that I need to use to change the island_id to island_names
protected function getColumns()
{
return [
'island_id'
'fisher_first_name',
'fisher_last_name',
];
}
This is also an extract from a FisherDatatable to show Island and Fisher relationship
public function query(Fisher $model)
{
return $model->newQuery()->with(['island']);
}
This is my Controller
namespace App\Http\Controllers;
use App\DataTables\FisherDataTable;
use App\Http\Requests;
use App\Http\Requests\CreateFisherRequest;
use App\Http\Requests\UpdateFisherRequest;
use App\Repositories\FisherRepository;
use App\Models\Island;
use Flash;
use App\Http\Controllers\AppBaseController;
use Response;
class FisherController extends AppBaseController
{
/** #var FisherRepository */
private $fisherRepository;
public function __construct(FisherRepository $fisherRepo)
{
$this->fisherRepository = $fisherRepo;
}
/**
* Display a listing of the Fisher.
*
* #param FisherDataTable $fisherDataTable
* #return Response
*/
public function index(FisherDataTable $fisherDataTable)
{
return $fisherDataTable->render('fishers.index');
}
/**
* Show the form for creating a new Fisher.
*
* #return Response
*/
public function create()
{
$islands = Island::pluck('island_name','id');
return view('fishers.create')->with('islands',$islands);
}
/**
* Store a newly created Fisher in storage.
*
* #param CreateFisherRequest $request
*
* #return Response
*/
public function store(CreateFisherRequest $request)
{
$input = $request->all();
$fisher = $this->fisherRepository->create($input);
Flash::success('Fisher saved successfully.');
return redirect(route('fishers.index'));
}
/**
* Display the specified Fisher.
*
* #param int $id
*
* #return Response
*/
public function show($id)
{
$fisher = $this->fisherRepository->find($id);
if (empty($fisher)) {
Flash::error('Fisher not found');
return redirect(route('fishers.index'));
}
return view('fishers.show')->with('fisher', $fisher);
}
/**
* Show the form for editing the specified Fisher.
*
* #param int $id
*
* #return Response
*/
public function edit($id)
{
$fisher = $this->fisherRepository->find($id);
$islands = Island::pluck('island_name','id');
if (empty($fisher)) {
Flash::error('Fisher not found');
return redirect(route('fishers.index'));
}
return view('fishers.edit')
->with('fisher', $fisher)
-> with('islands', $islands);
}
/**
* Update the specified Fisher in storage.
*
* #param int $id
* #param UpdateFisherRequest $request
*
* #return Response
*/
public function update($id, UpdateFisherRequest $request)
{
$fisher = $this->fisherRepository->find($id);
if (empty($fisher)) {
Flash::error('Fisher not found');
return redirect(route('fishers.index'));
}
$fisher = $this->fisherRepository->update($request->all(), $id);
Flash::success('Fisher updated successfully.');
return redirect(route('fishers.index'));
}
/**
* Remove the specified Fisher from storage.
*
* #param int $id
*
* #return Response
*/
public function destroy($id)
{
$fisher = $this->fisherRepository->find($id);
if (empty($fisher)) {
Flash::error('Fisher not found');
return redirect(route('fishers.index'));
}
$this->fisherRepository->delete($id);
Flash::success('Fisher deleted successfully.');
return redirect(route('fishers.index'));
}
}
just need to add name, title and data array to your getColumns methos
protected function getColumns()
{
return [
['name'=>'dropdown_label','title'=>'new name of label','data'=>"dropdown_label"],
['name'=>'dropdown_value','title'=>'new name of dropdwon value','data'=>"dropdown_value"],
'active'
];
}

Symfony 4 - complex form submited with json

I try to build form with nested entities.
src/Entity/Company
/**
* #ORM\ManyToOne(targetEntity="App\Entity\CompanyAddress", inversedBy="company")
* #Serializer\Groups({"company"})
*/
private $addresses;
/**
* #ORM\OneToOne(targetEntity="App\Entity\CompanyAddress")
* #ORM\JoinColumn(name="main_address")
*
* #Assert\NotBlank()
* #Serializer\Groups({"company"})
*/
private $mainAddress;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User")
* #ORM\JoinColumn(name="owner", onDelete="SET NULL", nullable=true)
* #Serializer\Groups({"company"})
*
*/
private $owner;
public function __construct()
{
$this->addresses = new ArrayCollection();
$this->accountants = new ArrayCollection();
}
/**
* #return array
*/
public function getAddresses()
{
return $this->addresses->toArray();
}
/**
* #param $addresses
* #return Company
*/
public function setAddresses($addresses): self
{
$this->addresses = $addresses;
}
/**
* #param CompanyAddress $address
* #return Company
*/
public function addAddress(CompanyAddress $address): self
{
if ($this->addresses->contains($address)) return $this;
$this->addresses->add($address);
return $this;
}
/**
* #param CompanyAddress $address
* #return Company
*/
public function removeAddress(CompanyAddress $address): self
{
if ($this->addresses->contains($address)) {
$this->addresses->removeElement($address);
// $address->setCompany(null);
}
return $this;
}
/**
* #return mixed
*/
public function getMainAddress()
{
return $this->mainAddress;
}
/**
* #param CompanyAddress $address
* #return Company
*/
public function setMainAddress(?CompanyAddress $address): self
{
$this->mainAddress = $address;
return $this;
}
/**
* #return User
*/
public function getOwner(): ?User
{
return $this->owner;
}
/**
* #param User|null $owner
* #return Company
*/
public function setOwner(?User $owner): self
{
$this->owner = $owner;
return $this;
}
// and other entity code
src/Entity/CompanyAddress.php
/**
* #ORM\OneToMany(targetEntity="App\Entity\Company", mappedBy="addresses", orphanRemoval=true)
*/
private $company;
/**
* #return Company
*/
public function getCompany(): Company
{
return $this->company;
}
/**
* #param Company $company
* #return CompanyAddress
*/
public function setCompany(?Company $company): self
{
$this->company = $company;
return $this;
}
// some other code
Now I build Form
src/Form/CompanyType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('shortName')
->add('nip')
->add('regon')
->add('description')
->add('addresses', CollectionType::class, ['entry_type' => CompanyAddressType::class, 'allow_add' => true])
->add('mainAddress', CompanyAddressType::class)
->add('accountants', CollectionType::class, ['entry_type' => CompanyAccountantType::class, 'allow_add' => true])
->add('owner', UserCompanyType::class, ['empty_data' => null])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Company::class,
]);
}
src/Form/CompanyAddressType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('country')
->add('city')
->add('street')
->add('house')
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => CompanyAddress::class,
]);
}
src/Controller/CompanyController.php
public function new(Request $request, CompanyService $cs)
{
// $this->denyAccessUnlessGranted('new');ump($request->getContent());
$company = new Company();
$form = $this->createForm(CompanyType::class, $company);
$form->submit(json_decode($request->getContent(), true));
if (!$form->isValid()) {
return $this->json([
'message' => 'Data validation error',
'errors' => $this->normalizeFormErrors($form)
], Response::HTTP_UNPROCESSABLE_ENTITY);
}
try {
$company = $cs->addCompany($company);
} catch (\Exception $e) {
return JsonResponse::create(['error' => $e->getMessage()], 406);
}
return JsonResponse::fromJsonString($cs->getSerializer()->serialize($company, ['id', 'company']));
}
and Service src/Services/CompanyService.php
public function addCompany(Company $company)
{
if ($this->companyRepository->findOneByNipOrRegon(['nip' => $company->getNip(), 'regon' => $company->getRegon()]))
throw new \Exception($this->translator->trans('Company with given NIP or REGON exists'));
try {
$this->em->persist($company);
$this->em->flush();
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
return $company;
}
Now I'm sending json data
{
"name":"Long Company Name",
"shortName":"Short Name",
"nip":"8888888",
"regon":"1111111",
"description":"Description",
"addresses": [ {
"city":"First City",
"street":"No street Name",
"house":"44",
"country":"Country"
}, {
"country":"Country",
"city":"Another City",
"street":"",
"house":"11"
}],
"mainAddress": {
"country":"Main Country",
"city":"Main City",
"street":"Main Street",
"house":"1"
},
"accountants": [],
"owner": {
"id":1
}
}
And I got error "Expected value of type \"App\\Entity\\CompanyAddress\" for association field \"App\\Entity\\Company#$addresses\", got \"Doctrine\\Common\\Collections\\ArrayCollection\" instead."
When I send empty array of adresses and send id as owner my owner in company is null :/ isn't get from database :/ When I remove option "empty_data" I get error "Could not determine access type for property "id" in class "App\Entity\User"
I'd like to add addresses from entity Comapany when I add Company, also I'd like to remove address from Company Entity.
How to create Entities from that Form? Or what fuctions should I add to Company entity?
See here :
/**
* #ORM\ManyToOne(targetEntity="App\Entity\CompanyAddress", inversedBy="company")
* #Serializer\Groups({"company"})
*/
private $addresses;
Your property is set as ManyToOne, which means in this case that it expects an instance of CompanyAddress. Yet you're giving it an array of CompanyAddress items.
I think this relation should be either OneToMany ( one company can have multiple adresses) or ManyToMany ( One company can have multiple adresses, which can be shared with other companies )
If you're not familiar with ManyToMany relationships, a little tutorial search should do the trick.

Restricting controller action to creator of post in Yii2

Is there an easy way to restrict a controller action to the owner/creator of the post without using full blown RBAC?
Right now I'm doing this for every controller:
public function actionUpdate( $id ) {
$model = $this->findModel( $id );
if ( $model->user_id != Yii::$app->user->identity->id ) {
throw new NotFoundHttpException( 'The requested page does not exist.' );
}
}
But I think there must be a better way to restrict certain controllers to the users who created the $model thats being edited.
1) The recommended way is to use RBAC and rules. It's covered well in official docs in according dedicated section.
Example of rule that checks if author id matches current user id passed via params:
namespace app\rbac;
use yii\rbac\Rule;
/**
* Checks if authorID matches user passed via params
*/
class AuthorRule extends Rule
{
public $name = 'isAuthor';
/**
* #param string|integer $user the user ID.
* #param Item $item the role or permission that this rule is associated with
* #param array $params parameters passed to ManagerInterface::checkAccess().
* #return boolean a value indicating whether the rule permits the role or permission it is associated with.
*/
public function execute($user, $item, $params)
{
return isset($params['post']) ? $params['post']->createdBy == $user : false;
}
}
Then you need to tie it with existing permission (can be done in migration or with extensions):
$auth = Yii::$app->authManager;
// add the rule
$rule = new \app\rbac\AuthorRule;
$auth->add($rule);
// add the "updateOwnPost" permission and associate the rule with it.
$updateOwnPost = $auth->createPermission('updateOwnPost');
$updateOwnPost->description = 'Update own post';
$updateOwnPost->ruleName = $rule->name;
$auth->add($updateOwnPost);
// "updateOwnPost" will be used from "updatePost"
$auth->addChild($updateOwnPost, $updatePost);
// allow "author" to update their own posts
$auth->addChild($author, $updateOwnPost);
Then you can check if you user can update post like this:
use yii\web\ForbiddenHttpException;
use Yii;
public function actionUpdate($id)
{
$model = $this->findModel($id);
if (!Yii::$app->user->can('updatePost', ['post' => $model])) {
throw new ForbiddenHttpException('You are not allowed to edit this post');
}
...
}
Also note that in case you found model first and user has no access to edit it, logically it's better to throw 403 Forbidden exception rather than 404, since it's found, but not allowed for editing.
Don't forget to include rule like that in AccessControl behavior:
[
'allow' => true,
'actions' => ['update'],
'roles' => ['#'],
],
It means that update action of this controller can be only accessed by authorized users excluding guests.
2) If for some reason you don't want to use RBAC, you can use your approach:
use yii\web\ForbiddenHttpException;
public function actionUpdate($id)
{
$model = $this->findModel($id);
if ($model->user_id != Yii::$app->user->id ) {
throw new ForbiddenHttpException('You are not allowed to edit this post.');
}
...
}
To improve this you can abstract from this check by moving this logic to helper method:
namespace app\posts\components;
use Yii;
class PostPermission
{
/**
* #param $model Post
* #return boolean
*/
public static function allowedToUpdate($model)
{
return $model->user_id = Yii:$app->user->id;
}
}
Then call it like that:
use app\posts\components\PostPermission;
use yii\web\ForbiddenHttpException;
if (!PostPermission::allowedToUpdate($model) {
throw new ForbiddenHttpException('You are not allowed to edit this post.');
}
It's just an example, method doesn't have to be static, you can construct instance using $model.
You can just directly create method in Post model, but it's better to not pollute model with such logic.
3) Another alternative that I can advise is to restrict scope initially to current user when finding model:
use yii\web\NotFoundHttpException;
/**
* #param integer $id
* #return Post
* #throws NotFoundHttpException
*/
protected function findModel($id)
{
$model = Post::find(['id'=> $id, 'user_id' => Yii::$app->user->id])->one();
if ($model) {
return $model;
} else {
throw new NotFoundHttpException('This post does not exist.');
}
}
This can be improved for site administrators:
use yii\web\NotFoundHttpException;
/**
* #param integer $id
* #return Post
* #throws NotFoundHttpException
*/
protected function findModel($id)
{
$query = Post::find()->where(['id' => $id]);
if (!Yii::$app->user->is_admin) { // replace with your own check
$query->andWhere(['user_id' => Yii::$app->user->id]);
}
$model = $query->one();
if ($model) {
return $model;
} else {
throw new NotFoundHttpException('This post does not exist.');
}
}
Then you only write:
public function actionUpdate($id)
{
$model = $this->findModel($id);
...
}
That way in both cases (model not found and not allowed for editing by current user), 404 Not Found exception will be raised. From other side, nothing is wrong with that, because technically for this user this model does not exist (since he is not author of it).
We can use
AccessControlFilter
for restricting controller action instead of RBAC. This below code will give access to the actionUpdate if it is only pass the denyCallback.
use yii\filters\AccessControl;
class SiteController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['update','delete'],
'rules' => [
[
'actions' => ['update'],
'allow' => false,
'denyCallback' => function ($rule, $action) { //PHP callable that should be called when this rule will deny the access.
//Write your logic here to deny the action
throw new \Exception('You are not allowed to access this page');
}
],
],
],
];
}
public function actionUpdate()
{
return $this->render('update');
}
}
For your reference https://github.com/yiisoft/yii2/blob/master/docs/guide/security-authorization.md

unserialize pdo mysql error - invalid data source name

I have these classes below for my online store.
This super class holds all common methods used by the child classes.
class grandpa
{
public function test1($string)
{
return $string;
}
}
So do the PDO connection,
class database_pdo extends grandpa
{
protected $connection = null;
protected $dsn,$username,$password;
public function __construct($dsn,$username,$password)
{
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->get_connection();
}
public function get_connection()
{
try
{
$this->connection = new PDO($this->dsn, $this->username, $this->password, array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch (PDOException $e)
{
$this->get_error($e);
}
}
public function __sleep()
{
return array('dsn', 'username', 'password');
}
public function __wakeup()
{
$this->get_connection();
}
public function get_error($e)
{
$this->connection = null;
die($e->getMessage());
}
public function __destruct()
{
$this->connection = null;
}
}
I have this class extend from pdo for other common methods that require pdo connection,
class papa extends database_pdo
{
protected $connection = null;
public function __construct($connection)
{
$this->connection = $connection;
}
public function test2($string)
{
return $string;
}
}
The child classes,
class kido_1 extends papa
{
public function __construct($connection)
{
parent::__construct($connection);
}
public function test3($string)
{
return $string;
}
}
How it use the classes above,
# Host used to access DB.
define('DB_HOST', 'localhost');
# Username used to access DB.
define('DB_USER', 'xxx');
# Password for the username.
define('DB_PASS', 'xxx');
# Name of your databse.
define('DB_NAME', 'xxx');
# Data source name.
define('DSN', 'mysql:host='.DB_HOST.';dbname='.DB_NAME);
$connection = new database_pdo(DSN,DB_USER,DB_PASS);
$kido = new kido($connection);
$_SESSION['cart'] = serialize($kido);
$kido = unserialize($_SESSION['cart']);
print_r($kido->test3('hello'));
I get this error,
invalid data source name
It is caused by unserialize() that I need it for my cart data...
How can I fix this? Or a better way of rewrite these classes?
Your papa::connection is a PDO object. Therefore, when you are trying to serialize $kido, you are trying to serialize a resource, which is not possible.
Try adding a $this->connection = null; in your database_pdo::__sleep() method.
a solution I think...
class papa extends grandpa
{
protected $connection = null;
public function __construct($connection)
{
$this->connection = $connection;
}
public function test2($string)
{
return $string;
}
}