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.
Top comments (42)
Hi,
I have tried this for Symfony 4.2.2.
When i edit my security.yaml, i have this error : " Unrecognized option "knpu_oauth2_client" under "security". Available options are "access_control", "access_decision_manager", "access_denied_url", "always_authenticate_before_granting", "encoders", "erase_credentials", "firewalls", "hide_user_not_found", "providers", "role_hierarchy", "session_fixation_strategy". "
I don't know how can i solve it ?
Hi ,
Did you found a solution for this problem , cause i have the same and couldn't fix it ?
Hi fellas,
Thank you for reply, I tried to get same error and I did as I said in this article and I got same error. I found the solution.
You have to insert these lines. security and knpu_oauth2_client must be same level for yaml format. Don't push knpu_oauth2_client to under security section.
Hi, thanks a lot for this tutorial, I followed all the steps but I still have this error :
{
"status": false,
"message": "User not found!"
}
I tried a var_dump(($this->getUser());
and I get NULL
So I don't know what is wrong, thanks for your help :)
Hi Julien,
Where do you write var_dump($this->getUser()); ?
You should try to write dumping the User into GoogleAuthenticator class and getUser() method? And in my opinion, keep on line-by-line with this way that class.
Ok I'll try this, I was tryin in the Google Controller... probably not the best idea
So I think I'm half way there, I had to change this, so I'm adding users in the bdd via google.
Once again, thanks a lot for you're toturial :)
Don't mention it :) I got happy for you
Hi,
Is it possible for you to explain how to mix a form login (simple page with forms) and the knpu oauth in a api that using lexik and jwt please ? I can log to my api with sending json to the login_check path that send me the jwt token and then use it to play with api route, but in a mixed login, how to implement it to connect with google and receive the jwt token (it is maybe a stupid question but i need to know it)
Thanks for this very great tut
thepracticaldev.s3.amazonaws.com/i...
The App\Security\GoogleAuthenticator::getUser() method must return a UserInterface. You returned App\Entity\User.
I don't know how can i solve it ?
Apparently, you need to implement UserInterface to User Entity class. Open the User entity file and import this line,
class User implement UserInterface
First thank you so much bro for this tutorial It helped me a lot and saved my project but there's one problem ... these infos doen't save to my database directly
Thanks for the answer. What's the matter with the database?
The infos that we brought from google doesn't persist / flush
Is there anything error or what?
This code should insert your user data into your database.
I had a little problem in my code and it worked thanks again , I want to ask you please how can I retrieve the Date of birth
As I read, there is a scope about it. Here you are, the documentation.
developers.google.com/people/v1/ho...
You need to add this scope as the documentation.
googleapis.com/auth/user.birthday....
where shall I add it ?
In fact, I don't know but Could try this? I've changed user_fields line.
Also, check out developers.google.com/+/web/api/re...
It didnt worked :(
HI I thing that something Google changed in past couple days
github.com/thephpleague/oauth2-goo...
so I dont know how to override so I can make it work - until somene fix the bundle.
I check it out the issue, I assume you find a solution, right?
Oh yes sorry I didnt write
in googleController should be
Perfect! Nowadays, I move to Symfony 5, thanks a lot!
Hi,
I implement new symfony security
symfony.com/doc/current/security.html
and now I have this error
Argument 1 passed to Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticator::__construct() must implement interface Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface, instance of App\Security\GoogleAuthAuthenticator given, called in /opt/app/vendor/symfony/security-http/Authenticator/Debug/TraceableAuthenticatorManagerListener.php on line 60
Hi,
I have tried this for Symfony 5. I cannot do the authentication because when I have done the login with google it returns {"status":false,"message":"User not found!"}.
I cannot understand what's wrong, can you help me?
Hi @marcomorain
Actually, I haven't moved to Symfony 5 version. However, it might be an easy way because most things got easier regarding security on that version as I know.
Thank you, very quickly helped set up!
I'm glad to hear that :)
Hi,
I don't know exactly why I am facing this error
error
<< Attempted to load class "AbstractGuardAuthenticator" from namespace "Symfony\Component\Security\Guard".
Did you forget a "use" statement for another namespace? >>