Mert Simsek
Posted on August 22, 2018
We are going to easily integrate with KnpUOAuth2ClientBundle for Symfony 4. You can read the detail about it, https://github.com/knpuniversity/oauth2-client-bundle. Today, we integrate Google Oauth2 server but we can integrate other servers easily. For this, I adjust the composer package.
composer require knpuniversity/oauth2-client-bundle league/oauth2-google
After installation, I have defined an Entity named User. You can run this command.
bin/console make:entity
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $email;
/**
* @ORM\Column(type="string", length=255)
*/
private $fullname;
/**
* @ORM\Column(type="datetime")
*/
private $created_at;
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;
}
public function getFullname(): ?string
{
return $this->fullname;
}
public function setFullname(string $fullname): self
{
$this->fullname = $fullname;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->created_at;
}
public function setCreatedAt(\DateTimeInterface $created_at): self
{
$this->created_at = $created_at;
return $this;
}
/**
* Returns the roles granted to the user.
*
* <code>
* public function getRoles()
* {
* return array('ROLE_USER');
* }
* </code>
*
* Alternatively, the roles might be stored on a ``roles`` property,
* and populated in any number of different ways when the user object
* is created.
*
* @return (Role|string)[] The user roles
*/
public function getRoles()
{
return array('ROLE_USER');
}
/**
* Returns the password used to authenticate the user.
*
* This should be the encoded password. On authentication, a plain-text
* password will be salted, encoded, and then compared to this value.
*
* @return string The password
*/
public function getPassword()
{
return null;
}
/**
* Returns the salt that was originally used to encode the password.
*
* This can return null if the password was not encoded using a salt.
*
* @return string|null The salt
*/
public function getSalt()
{
return null;
}
/**
* Returns the username used to authenticate the user.
*
* @return string The username
*/
public function getUsername()
{
return $this->email;
}
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
return null;
}
}
Then, I am going to create two files in the project directory. First, I have created Security directory. The following lines mean UserProvider file.
<?php
/**
* Created by IntelliJ IDEA.
* User: mert
* Date: 12/18/17
* Time: 12:58 PM
*/
namespace App\Security;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\User;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class UserProvider implements UserProviderInterface
{
private $entityManager;
/**
* UserProvider constructor.
* @param EntityManagerInterface $entityManager
* @internal param Client $httpClient
* @internal param UserOptionService $userOptionService
* @internal param ProjectService $projectService
* @internal param SessionService $sessionService
* @internal param Session $session
* @internal param UserOptionService $userOptionsService
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* Loads the user for the given username.
*
* This method must throw UsernameNotFoundException if the user is not
* found.
*
* @param string $username The username
*
* @return UserInterface
*
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function loadUserByUsername($username)
{
return $this->entityManager->createQueryBuilder('u')
->where('u.email = :email')
->setParameter('email', $username)
->getQuery()
->getOneOrNullResult();
}
/**
* Refreshes the user.
*
* It is up to the implementation to decide if the user data should be
* totally reloaded (e.g. from the database), or if the UserInterface
* object can just be merged into some internal array of users/identity
* map.
*
* @param UserInterface $user
* @return UserInterface
*
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
}
return $user;
}
/**
* Whether this provider supports the given user class.
*
* @param string $class
*
* @return bool
*/
public function supportsClass($class)
{
return $class === 'App\Security\User';
}
}
From now on, I have a user provider. Right now, we can adjust the authenticator.
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use League\OAuth2\Client\Provider\GoogleUser;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* Created by IntelliJ IDEA.
* User: mert
* Date: 12/18/17
* Time: 12:00 PM
*/
class GoogleAuthenticator extends SocialAuthenticator
{
private $clientRegistry;
private $em;
private $router;
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router)
{
$this->clientRegistry = $clientRegistry;
$this->em = $em;
$this->router = $router;
}
public function supports(Request $request)
{
return $request->getPathInfo() == '/connect/google/check' && $request->isMethod('GET');
}
public function getCredentials(Request $request)
{
return $this->fetchAccessToken($this->getGoogleClient());
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
/** @var GoogleUser $googleUser */
$googleUser = $this->getGoogleClient()
->fetchUserFromToken($credentials);
$email = $googleUser->getEmail();
$user = $this->em->getRepository('App:User')
->findOneBy(['email' => $email]);
if (!$user) {
$user = new User();
$user->setEmail($googleUser->getEmail());
$user->setFullname($googleUser->getName());
$user->setCreatedAt(new \DateTime(date('Y-m-d H:i:s')));
$this->em->persist($user);
$this->em->flush();
}
return $user;
}
/**
* @return \KnpU\OAuth2ClientBundle\Client\OAuth2Client
*/
private function getGoogleClient()
{
return $this->clientRegistry
->getClient('google');
}
/**
* Returns a response that directs the user to authenticate.
*
* This is called when an anonymous request accesses a resource that
* requires authentication. The job of this method is to return some
* response that "helps" the user start into the authentication process.
*
* Examples:
* A) For a form login, you might redirect to the login page
* return new RedirectResponse('/login');
* B) For an API token authentication system, you return a 401 response
* return new Response('Auth header required', 401);
*
* @param Request $request The request that resulted in an AuthenticationException
* @param \Symfony\Component\Security\Core\Exception\AuthenticationException $authException The exception that started the authentication process
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function start(Request $request, \Symfony\Component\Security\Core\Exception\AuthenticationException $authException = null)
{
return new RedirectResponse('/login');
}
/**
* Called when authentication executed, but failed (e.g. wrong username password).
*
* This should return the Response sent back to the user, like a
* RedirectResponse to the login page or a 403 response.
*
* If you return null, the request will continue, but the user will
* not be authenticated. This is probably not what you want to do.
*
* @param Request $request
* @param \Symfony\Component\Security\Core\Exception\AuthenticationException $exception
*
* @return \Symfony\Component\HttpFoundation\Response|null
*/
public function onAuthenticationFailure(Request $request, \Symfony\Component\Security\Core\Exception\AuthenticationException $exception)
{
// TODO: Implement onAuthenticationFailure() method.
}
/**
* Called when authentication executed and was successful!
*
* This should return the Response sent back to the user, like a
* RedirectResponse to the last page they visited.
*
* If you return null, the current request will continue, and the user
* will be authenticated. This makes sense, for example, with an API.
*
* @param Request $request
* @param \Symfony\Component\Security\Core\Authentication\Token\TokenInterface $token
* @param string $providerKey The provider (i.e. firewall) key
*
* @return void
*/
public function onAuthenticationSuccess(Request $request, \Symfony\Component\Security\Core\Authentication\Token\TokenInterface $token, $providerKey)
{
// TODO: Implement onAuthenticationSuccess() method.
}
}
If I call the request to Google oauth2 servers, I need to set Google Client Id
and Google Secret Key
. When you read https://support.google.com/googleapi/answer/6158849?hl=en, you will learn, how you can get these keys. Then, you should enable Google+ API. You are going to Google+ API page on Google Cloud Platform and only click the Enable button. That's it.
I have these keys and I am going to define them in .env file like this.
# This file is a "template" of which env vars need to be defined for your application
# Copy this file to .env file for development, create environment variables when deploying to production
# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=9258a6c0e5c19d0d58a8c48bbc757491
#TRUSTED_PROXIES=127.0.0.1,127.0.0.2
#TRUSTED_HOSTS=localhost,example.com
###< symfony/framework-bundle ###
GOOGLE_CLIENT_ID=***
GOOGLE_CLIENT_SECRET=***
###> doctrine/doctrine-bundle ###
# Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
# Configure your db driver and server_version in config/packages/doctrine.yaml
DATABASE_URL=mysql://root:symf0ny@mysql:3306/individual-vocabulary
###< doctrine/doctrine-bundle ###
From now on, I can configure this bundle in yml files. I need security.yml file for this. I have changed like this.
# To get started with security, check out the documentation:
# https://symfony.com/doc/current/security.html
security:
# https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
providers:
my_provider:
entity: { class: App:User, property: username }
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
logout:
path: /logout
target: /login
logout_on_user_change: true
guard:
authenticators:
- App\Security\GoogleAuthenticator
knpu_oauth2_client:
clients:
google:
# must be "google" - it activates that type!
type: google
# add and configure client_id and client_secret in parameters.yml
client_id: '%env(resolve:GOOGLE_CLIENT_ID)%'
client_secret: '%env(resolve:GOOGLE_CLIENT_SECRET)%'
# a route name you'll create
redirect_route: connect_google_check
redirect_params: {}
# Optional value for sending access_type parameter. More detail: https://developers.google.com/identity/protocols/OpenIDConnect#authenticationuriparameters
# access_type: ''
# Optional value for sending hd parameter. More detail: https://developers.google.com/identity/protocols/OpenIDConnect#hd-param
# hosted_domain: ''
# Optional value for additional fields to be requested from the user profile. If set, these values will be included with the defaults. More details: https://developers.google.com/+/web/api/rest/latest/people
# user_fields: {}
# whether to check OAuth2 "state": defaults to true
# use_state: true
Right now, I will create an interested controller named GoogleController. So, I can set directions for routers. I have run this command.
bin/console make:controller
These lines mean GoogleController file.
<?php
namespace App\Controller;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class GoogleController extends AbstractController
{
/**
* Link to this controller to start the "connect" process
*
* @Route("/connect/google", name="connect_google")
* @param ClientRegistry $clientRegistry
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function connectAction(ClientRegistry $clientRegistry)
{
return $clientRegistry
->getClient('google')
->redirect();
}
/**
* Facebook redirects to back here afterward
*
* @Route("/connect/google/check", name="connect_google_check")
* @param Request $request
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function connectCheckAction(Request $request)
{
if (!$this->getUser()) {
return new JsonResponse(array('status' => false, 'message' => "User not found!"));
} else {
return $this->redirectToRoute('default');
}
}
}
Now, I will have a forwarder in the interface. I have created a Twig file like this with Bootstrap 4. My value of href attribute is /connect/google.
When I clicked the Google button, it will ask me for permission. I allow it and I can enter successfully.
Posted on August 22, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.