A fully unit-tested package for easily integrating the Facebook SDK v5 into Lumen.
- Installation
- Facebook Login
- Saving Data From Facebook In The Database
- Logging The User Into Laravel
- Working With Multiple Apps
- Error Handling
- Testing
- Contributing
- Credits
- License
Add the Lumen Facebook SDK package to your composer.json
file.
{
"require": {
"linkthrow/lumen-facebook-sdk": "~4.0"
}
}
In your bootstrap/app.php, add the support provider and LumenFacebookSdkServiceProvider:
$app->register(Irazasyed\Larasupport\Providers\ArtisanServiceProvider::class);
$app->register(LinkThrow\LumenFacebookSdk\LumenFacebookSdkServiceProvider::class);
If you want to make use of the facade, add it to the aliases array in your bootstrap/app.php
First enable aliases via:
$app->withFacades();
Then add:
class_alias(LinkThrow\LumenFacebookSdk\FacebookFacade::class, 'Facebook');
But there are much better ways to use this package that don't use facades.
The IoC container will automatically resolve the LumenFacebookSdk
dependencies for you. You can grab an instance of LumenFacebookSdk
from the IoC container in a number of ways.
// Directly from App::make();
$fb = App::make('LinkThrow\LumenFacebookSdk\LumenFacebookSdk');
//In order to do this you must first register the App provider, add the following to bootstrap/app.php
$app->register(App\Providers\AppServiceProvider::class);
class_alias(Illuminate\Support\Facades\App::class, 'App');
// From a constructor
class FooClass {
public function __construct(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb) {
// . . .
}
}
// From a method
class BarClass {
public function barMethod(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb) {
// . . .
}
}
// Or even a closure
Route::get('/facebook/login', function(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb) {
// . . .
});
After creating an app in Facebook, you'll need to provide the app ID and secret. First publish the configuration file.
$ php artisan vendor:publish --provider="LinkThrow\LumenFacebookSdk\LumenFacebookSdkServiceProvider" --tag="config"
Where's the file? With the Larasupport package which we installed, Lumen will publish the config file to
config/lumen-facebook-sdk.php
.
You'll need to update the app_id
and app_secret
values in the config file with your app ID and secret.
By default the configuration file will look to environment variables for your app ID and secret. It is recommended that you use environment variables to store this info in order to protect your app secret from attackers. Make sure to update your /.env
file with your app ID & secret.
FACEBOOK_APP_ID=1234567890
FACEBOOK_APP_SECRET=SomeFooAppSecret
If you have a facebook_user_id
column in your user's table, you can add the SyncableGraphNodeTrait
to your User
model to have the user node from the Graph API automatically sync with your model.
class User extends Eloquent implements UserInterface {
use LinkThrow\LumenFacebookSdk\SyncableGraphNodeTrait;
protected static $graph_node_field_aliases = [
'id' => 'facebook_user_id',
];
}
More info on saving data from Facebook in the database.
Here's a full example of how you might log a user into your app using the redirect method.
This example also demonstrates how to exchange a short-lived access token with a long-lived access token and save the user to your users
table if the entry doesn't exist.
Finally it will log the user in using Laravel's built-in user authentication.
// Generate a login URL
Route::get('/facebook/login', function(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb)
{
// Send an array of permissions to request
$login_url = $fb->getLoginUrl(['email']);
// Obviously you'd do this in blade :)
echo '<a href="' . $login_url . '">Login with Facebook</a>';
});
// Endpoint that is redirected to after an authentication attempt
Route::get('/facebook/callback', function(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb)
{
// Obtain an access token.
try {
$token = $fb->getAccessTokenFromRedirect();
} catch (Facebook\Exceptions\FacebookSDKException $e) {
dd($e->getMessage());
}
// Access token will be null if the user denied the request
// or if someone just hit this URL outside of the OAuth flow.
if (! $token) {
// Get the redirect helper
$helper = $fb->getRedirectLoginHelper();
if (! $helper->getError()) {
abort(403, 'Unauthorized action.');
}
// User denied the request
dd(
$helper->getError(),
$helper->getErrorCode(),
$helper->getErrorReason(),
$helper->getErrorDescription()
);
}
if (! $token->isLongLived()) {
// OAuth 2.0 client handler
$oauth_client = $fb->getOAuth2Client();
// Extend the access token.
try {
$token = $oauth_client->getLongLivedAccessToken($token);
} catch (Facebook\Exceptions\FacebookSDKException $e) {
dd($e->getMessage());
}
}
$fb->setDefaultAccessToken($token);
// Save for later
Session::put('fb_user_access_token', (string) $token);
// Get basic info on the user from Facebook.
try {
$response = $fb->get('/me?fields=id,name,email');
} catch (Facebook\Exceptions\FacebookSDKException $e) {
dd($e->getMessage());
}
// Convert the response to a `Facebook/GraphNodes/GraphUser` collection
$facebook_user = $response->getGraphUser();
// Create the user if it does not exist or update the existing entry.
// This will only work if you've added the SyncableGraphNodeTrait to your User model.
$user = App\User::createOrUpdateGraphNode($facebook_user);
// Log the user into Laravel
Auth::login($user);
return redirect('/')->with('message', 'Successfully logged in with Facebook');
});
When we say "log in with Facebook", we really mean "obtain a user access token to make calls to the Graph API on behalf of the user." This is done through Facebook via OAuth 2.0. There are a number of ways to log a user in with Facebook using what the Facbeook PHP SDK calls "helpers".
The four supported login methods are:
- Login From Redirect (OAuth 2.0)
- Login From JavaScript (with JS SDK cookie)
- Login From App Canvas (with signed request)
- Login From Page Tab (with signed request)
One of the most common ways to log a user into your app is by using a redirect URL.
The idea is that you generate a unique URL that the user clicks on. Once the user clicks the link they will be redirected to Facebook asking them to grant any permissions your app is requesting. Once the user responds, Facebook will redirect the user back to a callback URL that you specify with either a successful response or an error response.
The redirect helper can be obtained using the SDK's getRedirectLoginHelper()
method.
You can get a login URL just like you you do with the Facebook PHP SDK v5.
Route::get('/facebook/login', function(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb) {
$login_link = $fb
->getRedirectLoginHelper()
->getLoginUrl('https://exmaple.com/facebook/callback', ['email', 'user_events']);
echo '<a href="' . $login_link . '">Log in with Facebook</a>';
});
But if you set the default_redirect_uri
callback URL in the config file, you can use the getLoginUrl()
wrapper method which will default the callback URL (default_redirect_uri
) and permission scope (default_scope
) to whatever you set in the config file.
$login_link = $fb->getLoginUrl();
Alternatively you can pass the permissions and a custom callback URL to the wrapper to overwrite the default config.
Note: Since the list of permissions sometimes changes but the callback URL usually stays the same, the permissions array is the first argument in the
getLoginUrl()
wrapper method which is the reverse of the SDK's methodgetRedirectLoginHelper()->getLoginUrl($url, $permissions)
.
$login_link = $fb->getLoginUrl(['email', 'user_status'], 'https://exmaple.com/facebook/callback');
// Or, if you want to default to the callback URL set in the config
$login_link = $fb->getLoginUrl(['email', 'user_status']);
After the user has clicked on the login link from above and confirmed or denied the app permission requests, they will be redirected to the specified callback URL.
The standard "SDK" way to obtain an access token on the callback URL is as follows:
Route::get('/facebook/callback', function(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb) {
try {
$token = $fb
->getRedirectLoginHelper()
->getAccessToken();
} catch (Facebook\Exceptions\FacebookSDKException $e) {
// Failed to obtain access token
dd($e->getMessage());
}
});
There is a wrapper method for getRedirectLoginHelper()->getAccessToken()
in LumenFacebookSdk called getAccessTokenFromRedirect()
that defaults the callback URL to the laravel-facebook-sdk.default_redirect_uri
config value.
Route::get('/facebook/callback', function(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb) {
try {
$token = $fb->getAccessTokenFromRedirect();
} catch (Facebook\Exceptions\FacebookSDKException $e) {
// Failed to obtain access token
dd($e->getMessage());
}
// $token will be null if the user denied the request
if (! $token) {
// User denied the request
}
});
If you're using the JavaScript SDK, you can obtain an access token from the cookie set by the JavaScript SDK.
By default the JavaScript SDK will not set a cookie, so you have to explicitly enable it with cookie: true
when you init()
the SDK.
FB.init({
appId : 'your-app-id',
cookie : true,
version : 'v2.5'
});
After you have logged a user in with the JavaScript SDK using FB.login()
, you can obtain a user access token from the signed request that is stored in the cookie that was set by the JavaScript SDK.
Route::get('/facebook/javascript', function(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb) {
try {
$token = $fb->getJavaScriptHelper()->getAccessToken();
} catch (Facebook\Exceptions\FacebookSDKException $e) {
// Failed to obtain access token
dd($e->getMessage());
}
// $token will be null if no cookie was set or no OAuth data
// was found in the cookie's signed request data
if (! $token) {
// User hasn't logged in using the JS SDK yet
}
});
TokenMismatchException: By default your canvas app will throw a
TokenMismatchException
when you try to view it in Facebook. See how to fix this issue.
If your app lives within the context of a Facebook app canvas, you can obtain an access token from the signed request that is POST
'ed to your app on the first page load.
Note: The canvas helper only obtains an existing access token from the signed request data received from Facebook. If the user visiting your app has not authorized your app yet or their access token has expired, the
getAccessToken()
method will returnnull
. In that case you'll need to log the user in with either a redirect or JavaScript.
Use the SDK's canvas helper to obtain the access token from the signed request data.
Route::get('/facebook/canvas', function(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb) {
try {
$token = $fb->getCanvasHelper()->getAccessToken();
} catch (Facebook\Exceptions\FacebookSDKException $e) {
// Failed to obtain access token
dd($e->getMessage());
}
// $token will be null if the user hasn't authenticated your app yet
if (! $token) {
// . . .
}
});
TokenMismatchException: By default your Page tab will throw a
TokenMismatchException
when you try to view it in Facebook. See how to fix this issue.
If your app lives within the context of a Facebook Page tab, that is the same as an app canvas and the "Login From App Canvas" method will also work to obtain an access token. But a Page tab also has additional data in the signed request.
The SDK provides a Page tab helper to obtain an access token from the signed request data within the context of a Page tab.
Route::get('/facebook/page-tab', function(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb) {
try {
$token = $fb->getPageTabHelper()->getAccessToken();
} catch (Facebook\Exceptions\FacebookSDKException $e) {
// Failed to obtain access token
dd($e->getMessage());
}
// $token will be null if the user hasn't authenticated your app yet
if (! $token) {
// . . .
}
});
Facebook supports two other types of authorization URL's - rerequests and re-authentications.
Rerequests (or re-requests?) ask the user again for permissions they have previously declined. It's important to use a rerequest URL for this instead of just redirecting them with the normal log in link because:
Once someone has declined a permission, the Login Dialog will not re-ask them for it unless you explicitly tell the dialog you're re-asking for a declined permission. - Facebook Documentation
You can generate a rerequest URL using the getReRequestUrl()
method.
$rerequest_link = $fb->getReRequestUrl(['email'], 'https://exmaple.com/facebook/login');
// Or, if you want to default to the callback URL set in the config
$rerequest_link = $fb->getReRequestUrl(['email']);
Re-authentications force a user to confirm their identity by asking them to enter their Facebook account password again. This is useful for adding another layer of security before changing or view sensitive data on your web app.
You can generate a re-authentication URL using the getReAuthenticationUrl()
method.
$re_authentication_link = $fb->getReAuthenticationUrl(['email'], 'https://exmaple.com/facebook/login');
// Or, if you want to default to the callback URL set in the config
$re_authentication_link = $fb->getReAuthenticationUrl(['email']);
// Or without permissions
$re_authentication_link = $fb->getReAuthenticationUrl();
In most cases you won't need to save the access token to a database unless you plan on making requests to the Graph API on behalf of the user when they are not browsing your app (like a 3AM CRON job for example).
After you obtain an access token, you can store it in a session to be used for subsequent requests.
Session::put('facebook_access_token', (string) $token);
Then in each script that makes calls to the Graph API you can pull the token out of the session and set it as the default.
$token = Session::get('facebook_access_token');
$fb->setDefaultAccessToken($token);
Saving data received from the Graph API to a database can sometimes be a tedious endeavor. Since the Graph API returns data in a predictable format, the SyncableGraphNodeTrait
can make saving the data to a database a painless process.
Any Eloquent model that implements the SyncableGraphNodeTrait
will have the createOrUpdateGraphNode()
method applied to it. This method really makes it easy to take data that was returned directly from Facebook and create or update it in the local database.
use LinkThrow\LumenFacebookSdk\SyncableGraphNodeTrait;
class Event extends Eloquent {
use SyncableGraphNodeTrait;
}
For example if you have an Eloquent model named Event
, here's how you might grab a specific event from the Graph API and insert it into the database as a new entry or update an existing entry with the new info.
$response = $fb->get('/some-event-id?fields=id,name');
$eventNode = $response->getGraphEvent();
// A method called createOrUpdateGraphNode() on the `Event` eloquent model
// will create the event if it does not exist or it will update the existing
// record based on the ID from Facebook.
$event = Event::createOrUpdateGraphNode($eventNode);
The createOrUpdateGraphNode()
will automatically map the returned field names to the column names in your database. If, for example, your column names on the events
table don't match the field names for an Event node, you can map the fields.
Since the names of the columns in your database might not match the names of the fields of the Graph nodes, you can map the field names in your User
model using the $graph_node_field_aliases
static variable.
The keys of the array are the names of the fields on the Graph node. The values of the array are the names of the columns in the local database.
use LinkThrow\LumenFacebookSdk\SyncableGraphNodeTrait;
class User extends Eloquent implements UserInterface
{
use SyncableGraphNodeTrait;
protected static $graph_node_field_aliases = [
'id' => 'facebook_user_id',
'name' => 'full_name',
'graph_node_field_name' => 'database_column_name',
];
}
By default the createOrUpdateGraphNode()
method will try to insert all the fields of a node into the database. But sometimes the Graph API will return fields that you didn't specifically ask for and don't exist in your database. In those cases we can white list specific fields with the $graph_node_fillable_fields
property.
use LinkThrow\LumenFacebookSdk\SyncableGraphNodeTrait;
class Event extends Eloquent
{
use SyncableGraphNodeTrait;
protected static $graph_node_fillable_fields = ['id', 'name', 'start_time'];
}
Use the name of the database column. For example, if you've aliased the
id
field to thefacebook_id
column in your databse, you'll want to specifyfacebook_id
in your$graph_node_fillable_fields
array.
Since the Graph API will return some of the fields from a request as other nodes/objects, you can reference the fields on those using Laravel's array_dot()
notation.
An example might be making a request to the /me/events
endpoint and looping through all the events and saving them to your Event
model. The Event node will return the place.location fields as Location nodes. The response data might look like this:
{
"data": [
{
"id": "123",
"name": "Foo Event",
"place": {
"location": {
"city": "Dearborn",
"state": "MI",
"country": "United States",
. . .
},
"id": "827346"
}
},
. . .
]
}
Let's assume you have an event table like this:
Schema::create('events', function(Blueprint $table)
{
$table->increments('id');
$table->bigInteger('facebook_id')->nullable()->unsigned()->index();
$table->string('name')->nullable();
$table->string('city')->nullable();
$table->string('state')->nullable();
$table->string('country')->nullable();
});
Here's how you would map the nested fields to your database table in your Event
model:
use LinkThrow\LumenFacebookSdk\SyncableGraphNodeTrait;
class Event extends Eloquent
{
use SyncableGraphNodeTrait;
protected static $facebook_field_aliases = [
'id' => 'facebook_id',
'place.location.city' => 'city',
'place.location.state' => 'state',
'place.location.country' => 'country',
];
}
The Facebook PHP SDK will convert most date formats into instances of DateTime
. This can be problematic when you want to insert a date/time value into the database (e.g. the start_time
field of an Event node).
By default the SyncableGraphNodeTrait
will convert all DateTime
instances to the following date()
format:
Y-m-d H:i:s
That should the proper format for most cases on most relational databases. But this format is missing the timezone which might be important to your application. Furthermore if you're storing the date/time values in a different format, you'll want to customize the format that DateTime
instances get converted to. To do this just add a $graph_node_date_time_to_string_format
property to your model and set it to any valid date format.
use LinkThrow\LumenFacebookSdk\SyncableGraphNodeTrait;
class Event extends Eloquent
{
use SyncableGraphNodeTrait;
protected static $graph_node_date_time_to_string_format = 'c'; # ISO 8601 date
}
The Laravel Facebook SDK makes it easy to log a user in with Laravel's built-in authentication driver.
In order to get Facebook authentication working with Laravel's built-in authentication, you'll need to store the Facebook user's ID in your user's table.
Naturally you'll need to create a column for every other piece of information you want to keep about the user.
You can store the access token in the database if you need to make requests on behalf of the user when they are not browsing your app (like a 3AM cron job). But in general you won't need to store the access token in the database.
You'll need to generate a migration to modify your users
table and add any new columns.
Note: Make sure to change
<name-of-users-table>
to the name of your user table.
$ php artisan make:migration add_facebook_columns_to_users_table --table="<name-of-users-table>"
Now update the migration file to include the new fields you want to save on the user. At minimum you'll need to save the Facebook user ID.
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddFacebookColumnsToUsersTable extends Migration
{
public function up()
{
Schema::table('users', function(Blueprint $table)
{
// If the primary id in your you user table is different than the Facebook id
// Make sure it's an unsigned() bigInteger()
$table->bigInteger('facebook_user_id')->unsigned()->index();
// Normally you won't need to store the access token in the database
$table->string('access_token')->nullable();
});
}
public function down()
{
Schema::table('users', function(Blueprint $table)
{
$table->dropColumn(
'facebook_user_id',
'access_token'
);
});
}
}
Don't forget to run the migration.
$ php artisan migrate
If you plan on using the Facebook user ID as the primary key, make sure you have a column called id
that is an unsigned big integer and indexed. If you are storing the Facebook ID in a different field, make sure that field exists in the database and make sure to map to it in your model to your custom id name.
If you're using the Eloquent ORM and storing the access token in the database, make sure to hide the access_token
field from possible exposure in your User
model.
Don't forget to add the SyncableGraphNodeTrait
to your user model so you can sync your model with data returned from the Graph API.
# User.php
use LinkThrow\LumenFacebookSdk\SyncableGraphNodeTrait;
class User extends Eloquent implements UserInterface {
use SyncableGraphNodeTrait;
protected $hidden = ['access_token'];
}
After the user has logged in with Facebook and you've obtained the user ID from the Graph API, you can log the user into Laravel by passing the logged in user's User
model to the Auth::login()
method.
class FacebookController {
public function getUserInfo(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb) {
try {
$response = $fb->get('/me?fields=id,name,email');
} catch (Facebook\Exceptions\FacebookSDKException $e) {
dd($e->getMessage());
}
// Convert the response to a `Facebook/GraphNodes/GraphUser` collection
$facebook_user = $response->getGraphUser();
// Create the user if it does not exist or update the existing entry.
// This will only work if you've added the SyncableGraphNodeTrait to your User model.
$user = App\User::createOrUpdateGraphNode($facebook_user);
// Log the user into Laravel
Auth::login($user);
}
}
If you have multiple Facebook apps that you'd like to use in the same script or you want to tweak the settings during runtime, you can create a new instance of LumenFacebookSdk
with the custom settings.
Route::get('/example', function(LinkThrow\LumenFacebookSdk\LumenFacebookSdk $fb) {
// All the possible configuration options are available here
$fb2 = $fb->newInstance([
'app_id' => env('FACEBOOK_APP_ID2'),
'app_secret' => env('FACEBOOK_APP_SECRET2'),
'default_graph_version' => 'v2.5',
// . . .
]);
});
The Facebook PHP SDK throws Facebook\Exceptions\FacebookSDKException
exceptions. Whenever there is an error response from Graph, the SDK will throw a Facebook\Exceptions\FacebookResponseException
which extends from Facebook\Exceptions\FacebookSDKException
. If a Facebook\Exceptions\FacebookResponseException
is thrown you can grab a specific exception related to the error from the getPrevious()
method.
try {
// Stuffs here
} catch (Facebook\Exceptions\FacebookResponseException $e) {
$graphError = $e->getPrevious();
echo 'Graph API Error: ' . $e->getMessage();
echo ', Graph error code: ' . $graphError->getCode();
exit;
} catch (Facebook\Exceptions\FacebookSDKException $e) {
echo 'SDK Error: ' . $e->getMessage();
exit;
}
The LumenFacebookSdk does not throw any custom exceptions.
If your app is being served from within the context of an app canvas or Page tab, you'll likely see a TokenMismatchException
error when you try to view the app on Facebook. This is because Facebook will render your app by sending a POST request to it with a signed_request
param and since Laravel 5 has CSRF protection that is enabled for every non-read request, the error is triggered.
Although it's possible to disable this feature completely, it's certainly not recommended as CSRF protection is an important security feature to have on your site and it should be enabled on every route by default.
Add an exception to your canvas endpoint to the $except
array in your app\Http\Middleware\VerifyCsrfToken.php
file.
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
'my-app/canvas',
'my-app/page-tab',
// ... insert all your canvas endpoints here
];
}
The tests are written with phpunit
. You can run the tests from the root of the project directory with the following command.
$ ./vendor/bin/phpunit
Please see CONTRIBUTING for details.
This package is maintained by Sammy Kaye Powers. See a full list of contributors.
The MIT License (MIT). Please see License File for more information.