Facebook login with Symfony
Tags: tutorial, php, symfony, login
Published: 2021-01-12 11:25:42

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](https://symfony.com/doc/current/security/guard_authentication.html) to auth user in the backend.

### Facebook libraries

Create new app [here](https://developers.facebook.com/apps).

You need Facebook Javascript SDK for the frontend and PHP library for the backend.
Install PHP Facebook library:
```bash
composer require facebook/graph-sdk
```

Include Facebook Javascript SDK to your template. It should look like that:
```html
<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>
```
The simplest way is to generate this snippet from [Facebook developers](https://developers.facebook.com/apps) page in **Facebook Login** - **Quickstart** section.

### Frontend

Next, you need to add a button and handle login result. Button example:
```html
<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>

```
[Here](https://developers.facebook.com/docs/facebook-login/web/login-button/) is a button configurator.

I created a Javascript module with that code:
```js
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);
}
```
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:
```yaml
security:
    firewalls:
        main:
            guard:
                authenticators:
                    - App\Security\LoginFormAuthenticator
                    - App\Security\FacebookAuthenticator
                entry_point: App\Security\LoginFormAuthenticator
```
  
Now you need to create Guard file, which extends AbstractFormLoginAuthenticator:
```php
class FacebookAuthenticator extends AbstractFormLoginAuthenticator {}
```

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: 
```php
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');
}
```
**getCredentials** - gets Facebook auth response and decode it to an array: 
```php
public function getCredentials(Request $request)
{
    return json_decode($request->get('fbAuthResponse'), true);
}
```

**onAuthenticationSuccess** - I redirect the user to the landing page:
```php
public function onAuthenticationSuccess(
    Request $request, 
    TokenInterface $token, 
    $providerKey
)
{
    return new RedirectResponse(
        $this->urlGenerator->generate('app_landing')
    );
}
```
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:
```php
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());
}
```
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:
```php
public function __construct(
    FacebookService $fbService, 
    UrlGeneratorInterface $urlGenerator
)
{
    $this->fbService = $fbService;
    $this->urlGenerator = $urlGenerator;
}
```

My fbService looks like this:
```php
<?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;
    }
}
```
In order to autowire FacebookService constructor arguments, add variables to .env file (according to your environment) and add parameters to services.yaml config:
```yaml
services:
    App\Service\FacebookService:
        arguments:
            $fbAppId: '%env(FB_APP_ID)%'
            $fbAppSecret: '%env(FB_APP_SECRET)%'
            $fbGraphVersion: '%env(FB_GRAPH_VERSION)%'
```

Here is a FacebookUser entity. You can use an array instead of an entity, but OOP is more convenient for me. 
```php
<?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;
    }
}
```
That's it.  

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