Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use user entity provider #1961

Closed
lxregistry opened this issue Oct 22, 2023 · 3 comments
Closed

How to use user entity provider #1961

lxregistry opened this issue Oct 22, 2023 · 3 comments

Comments

@lxregistry
Copy link

lxregistry commented Oct 22, 2023

Q A
Bug? no
New Feature? no
Support question? yes
Version 2.x

Actual Behavior

What is the actual behavior?
The bundle needs a service that is able to load users based on the user response of the oauth endpoint. If you have a custom service it should implement the interface: HWI\Bundle\OAuthBundle\Security\Core\User\OAuthAwareUserProviderInterface.

could you provide more documentation about this: A) Have a user provider that implements OAuthAwareUserProviderInterface
or examples,

Expected Behavior

What is the behavior you expect?

Steps to Reproduce

What are the steps to reproduce this bug? Please add code examples,
screenshots or links to GitHub repositories that reproduce the problem.

Possible Solutions

If you have already ideas how to solve the issue, add them here.
(remove this section if not needed)

@maciekpaprocki
Copy link

I agree. I am struggling to find documentation on how to actually do that. Seems like fosuserbundle is really not needed anymore.

@maciekpaprocki
Copy link

Actually, i implemented that, but there's only basic testing on my side for now.

Entity

<?php

namespace App\Entity\User;

use App\Repository\User\UserOAuthRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: UserOAuthRepository::class)]
class UserOAuth
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 40)]
    private ?string $provider = null;

    #[ORM\Column(length: 255)]
    private ?string $identifier = null;

    #[ORM\Column(length: 255, nullable: true)]
    private ?string $accessToken = null;

    #[ORM\Column(length: 255, nullable: true)]
    private ?string $refreshToken = null;

    #[ORM\ManyToOne(inversedBy: 'userAuths')]
    #[ORM\JoinColumn(nullable: false)]
    private ?User $user = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getProvider(): ?string
    {
        return $this->provider;
    }

    public function setProvider(string $provider): static
    {
        $this->provider = $provider;

        return $this;
    }

    public function getIdentifier(): ?string
    {
        return $this->identifier;
    }

    public function setIdentifier(?string $identifier): static
    {
        $this->identifier = $identifier;

        return $this;
    }

    public function getAccessToken(): ?string
    {
        return $this->accessToken;
    }

    public function setAccessToken(?string $accessToken): static
    {
        $this->accessToken = $accessToken;

        return $this;
    }

    public function getRefreshToken(): ?string
    {
        return $this->refreshToken;
    }

    public function setRefreshToken(?string $refreshToken): static
    {
        $this->refreshToken = $refreshToken;

        return $this;
    }

    public function getUser(): ?User
    {
        return $this->user;
    }

    public function setUser(?User $user): static
    {
        $this->user = $user;

        return $this;
    }
}
<?php

declare(strict_types=1);

namespace App\Security;

use App\Entity\User\User;
use App\Entity\User\UserOAuth;
use App\Repository\User\UserOAuthRepository;
use App\Repository\User\UserRepository;
use BadMethodCallException;
use Doctrine\ORM\EntityManagerInterface;
use HWI\Bundle\OAuthBundle\Connect\AccountConnectorInterface;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthAwareUserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;

use Webmozart\Assert\Assert;
use function sha1;
use function substr;


class OAuthUserProvider implements AccountConnectorInterface, OAuthAwareUserProviderInterface
{
    public function __construct(
        private UserRepository         $userRepository,
        private UserOAuthRepository    $userAuthRepository,
        private EntityManagerInterface $em,
    )
    {
    }

    public function connect(UserInterface $user, UserResponseInterface $response): void
    {
        $this->updateUserByOAuthUserResponse($user, $response);
    }

    public function loadUserByOAuthUserResponse(UserResponseInterface $response)
    {

        $oauth = $this->userAuthRepository->findOneBy([
            'provider' => $response->getResourceOwner()->getName(),
            'identifier' => $response->getUsername(),
        ]);

        if ($oauth instanceof UserOAuth) {
            return $oauth->getUser();
        }

        if (null !== $response->getEmail()) {
            $user = $this->userRepository->findOneByEmail($response->getEmail());
            if (null !== $user) {
                return $this->updateUserByOAuthUserResponse($user, $response);
            }

            return $this->createUserByOAuthUserResponse($response);
        }

        throw new BadMethodCallException('Email is null or not provided');

    }

    private function createUserByOAuthUserResponse(UserResponseInterface $response): User
    {

        $user = new User();


        $user->setEmail($response->getEmail());
        $user->setPassword(substr(sha1($response->getAccessToken()), 0, 20));

        return $this->updateUserByOAuthUserResponse($user, $response);
    }


    private function updateUserByOAuthUserResponse(UserInterface $user, UserResponseInterface $response): User
    {
        /** @var User $user */
        Assert::isInstanceOf($user, User::class);


        $oauth = new UserOAuth();
        $oauth->setIdentifier($response->getUsername());
        $oauth->setProvider($response->getResourceOwner()->getName());
        $oauth->setAccessToken($response->getAccessToken());
        $oauth->setRefreshToken($response->getRefreshToken());

        $user->addUserOAuth($oauth);
        $this->em->persist($user);
        $this->em->persist($oauth);
        $this->em->flush();

        return $user;
    }
}

And original User entity with some extra fields


<?php

namespace App\Entity\User;

use App\Entity\Product\Product;
use App\Repository\User\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]
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 = [];

    #[ORM\Column]
    private ?string $password = null;

    #[ORM\OneToMany(mappedBy: 'seller', targetEntity: Product::class, orphanRemoval: true)]
    private Collection $products;

    #[ORM\Column(type: 'boolean')]
    private $isVerified = false;

    #[ORM\ManyToMany(targetEntity: Product::class)]
    private Collection $basket;


    #[ORM\JoinTable(name: 'user_likes')]
    #[ORM\ManyToMany(targetEntity: Product::class, inversedBy: 'likingUsers')]
    private Collection $likes;

    #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserOAuth::class)]
    private Collection $userOAuths;

    public function __construct()
    {
        $this->products = new ArrayCollection();
        $this->basket = new ArrayCollection();
        $this->likes = new ArrayCollection();
        $this->userOAuths = new ArrayCollection();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(string $email): static
    {
        $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): static
    {
        $this->roles = $roles;

        return $this;
    }

    /**
     * @see PasswordAuthenticatedUserInterface
     */
    public function getPassword(): string
    {
        return $this->password;
    }

    public function setPassword(string $password): static
    {
        $this->password = $password;

        return $this;
    }

    /**
     * @see UserInterface
     */
    public function eraseCredentials(): void
    {
        // If you store any temporary, sensitive data on the user, clear it here
        // $this->plainPassword = null;
    }

    /**
     * @return Collection<int, Product>
     */
    public function getProducts(): Collection
    {
        return $this->products;
    }

    public function addProduct(Product $product): static
    {
        if (!$this->products->contains($product)) {
            $this->products->add($product);
            $product->setSeller($this);
        }

        return $this;
    }

    public function removeProduct(Product $product): static
    {
        if ($this->products->removeElement($product)) {
            // set the owning side to null (unless already changed)
            if ($product->getSeller() === $this) {
                $product->setSeller(null);
            }
        }

        return $this;
    }

    public function isVerified(): bool
    {
        return $this->isVerified;
    }

    public function setIsVerified(bool $isVerified): static
    {
        $this->isVerified = $isVerified;

        return $this;
    }

    /**
     * @return Collection<int, Product>
     */
    public function getBasket(): Collection
    {
        return $this->basket;
    }

    public function addBasket(Product $basket): static
    {
        if (!$this->basket->contains($basket)) {
            $this->basket->add($basket);
        }

        return $this;
    }

    public function removeBasket(Product $basket): static
    {
        $this->basket->removeElement($basket);

        return $this;
    }

    /**
     * @return Collection<int, Product>
     */
    public function getLikes(): Collection
    {
        return $this->likes;
    }

    public function addLike(Product $like): static
    {
        if (!$this->likes->contains($like)) {
            $this->likes->add($like);
        }

        return $this;
    }

    public function removeLike(Product $like): static
    {
        $this->likes->removeElement($like);

        return $this;
    }

    /**
     * @return Collection<int, UserOAuth>
     */
    public function getUserOAuths(): Collection
    {
        return $this->userOAuths;
    }

    public function addUserOAuth(UserOAuth $userAuth): static
    {
        if (!$this->userOAuths->contains($userAuth)) {
            $this->userOAuths->add($userAuth);
            $userAuth->setUser($this);
        }

        return $this;
    }

    public function removeUserOAuth(UserOAuth $userAuth): static
    {
        if ($this->userOAuths->removeElement($userAuth)) {
            // set the owning side to null (unless already changed)
            if ($userAuth->getUser() === $this) {
                $userAuth->setUser(null);
            }
        }

        return $this;
    }
}

Seems to be working for now. Will see once things start expiring :)

Please don't consider that production code. I am not sure what I am really doing.

@stloyd
Copy link
Collaborator

stloyd commented Feb 16, 2024

As pointed in #1964, the documentation could do better job, there is an entity provider already, you can setup it like:

services:
    hwi_oauth.user.provider.entity:
        class: HWI\Bundle\OAuthBundle\Security\Core\User\EntityUserProvider
        arguments:
            $class: App\Entity\User
            $properties:
                'facebook': 'facebook'
                'google': 'google'
# security.yaml
security:
    firewalls:
        main:
            # ...
            oauth:
                #...
                oauth_user_provider:
                    service: hwi_oauth.user.provider.entity

@stloyd stloyd changed the title support request How to use user entity provider Feb 16, 2024
@stloyd stloyd closed this as completed Feb 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants