loading...

Integrate Oauth2 for Symfony 4

_mertsimsek profile image Mert Simsek Updated on ・7 min read

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.

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

Discussion

pic
Editor guide
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 Author

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

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 Author

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

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 Author

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

Collapse
bali48 profile image
Muhammad Bilal

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 Author

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 Author

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 Author

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 Author

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 Author

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
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
sasa1007 profile image
Sasa Milivojevic

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 Author

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 Author

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

Collapse
raphaelmouton profile image
Raphael Mouton

Really cool ! Thanks a lot ! Works perfectly for me !
I'm going to try the same with facebook :)

Collapse
_mertsimsek profile image
Mert Simsek Author

That's superb! I am pleased.
Yes, i integrated Facebook last week. It happened fine :)

Collapse
byhaskell profile image
Alexander

Thank you, very quickly helped set up!

Collapse
_mertsimsek profile image
Mert Simsek Author

I'm glad to hear that :)

Collapse
babababou profile image
babababou

Hey great tuto ! thks !
Can you make a little one for facebook ??? Because I really can't do it....

Tks you :)

Collapse
_mertsimsek profile image
Mert Simsek Author

Hi,

Thank you for pretty reply :) In fact, there is a document related as you want. Here you are :)

github.com/knpuniversity/oauth2-cl...

Collapse
thomascms45 profile image
ThomasCms

@babababou did you success ? Cause I'm following knpuniversity documentation but I feel like I have to be in HTTPS...

Collapse
sasa1007 profile image
Sasa Milivojevic

Great tutorial everything works great

Collapse
_mertsimsek profile image
Mert Simsek Author

I'm glad to hear it from you :)

Collapse
sasa1007 profile image
Sasa Milivojevic

Hi,

If I try to login but I dont have gmail account, I get epmty form for login on gmail account, but I want if there is no gmail account to redirect user on other url for log in.

Can you help?

Collapse
sfmok profile image
Mokhtar Tlili

I'm wondering about which purpose you create UserProvider. It looks you didn't use it in Authenticator

Collapse
disvroian profile image
disvroian

There is no OAuth2 client called "google". Available are:
This is what i have. Can someone help me ?

Collapse
_mertsimsek profile image
Mert Simsek Author

what is excatly the issue? Can you clarify it more?

Collapse
saninshakya profile image
San sh

How can I store the username at session after successful login?