DEV Community

Cover image for Facebook login with Symfony
Konstantin Bogomolov
Konstantin Bogomolov

Posted on • Originally published at bogomolov.tech

Facebook login with Symfony

My goal was to add Continue with Facebook (Log In) button into the existing Symfony application. I do not provide here full code. Only the main parts to creating your Facebook login button. Here is what I did.

Overview

Workflow overview is:

  1. User clicks the Facebook login button
  2. Facebook authenticate user
  3. App sends facebook token (auth data) to Symfony backend
  4. Backend gets user data (name and email) using auth data and logs in with Symfony handlers.

So Facebook is only necessary for the only first part - checking the person who wants to log in.

I use Guard to auth user in the backend.

Facebook libraries

Create new app here.

You need Facebook Javascript SDK for the frontend and PHP library for the backend.
Install PHP Facebook library:

composer require facebook/graph-sdk
Enter fullscreen mode Exit fullscreen mode

Include Facebook Javascript SDK to your template. It should look like that:

<script async defer crossorigin="anonymous"
        src="https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v7.0&appId=<your app id here>">
</script>
Enter fullscreen mode Exit fullscreen mode

The simplest way is to generate this snippet from Facebook developers page in Facebook Login - Quickstart section.

Frontend

Next, you need to add a button and handle login result. Button example:

<div class="fb-login-button"
     data-size="large"
     data-button-type="continue_with"
     data-layout="default"
     data-auto-logout-link="false"
     data-use-continue-as="false"
     data-scope="public_profile,email"
     onlogin="fbGetLoginStatus();"
     data-width=""></div>

Enter fullscreen mode Exit fullscreen mode

Here is a button configurator.

I created a Javascript module with that code:

window.fbGetLoginStatus = function () {
    FB.getLoginStatus(function (response) {
        if (isConnected(response)) {
            fbLogIn(response);
        }
    });
}
function isConnected(response) {
    return response.status === 'connected';
}

function fbLogIn(response) {
    let loginForm = document.querySelector('.login-form');
    let input = getHiddenInput("fbAuthResponse", JSON.stringify(response.authResponse));    
    loginForm.appendChild(input);
    loginForm.submit();
}

function getHiddenInput(name, value) {
    let input = document.createElement("input");
    input.setAttribute("type", "hidden");
    input.setAttribute("name", name);
    input.setAttribute("value", value);
}
Enter fullscreen mode Exit fullscreen mode

When a user clicks to *Continue with Facebook button and successfully logged in, then I submit a login (or registration) form and send data to backend via POST request.

Backend - Symfony Guard

I had already one Guard for usual login-password authentication. So I add the second one. Here is my security.yaml config for Guard:

security:
    firewalls:
        main:
            guard:
                authenticators:
                    - App\Security\LoginFormAuthenticator
                    - App\Security\FacebookAuthenticator
                entry_point: App\Security\LoginFormAuthenticator
Enter fullscreen mode Exit fullscreen mode

Now you need to create Guard file, which extends AbstractFormLoginAuthenticator:

class FacebookAuthenticator extends AbstractFormLoginAuthenticator {}
Enter fullscreen mode Exit fullscreen mode

The main methods of the FacebookAuthenticator will be:

supports - it checks if you should use that Guard. I used the same logic for the login and register page. So it looks like that:

public function supports(Request $request)
{
    $route = $request->attributes->get('_route');
    $isLoginOrRegister = in_array($route, ['app_login', 'app_register']);
    return $isLoginOrRegister 
        && $request->isMethod('POST') 
        && $request->get('fbAuthResponse');
}
Enter fullscreen mode Exit fullscreen mode

getCredentials - gets Facebook auth response and decode it to an array:

public function getCredentials(Request $request)
{
    return json_decode($request->get('fbAuthResponse'), true);
}
Enter fullscreen mode Exit fullscreen mode

onAuthenticationSuccess - I redirect the user to the landing page:

public function onAuthenticationSuccess(
    Request $request, 
    TokenInterface $token, 
    $providerKey
)
{
    return new RedirectResponse(
        $this->urlGenerator->generate('app_landing')
    );
}
Enter fullscreen mode Exit fullscreen mode

In order to use a url generator, add it to constructor (see below).

getUser - the main part, where you gets email and name from Facebook Graph API and return User entity:

public function getUser($credentials, UserProviderInterface $userProvider)
{
    if (empty($credentials['accessToken'])) {
        throw new CustomUserMessageAuthenticationException('Your message here');
    }

    $fbUser = $this->fbService->getUser($credentials['accessToken']);

    if (empty($fbUser->getEmail())) {
        throw new CustomUserMessageAuthenticationException('Your message here');
    }

    return $userProvider->loadUserByUsername($fbUser->getEmail());
}
Enter fullscreen mode Exit fullscreen mode

I use email and name for registration when the user does not exist. Here is only the login part, without registration for simplicity.

Here is the way you can autowire services in Guard constructor:

public function __construct(
    FacebookService $fbService, 
    UrlGeneratorInterface $urlGenerator
)
{
    $this->fbService = $fbService;
    $this->urlGenerator = $urlGenerator;
}
Enter fullscreen mode Exit fullscreen mode

My fbService looks like this:

<?php

namespace App\Service;

use App\Entity\FacebookUser;
use Facebook\Facebook;

class FacebookService
{
    private $client;

    public function __construct(
        string $fbAppId, 
        string $fbAppSecret, 
        string $fbGraphVersion
    )
    {
        $this->client = new Facebook([
            'app_id' => $fbAppId,
            'app_secret' => $fbAppSecret,
            'default_graph_version' => $fbGraphVersion,
        ]);

    }

    public function getUser(string $token): FacebookUser
    {
        $user = new FacebookUser();

        try {
            $fbUser = $this->client->get("/me?fields=name,email", $token);
            $data = $fbUser->getDecodedBody();
            $user
                ->setName($data['name'])
                ->setEmail($data['email']);
        } catch (\Throwable $exception) {
            // handle exception here
        }

        return $user;
    }
}
Enter fullscreen mode Exit fullscreen mode

In order to autowire FacebookService constructor arguments, add variables to .env file (according to your environment) and add parameters to services.yaml config:

services:
    App\Service\FacebookService:
        arguments:
            $fbAppId: '%env(FB_APP_ID)%'
            $fbAppSecret: '%env(FB_APP_SECRET)%'
            $fbGraphVersion: '%env(FB_GRAPH_VERSION)%'
Enter fullscreen mode Exit fullscreen mode

Here is a FacebookUser entity. You can use an array instead of an entity, but OOP is more convenient for me.

<?php

namespace App\Entity;

class FacebookUser
{
    private $name;
    private $email;

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName($name): self
    {
        $this->name = $name;
        return $this;
    }

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

    public function setEmail(string $email): self
    {
        $this->email = $email;
        return $this;
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it.

Actually, there are some more methods in Guard, but it shouldn't be complex to create them.

Top comments (0)