I am trying to build a server-to-server auth flow using the Facebook PHP SDK and no Javascript, as outlined here. So far, I have successfully created a LoginUrl that lets the User sign in with Facebook, then redirect back to my App and check the state parameter for CSFR protection.
My Problem is, that I can't seem to get the API-call working that should swap my Auth Code for an access token. I pillaged every similar problem anyone else that Google was able to find had encountered for possible solutions.
Yet the end result was always the same: no access token, no error message that I could evaluate.
Researching the topic yielded the following advice, which I tested:
The URL specified in the App Settings must be a parent folder of $appUrl.
use curl to make the request instead of the SDK function api()
I've been at this for 2 days straight now and really could use some help.
<?php
require '../inc/php-sdk/src/facebook.php';
// Setting some config vars
$appId = 'MY_APP_ID';
$secret = 'MY_APP_SECRET';
$appUrl = 'https://MY_DOMAIN/appFolder';
$fbconfig = array('appId'=>$appId, 'secret'=>$secret);
$facebook = new Facebook($fbconfig);
// Log User in with Facebook and come back with Auth Code if not yet done
if(!(isset($_SESSION['login']))){
$_SESSION['login']=1;
header('Location: '.$facebook->getLoginUrl());
}
// process Callback from Facebook User Login
if($_SESSION['login']===1) {
/* CSFR Protection: getLoginUrl() generates a state string and stores it
in "$_SESSION['fb_'.$fbconfig['appId'].'_state']". This checks if it matches the state
obtained via $_GET['state']*/
if (isset($_SESSION['fb_'.$fbconfig['appId'].'_state'])&&isset($_GET['state'])){
// Good Case
if ($_SESSION['fb_'.$fbconfig['appId'].'_state']===$_GET['state']) {
$_SESSION['login']=2;
}
else {
unset($_SESSION['login']);
echo 'You may be a victim of CSFR Attacks. Try logging in again.';
}
}
}
// State check O.K., swap Code for Token now
if($_SESSION['login']===2) {
$path = '/oauth/access_token';
$api_params = array (
'client_id'=>$appId,
'redirect_uri'=>$appUrl,
'client_secret'=>$secret,
'code'=>$_GET['code']
);
$access_token = $facebook->api($path, 'GET', $api_params);
var_dump($access_token);
}
The easiest way I found to do this is to extend the Facebook class and expose the protected getAccessTokenFromCode() method:
<?php
class MyFacebook extends Facebook {
/** If you simply want to get the token, use this method */
public function getAccessTokenFromCode($code, $redirectUri = null)
{
return parent::getAccessTokenFromCode($code, $redirectUri);
}
/** If you would like to get and set (and extend), use this method instead */
public function setAccessTokenFromCode($code)
{
$token = parent::getAccessTokenFromCode($code);
if (empty($token)) {
return false;
}
$this->setAccessToken($token);
if (!$this->setExtendedAccessToken()) {
return false;
}
return $this->getAccessToken();
}
}
I also included a variation on the convenience method I use to set the access token, since I don't actually need a public "get" method in my own code.
Related
Form redirect for confirmation can be currently managed using one of these two options:
1/ Flash message: using flashbag on the form page or another page like this:
$this->addFlash('success', 'Thank you');
return $this->redirectToRoute('confirmation_page');
2/ Confirmation page: using a dedicated confirmation like this:
return $this->redirectToRoute('confirmation_page');
BUT using option 2 makes the confirmation_page directly accessible from the browser without having submitted the form before. I am currently using flashbag mechanism to fix it by adding a $this->addFlash('success', true); before the redirection in the form and then checking the flashbag content in the confirmation page so that the route is accessible only once after being successfully redirected from the form.
Is there any best practice or more appropriate way to manage it?
/**
* #Route("/confirmation", methods="GET", name="confirmation_page")
*/
public function confirmation(): Response
{
$flashbag = $this->get('session')->getFlashBag();
$success = $flashbag->get("success");
if (!$success) {
return $this->redirectToRoute('app_home');
}
return $this->render('templates/confirmation.html.twig');
}
Flash Message is designed to display messages. Instead, use sessions in your application.
When submitting the confirmation form, create a variable in the session before the redirect
$this->requestStack->getSession()->set('verifyed',true);
return $this->redirectToRoute('confirmation_page');
Use the created variable in your method
public function confirmation(): Response
{
if (!$this->requestStack->getSession()->get('verifyed')) {
return $this->redirectToRoute('app_home');
}
return $this->render('templates/confirmation.html.twig');
}
Don't forget to inject the RequestStack into your controller
private RequestStack $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
I'm using CakePHP 2.3.1 and this plugin: https://github.com/webtechnick/CakePHP-Facebook-Plugin to implement a facebook authentication. I followed this screen cast http://tv.cakephp.org/video/webtechnick/2011/01/12/nick_baker_--_facebook_integration_with_cakephp but it doesn't work, I can't receive the Facebook user information. I mean $this->Connect->user() always return null.
First make sure your user as granted access to your app. Then make a call to retrieve personal details through the api(/me) => $this->Connect->user()
Personally, I use this
public function beforeFilter(){
if($this->Connect->FB->getUser() != 0){
$this->set('facebookUser', $this->Connect->user());
}
Initiating ConnectComponent will populate (or not) the Auth Session. If no id is to be found, redirect user to a login dialog.
Call this method from beforefilter..and save the post data received in some variable like fb_data here.
protected function _valdiateFbRequest() {
if (!isset($this->request->data['signed_request'])) {
// not a valid request from fb
// throw exception or handle however you want
return;
}
$this->signedRequest = $this->request->data['signed_request'];
$this->fb_data=$this->Connect->registrationData(); //save fb data
unset($this->request->data['signed_request']);
if (empty($this->request->data)) {
$this->Security->csrfCheck = false;
}
// validate the request
}
I use Webtechnick Facebook plugin for cakephp 1.3 website. I implemented it about a year ago. And it worked fine until now. But today I found out that when I try to login(as a new user) it does not save facebook user data, because $this->Connect->user() (which result is taken from $this->FB->api('/me'), /plugins/facebook/controller/components/connect.php, line 194) returns nothing. I tried also, this facebook plugin on another cakephp 2.0 website, but the same thing was there.
I think, that there was some change in facebook api, because I did not absolutely make any change on the website, which could bring to that result.
this is user function in connect.php component
function user($field = null){
if(isset($this->uid)){
$this->uid = $this->uid;
if($this->Controller->Session->read('FB.Me') == null){
$this->Controller->Session->write('FB.Me', $this->FB->api('/me'));
}
$this->me = $this->Controller->Session->read('FB.Me');
}
else {
$this->Controller->Session->delete('FB');
}
if(!$this->me){
return null;
}
if($field){
$retval = Set::extract("/$field", $this->me);
return empty($retval) ? null : $retval[0];
}
return $this->me;
}
and my beforeFacebookSave() function in app_controller
public function beforeFacebookSave() {
$fbUser = $this->Connect->user ();
//debug($fbUser); // outputs nothing
$this->Connect->authUser ['User'] ['email'] = $fbUser ['email'];
$this->Connect->authUser ['User'] ['first_name'] = $fbUser ['first_name'];
$this->Connect->authUser ['User'] ['last_name'] = $fbUser ['last_name'];
return true;
}
Thank you !
There was a certificate change on Facebook that wasn't reflected in the SDK (because it used the old certificate). Since the plugin is based on PHP SDK, you should just fetch the latest version of the repo https://github.com/webtechnick/CakePHP-Facebook-Plugin. The author has pushed the commit to include the new PHP SDK with the new certificate.
https://github.com/webtechnick/CakePHP-Facebook-Plugin/tree/master/Vendor
Your error log should have a Facebook Exception due to SSL problems which chokes the API calls causing /me to return empty.
try this, it work for me
$infos = $facebook->api('/me?fields=id,first_name,last_name,picture,email');
In servicestack OAuth implementation I only saw possibility to automatically login with eg. facebook account.
But is there abbility to support registration process with facebook login. What I wanted is to let users login to facebook app, and then load their Name, Surname and email and prefill needed text boxes for real registration on my site (since I also have to have mobile phone verification etc.) I don't want user to be authorized and authenticated when he logs in with facebook. Only credentials login should be valid one for full site access.
Edit: I found a solution.
In FacebookProvider.cs
public override bool IsAuthorized(IAuthSession session, IOAuthTokens tokens, Auth request = null)
{
if (request != null)
{
if (!LoginMatchesSession(session, request.UserName)) return false;
}
return tokens != null && session.UserName!=null && !string.IsNullOrEmpty(tokens.AccessTokenSecret);
}
The catch was the && session.UserName!=null part. So we can check if user is logged in using credentials, this will be !=null and user can use all services. If not, this will be ==null and he can only get facebook info from session.
The SocialBootstrap API project shows an example of handling the callback after a successful Authentication by overriding the OnAuthenticated() hook of its custom user session:
I've pulled out, rewrote some and highlighted some of the important bits:
public class CustomUserSession : AuthUserSession
{
public override void OnAuthenticated(IServiceBase authService,
IAuthSession session,
IOAuthTokens tokens,
Dictionary<string, string> authInfo)
{
base.OnAuthenticated(authService, session, tokens, authInfo);
//Populate matching fields from this session into your own MyUserTable
var user = session.TranslateTo<MyUserTable>();
user.Id = int.Parse(session.UserAuthId);
user.GravatarImageUrl64 = CreateGravatarUrl(session.Email, 64);
foreach (var authToken in session.ProviderOAuthAccess)
{
if (authToken.Provider == FacebookAuthProvider.Name)
{
user.FacebookName = authToken.DisplayName;
user.FacebookFirstName = authToken.FirstName;
user.FacebookLastName = authToken.LastName;
user.FacebookEmail = authToken.Email;
}
else if (authToken.Provider == TwitterAuthProvider.Name)
{
user.TwitterName = authToken.DisplayName;
}
}
//Resolve the DbFactory from the IOC and persist the user info
using (var db = authService.TryResolve<IDbConnectionFactory>().Open())
{
//Update (if exists) or insert populated data into 'MyUserTable'
db.Save(user);
}
}
//Change `IsAuthorized` to only verify users authenticated with Credentials
public override bool IsAuthorized(string provider)
{
if (provider != AuthService.CredentialsProvider) return false;
return base.IsAuthorized(provider);
}
}
Basically this user-defined custom logic (which gets fired after every successful authentication) extracts data from the UserSession and stores it in a custom 'MyUserTable'.
We've also overridden the meaning of IsAuthorized to only accept users that have authenticated with CredentialsAuth.
You can use this data to complete the rest of the registration.
Other possible customizations
ServiceStack's built-in Auth persists the AuthData and populates the Session automatically for you. If you want to add extra validation assertions you can simply use your own custom [Authentication] attribute instead containing additional custom logic. Look at the implementation of the built-in AuthenticateAttribute as a guide.
there is a custom login form that should give users access to certain contents on the same page. That works so far with Users stored as Members in the SS database and I was checking after Login if the user has permissions like this in the Page Class:
function isAllowed() {
if (Member::currentUser()) {
$PresseGroup = DataObject::get_one('Group', "Code = 'presse'");
$AdminGroup = DataObject::get_one('Group', "Code = 'administrators'");
if (Member::currentUser()->inGroup($PresseGroup->ID) || Member::currentUser()->inGroup($AdminGroup->ID)) {
return true;
}
}
}
in the Template I just did this:
<% if isAllowed %>
SecretContent
<% end_if %>
OK so far, but now the users will not be stored in the silverstripe database - they are stored on a another server.
On that external server is running a little php script accepting the username and password. The script just returns user has permission: true or false.
I´m calling that script via cURL.
I planned to overwrite the dologin Function of MemberLoginForm. Now I just wonder how to check after Login that the User got the permission and display the contents... I tried to set a variable in the controller of the Page or should I set a session Variable? Thats my attempt (CustomLoginForm extends MemberLoginForm):
public function dologin($data) {
if(userHasPermission("user1", "pw")==true){
$this->controller->Test("test");
}
$link = $this->controller->Link();
$this->performLogin($data);
$this->controller->redirect($link);
}
I hope someone can help me with that - I know very specific - problem.
Many thanx,
Florian
In SilverStripe you can create a custom authenticator, which means users can log in on your website with accounts that are stored somewhere else, or even just a hard coded user and password.
You can check out the OpenID Authentication Module for example code on how to do it
But for your task this might even be to complex of a solution, how about after login just do something like Session::set('isAllowed', true); and to check if the user is allowed to view:
function isAllowed() {
if (Member::currentUser()) {
$PresseGroup = DataObject::get_one('Group', "Code = 'presse'");
$AdminGroup = DataObject::get_one('Group', "Code = 'administrators'");
if (Member::currentUser()->inGroup($PresseGroup->ID) || Member::currentUser()->inGroup($AdminGroup->ID)) {
return true;
}
}
// if Member::currentUser() is not allowed to view,
// return the session, which is either set to true or it returns null if not set
return Session::get('isAllowed');
}