OAuth авторизация через facebook

OAuth авторизация через facebook

iSergium

Facebook вряд ли нуждается в представлении — его знает каждый активный пользователь интернета. Это крупнейшая социальная сеть и второй по посещаемости сайт в мире, который вскоре отметит десятилетний юбилей. Количество зарегистрированных пользователей этой сети перевалило за миллиард еще в 2012 году, ежедневная аудитория сайта — более 500 миллионов человек. Количество русскоязычных пользователей facebook на март 2013 составило около 9 миллионов человек. Достаточно, чтобы упростить этим людям авторизацию на Вашем сайте?

facebook by FDP

Шаг 0. Подготовка

Если у Вас еще нет аккаунта facebook, то регистрируемся, что ли. После ввода данных придется пропустить (или заполнить) 4 ненужных шага (поиск друзей, ввод дополнительной информации, выбор увлечений, загрузка фото).
Далее вам дорога на developers.facebook.com. Регистрируемся как разработчик (зеленая кнопка "Register Now" в правом верхнем углу) в 4 шага. На первом не теряйтесь — принимайте условия и продолжайте. На втором нужно указывать номер телефона (странным образом с МТС facebook не дружит и смс на него лично мне не приходили). Заполнять данные на третьем шаге не обязательно — можно пропустить. А на четвертом просто принимаем поздравления. (Интерфейс мог успеть измениться со времени создания скриншотов)

coddism developers facebook congratulations

Создаем новое приложение, кнопка "Создать новое приложение" ("Create New App"). Достаточно ввести только App Name. Остальные данные не обязательны для заполнения, но основные иконки всё же лучше загрузить. У приложений довольно много настроек (в том числе ненужных), навигация по ним реализована в меню слева.
Все созданные приложения доступны по этой ссылке.
Данные приложения можно посмотреть и в его просмотре и в редактировании его основных настроек.

coddism developers facebook

Остальные шаги.

Основные определения были описаны в начале этой статьи, почитайте вступление, если еще не читали. Про redirect() было сказано здесь.

Как всегда, создаём класс и начинаем с констант. И, как всегда, скачать конечный результат можно ниже из раздела "дополнительно".

class OAuthFB {
    const APP_ID = 1234567890; //App ID/API Key
    const APP_SECRET = 'sometestappsecret'; //App Secret
    const URL_CALLBACK = 'http://example.com/oauth/fb.php'; //URL, на который произойдет перенаправление после авторизации
}

Первым делом необходимо перенаправление пользователя на фэйсбук для авторизации и подтверджения доступа. Да, здесь используются сессии, потому перед вызовом метода нужно будет стартануть сессию, ниже это будет расписано. Добавляем метод (sprinf понадобился для того, чтобы при преобразовании числа в строку не было конвертации во что-то такое: 1.23456789E+10):

class OAuthFB {
...
    const URL_OATH = 'https://www.facebook.com/dialog/oauth';
    public static function goToAuth()
    {
        $_SESSION['state'] = md5(uniqid(rand(), TRUE));
        Utils::redirect(self::URL_OATH .
            '?client_id=' . sprintf('%.0f', self::APP_ID) .
            '&redirect_uri=' . urlencode(self::URL_CALLBACK) .
            "&state=" . $_SESSION['state']);
    }
...
}

После авторизации и подтверждения доступа фэйсбук перекидывает пользователя на адрес URL_CALLBACK?code=xxx&state=yyy.  Если пользователь не разрешил приложению доступ к своим данным, то фэйсбук перенаправит его на URL_CALLBACK вот с таким GET-массивом (state будет отличаться):

Array
(
    [error] => access_denied
    [error_code] => 200
    [error_description] => Permissions error
    [error_reason] => user_denied
    [state] => 7262836fbd03301ee4d3291b15044ca6
)

В случае удачного ответа нужно сверить state. Если всё совпадает (а не совпасть они могут только в случае несанкционированного доступа), то запрашиваем токен доступа, а по токену - данные пользователя. Вот все три функции:

class OAuthFB {
    const URL_ACCESS_TOKEN = 'https://graph.facebook.com/oauth/access_token';
    const URL_GET_ME = 'https://graph.facebook.com/me';
...
    public static function checkState($state) {
        return (isset($_SESSION['state']) && ($_SESSION['state'] === $state));
    }
 
    public static function getToken($code) {
        $url = self::URL_ACCESS_TOKEN .
            '?client_id=' . sprintf('%.0f', self::APP_ID) .
            '&redirect_uri=' . urlencode(self::URL_CALLBACK) .
            '&client_secret=' . self::APP_SECRET .
            '&code=' . $code;
 
        if (!($response = @file_get_contents($url))) {
            return false;
        }
 
        parse_str($response, $result);
 
        if (empty($result['access_token'])) {
            return false;
        }
 
        self::$token = $result['access_token'];
        return true;
    }
 
    public static function getUser() {
        if (!self::$token) {
            return false;
        }
 
        $url = self::URL_GET_ME . '?fields=id,name&access_token=' . self::$token;
 
        if (!($user = @file_get_contents($url))) {
            return false;
        }
 
        $user = json_decode($user);
        if (empty($user)) {
            return false;
        }
 
        self::$userId = $user->id;
        return self::$userData = $user;
    }
...
}

В итоге общая логика выглядит так:

session_start();
if (!empty($_GET['error'])) {
    // Пришёл ответ с ошибкой.
} elseif (empty($_GET['code'])) {
    // Самый первый запрос. Отправляем пользователя на авторизацию
    OAuthFB::goToAuth();
} else {
    // Ответ от facebook пришёл удачный:
    // Проверка state
    if (!OAuthFB::checkState($_GET['state'])) {
        die("The state does not match. You may be a victim of CSRF.");
    }
    // Запрос токена
    if (!OAuthFB::getToken($_GET['code'])) {
        die('Error - no token by code');
    }
    // Запрос данных пользователя
    $user = OAuthFB::getUser();
    print_r($user);
}

Ответ будет в таком формате:

stdClass Object
(
    [id] => 6815841748
    [name] => Barack Obama
)

Или, если указать немного расширенный fields в getUser(), в таком:

stdClass Object
(
    [about] => This page is run by Organizing for Action. To visit the White House Facebook page, go to facebook.com/WhiteHouse.
    [birthday] => 08/04/1961
    [category] => Politician
    [category_list] => Array
        (
            [0] => stdClass Object
                (
                    [id] => 1700
                    [name] => Politician
                )
 
        )
 
    [checkins] => 22
    [is_published] => 1
    [location] => stdClass Object
        (
            [street] =>
            [city] => Washington
            [state] => DC
            [country] => United States
            [zip] =>
            [latitude] => 38.898634123938
            [longitude] => -77.036682644343
        )
 
    [talking_about_count] => 1241693
    [username] => barackobama
    [website] => http://www.barackobama.com http://www.whitehouse.gov/
    [were_here_count] => 0
    [id] => 6815841748
    [name] => Barack Obama
    [link] => http://www.facebook.com/barackobama
    [likes] => 36444396
    [cover] => stdClass Object
        (
            [cover_id] => 10151369410101749
            [source] => https://fbcdn-sphotos-h-a.akamaihd.net/hphotos-ak-ash3/s720x720/58279_10151369410101749_598350508_n.jpg
            [offset_y] => 6
            [offset_x] => 0
        )
 
)

Полный список полей, которые можно запросить, находится здесь, необходимые для запроса поля права там указаны. В моем примере запроса используется fields=id,name - запрашиваются только id и имя пользователя. Чтобы запросить данные кого-либо другого достаточно сменить "me" в URL_GET_ME на id или username "жертвы", например https://graph.facebook.com/barackobama.

coddism авторизация facebook

Дополнительно: