My goal was to add the 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.
To test the Facebook login button locally, you need to get HTTPS working on localhost.
Overview
Workflow overview is:
- User clicks the Facebook login button
- Facebook authenticate user
- App sends facebook token (auth data) to Symfony backend
- 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
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>
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>
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);
}
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
Now you need to create Guard file, which extends AbstractFormLoginAuthenticator:
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:
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:
public function getCredentials(Request $request)
{
return json_decode($request->get('fbAuthResponse'), true);
}
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')
);
}
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());
}
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;
}
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;
}
}
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)%'
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;
}
}
That’s it.
Actually, there are some more methods in Guard, but it shouldn’t be complex to create them.