DEV Community

Mert Simsek
Mert Simsek

Posted on • Edited on

Integrate Oauth2 for Symfony 4

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
Enter fullscreen mode Exit fullscreen mode

After installation, I have defined an Entity named User. You can run this command.

bin/console make:entity
Enter fullscreen mode Exit fullscreen mode
<?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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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';
    }
}
Enter fullscreen mode Exit fullscreen mode

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.
    }
}
Enter fullscreen mode Exit fullscreen mode

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 ###
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
        }

    }

}
Enter fullscreen mode Exit fullscreen mode

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.

alt text

alt text

When I clicked the Google button, it will ask me for permission. I allow it and I can enter successfully.

alt text

Top comments (42)

Collapse
 
clementinioo profile image
B1905

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 ?

Collapse
 
inchnus profile image
MedRyanReychico

Hi ,
Did you found a solution for this problem , cause i have the same and couldn't fix it ?

Collapse
 
_mertsimsek profile image
Mert Simsek • Edited

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.

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:
                    type: google
                    client_id: '%env(resolve:GOOGLE_CLIENT_ID)%'
                    client_secret: '%env(resolve:GOOGLE_CLIENT_SECRET)%'
                    redirect_route: connect_google_check
                    redirect_params: {}
Collapse
 
julienlav profile image
julien-lav • Edited

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 :)

Collapse
 
_mertsimsek profile image
Mert Simsek

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.

Collapse
 
julienlav profile image
julien-lav

Ok I'll try this, I was tryin in the Google Controller... probably not the best idea

Thread Thread
 
julienlav profile image
julien-lav • Edited

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 :)

Thread Thread
 
_mertsimsek profile image
Mert Simsek

Don't mention it :) I got happy for you

Collapse
 
dechans profile image
Hans DECAESTEKER

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

Collapse
 
bali48 profile image
Muhammad Bilal • Edited

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 ?

Collapse
 
_mertsimsek profile image
Mert Simsek

Apparently, you need to implement UserInterface to User Entity class. Open the User entity file and import this line,

class User implement UserInterface

Collapse
 
mbj97 profile image
MBJ-97

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

Collapse
 
_mertsimsek profile image
Mert Simsek

Thanks for the answer. What's the matter with the database?

Collapse
 
mbj97 profile image
MBJ-97

The infos that we brought from google doesn't persist / flush

Thread Thread
 
_mertsimsek profile image
Mert Simsek • Edited

Is there anything error or what?

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;
    }

This code should insert your user data into your database.

Thread Thread
 
mbj97 profile image
MBJ-97

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

Thread Thread
 
_mertsimsek profile image
Mert Simsek

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....

Thread Thread
 
mbj97 profile image
MBJ-97

where shall I add it ?

Thread Thread
 
_mertsimsek profile image
Mert Simsek

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...

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: {'birthday'}
            # whether to check OAuth2 "state": defaults to true
            # use_state: true
Thread Thread
 
mbj97 profile image
MBJ-97

It didnt worked :(

Collapse
 
sasa1007 profile image
Sasa Milivojevic • Edited

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.

Collapse
 
_mertsimsek profile image
Mert Simsek

I check it out the issue, I assume you find a solution, right?

Collapse
 
sasa1007 profile image
Sasa Milivojevic

Oh yes sorry I didnt write

in googleController should be

public function connectAction(ClientRegistry $clientRegistry)
{
    return $clientRegistry
        ->getClient('google')
        ->redirect([], [
            'prompt' => 'consent',
        ]);
}
Thread Thread
 
_mertsimsek profile image
Mert Simsek

Perfect! Nowadays, I move to Symfony 5, thanks a lot!

Collapse
 
sasa1007 profile image
Sasa Milivojevic

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

Collapse
 
marcomorandin profile image
MarcoMorandin

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?

Collapse
 
_mertsimsek profile image
Mert Simsek

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.

Collapse
 
byhaskell profile image
Alexander

Thank you, very quickly helped set up!

Collapse
 
_mertsimsek profile image
Mert Simsek

I'm glad to hear that :)

Collapse
 
zouhir923 profile image
harabazan zouhir

Image description

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? >>