ru:https://highload.today/blogs/facebook-limited-login-for-developers/ ua:https://highload.today/uk/blogs/facebook-limited-login-for-developers/
logo
Вопросы      14/02/2022

Facebook создал проблему бэкендерам: как работать с новой системой авторизации

Николай Коваленко BLOG

Backend Developer в Ronis Business Tools

Недавно Facebook предложил вариант реализации ограниченного логина (Limited Login) для разработчиков приложений. Особенность нового способа авторизации в том, что данные конечного пользователя, использующего вход в Facebook через приложение, не используются для персонализации или измерения эффективности рекламы соцсети.

Для пользователей выгода очевидна — конфиденциальность. Facebook при этом может выполнять последние требования мобильных операционных систем.

А вот у бэкенд-разработчиков веб-приложений с iOS-фронтендом появилась новая проблема — необходимость валидации Facebook-токена авторизации (AuthenticationToken), завернутого в токен OpenID Connect JWT.

На момент написания статьи необходимость обязательного использования ограниченного вход в Facebook относится только к iOS-приложениям. Это связано с новыми условиями использования iOS 14.5 и принудительным использованием инфраструктуры прозрачности отслеживания приложений.

Что касается iOS, разработчикам разрешено использовать обычный вход в Facebook на iOS с версией 14.4 или ниже. Но с версии 14.5 разработчики должны показывать всплывающее окно ATT и выбирать способ входа в систему в зависимости от ответа пользователя.

Как работает эта технология в Facebook, можно прочитать в описании технологии Limited Login для разработчиков.

Валидация Limited Login OIDC Facebook-токена

Процесс валидации токена также подробно описан в доке выше. Но примеров реализации валидации нового токена Facebook не предоставляет, что вызывает некоторые проблемы с имплементацией, если вы раньше не сталкивались с такими понятиями как OpenID, JWK, PEM и JWT.

Успешный вход в Facebook с использованием технологии ограниченного входа возвращает AuthenticationToken, который представляет собой JWT-токен, всю необходимую информацию о пользователе и данные, которые требуются для ее валидации.

Чтобы убедиться в валидности данных, нужно проверить срок действия токена и  достоверность подписи данных. Также важно проверить, что токен выдан именно Facebook и именно для вашего Facebook-приложения.

Только убедившись в подлинности токена можно доверять предоставленным данным.

Онлайн-курс "Маркетингова аналітика" від Laba.
Опануйте інструменти для дослідження ринку й аудиторії та проведення тестувань.Дізнайтесь, як оптимізувати поточні рекламні кампанії та будувати форкасти наступних.
Детальніше про курс

Из документации Facebook и логики работы используемых технологий можно сделать вывод, что реализация процесса получения данных пользователя из AuthenticationToken для дальнейшего использования выглядит примерно так:

  • получаем связку паблик-ключей для Facebook OIDC в JWK-формате;
  • конвертируем ключ в в PEM-формат требуемый для валидации JWT-токена;
  • проверяем валидность и подлинность JWT-токена;
  • используем достоверные данные пользователя.

Реализация валидации OIDC Facebook-токена на PHP

Итак, приступим к реализации процесса получения подлинных данных пользователя из Facebook OIDC по порядку.

Точка входа oauth/openid/jwks возвращает связку паблик-ключей для Facebook OIDC-имплементации в JWK-формате. Для ее получения реализуем FacebookJWKProvider, предоставляющий паблик-ключ фейсбук по идентификатору ключа (kid):

class FacebookJWKProvider
{
    private const FACEBOOK_JWKS_SOURCE_URL = ‘https://www.facebook.com/.well-known/oauth/openid/jwks/’;

    /** @var Client */
    private $client;

    /**
    * @return string
    */
    private function getFacebookJWKByKeyId(string $keyId): string
    {
        $response = $this->getGuzzleHttpClient()->request(
            'GET',
            self::FACEBOOK_JWKS_SOURCE_URL
        );

        $jsonResponse = $response->getBody()->getContents();
        $decodedResponse = json_decode($jsonResponse, 512, JSON_THROW_ON_ERROR);
        $jwkSet = $decodedResponse[‘keys’];

        $mappedJwkSet = [];
        foreach ($jwkSet as $jwk) {
            $mappedJwkSet[$jwk['kid']] = $jwk;
        }

        return $mappedJwkSet[$keyId] ?? null;
    }

    /**
    * @return Client
    */
    private function getGuzzleHttpClient(): Client
    {
        if (!$this->client) {
            $this->client = new Client([
                'timeout' => 10,
            ]);
        }
        return $this->client;
    }
}

Далее нам нужно реализовать конвертер полученного ключа из JWK в PEM формат — JWKToPEMConvertor.

Для реализации конвертации публичного ключа Facebook из JWK в PEM-формат понадобится библиотека phpseclib. В примере используется ее вторая версия, которую можно легко установить с помощью композера:

Курс Digital Marketing від Mate academy.
На курсі ви навчитесь запускати рекламу в кабінтеах Facebook/Instagram та Google. Ви також познайомитися з SEO та Email marketing. Це ті навики які найчастіше просять ІТ компанії від Junior Marketers. А ми вас не лише навчимо, а й працевлаштуємо!
Дізнатися більше про курс

composer require phpseclib/phpseclib:2.0

Если вам потребуется использовать phpseclib третьей версии, то загрузку ключа придется немного переписать.

Также для реализации конвертора потребуется Base64UrlDecoder. К сожалению, язык программирования PHP не поддерживает Base64URL-стандарт, но не составит труда реализовать Base64URL-декодер стандартными функциями языка:

class Base64UrlDecoder
{
    /**
    * @param string $base64UrlEncodedString
    * @return string
    */
    private base64UrlDecode(string base64UrlEncodedString): string
    {
        return base64_decode(strtr($base64UrlEncodedString, '-_', '+/');
    }
}

Теперь реализуем сам JWKToPEMConvertor:

class JWKToPEMConvertor
{
    /** @var Base64UrlDecoder */
    private $base64UrlDecoder;

    /**
    * @param Base64UrlDecoder $base64UrlDecoder
    */
    public function __construct(
    Base64UrlDecoder $base64UrlDecoder
    ) {
        $this->base64UrlDecoder = $base64UrlDecoder;
    }

    /**
    * @param array $jwk
    * @return string
    */
    public function convert(array $jwk): string
    {
        if (
            !array_key_exists('e', $jwk)
            || !array_key_exists('n', $jwk)
            || !array_key_exists('kty', $jwk)
        ) {
            throw new \InvalidArgumentException('Invalid JWK');
        }

        if ($jwk['kty'] !== 'RSA') {
            throw new \InvalidArgumentException('RSA key type is currently only supported.');
        }

        $keySrc = [
            'e' => new BigInteger(
                base64_decode($jwk['e']),
                256
            ),
            'n' => new BigInteger(
                $this->base64UrlDecoder->base64UrlDecode($jwk['n']),
                256
            ),
        ];

        $rsa = new RSA();
        $rsa->loadKey($keySrc);

        return $rsa->getPublicKey();
        //return PublicKeyLoader::load($keySrc); // phpseclib 3.0+ variant
    }
}

Итак, у нас есть JWT-токен и теперь мы можем получить паблик-ключ Facebook и конвертировать его в PEM-формат.

Осталось декодировать токен, выполнить валидацию и проверить его подлинность.

Декодировать токен проще всего при помощи firebase-библиотеки firebase/php-jwt, которую можно легко установить через композер:

composer require firebase/php-jwt:5.0.0

Реализуем для декодирования и валидации токена FacebookAuthenticationTokenVerifier:

Онлайн-інтенсив "Як створити рекомендаційну модель за 2 дні" від robot_dreams.
Ви пройдете етапи вибору, навчання, оцінки рекомендаційної моделі для електронної бібліотеки та отримаєте індивідуальний фідбек від лекторки.
Приєднатись до інтенсиву
class FacebookAuthenticationTokenVerifier
{
    private const FACEBOOK_ISSUER = 'https://facebook.com';

    /** @var FacebookJWKProvider */
    public $facebookJWKProvider;

    /** @var JWKToPEMConvertor */
    public $jWKToPEMConvertor;

    /** @var Base64UrlDecoder */
    private $base64UrlDecoder;

    /** @var string */
    private $facebookApplicationId;

    /**
    * @param FacebookJWKProvider $facebookJWKProvider
    * @param JWKToPEMConvertor $jWKToPEMConvertor
    * @param Base64UrlDecoder $base64UrlDecoder
    * @param string $facebookApplicationId
    */
    public function __construct(
        FacebookJWKProvider $facebookJWKProvider,
        JWKToPEMConvertor $jWKToPEMConvertor,
        Base64UrlDecoder $base64UrlDecoder,
        string $facebookApplicationId
    ) {
        $this->facebookJWKProvider = $facebookJWKProvider;
        $this->jWKToPEMConvertor = $jWKToPEMConvertor;
        $this->base64UrlDecoder = $base64UrlDecoder;
        $this->facebookApplicationId = $facebookApplicationId;
    }

    /**
    * @param string $authenticationToken
    * @return string|null
    */
    public function getVerifiedAuthenticationToken(string $authenticationToken): ?string
    {
        list($encodedHeader) = explode('.', $authenticationToken);
        $header = json_decode(
            $this->base64UrlDecoder->base64UrlDecode($encodedHeader)),
            true,
            512,
            JSON_THROW_ON_ERROR
        );

        if (!($publicKeyId = $header['kid'] ?? null)) {
            throw new \InvalidArgumentException('Key id in facebook authentication token not found');
        }

        $jwk = $this->facebookJWKProvider->getFacebookJWKByKeyId($publicKeyId);
        $publicKeyAsPem = $this->jWKToPEMConvertor->convert($jwk);
        $decodedAuthenticationToken = JWT::decode(
            $authenticationToken,
            $publicKeyAsPem,
            ['RS256']
        );

        if (decodedAuthenticationToken->iss !== self::FACEBOOK_ISSUER) {
            throw new \InvalidArgumentException('Token Issuer is invalid');
        }

        if ($decodedAuthenticationToken->aud !== $this->facebookApplicationId) {
            throw new \InvalidArgumentException('Token Application ID is invalid');
        }

        return $decodedAuthenticationToken;
    }
}

Верифицированный токен представляет собой объект с набором данных пользователя, которые можно смело использовать как доверенные:

{
    "iss": "https://facebook.com",
    "aud": "Facebook Application ID",
    "sub": "Facebook User ID",
    "iat": 1640871169,
    "exp": 1640874769,
    "jti": "Token UID"
    "nonce": "Your NONCE",
    "given_name": "Facebook User First Name",
    "family_name": "Facebook User Second Name",
    "name": "Facebook User Full Name",
    "picture": "https://platform-lookaside.fbsbx.com/platform/profilepic/picture-url"
}

Вот такая вот оказалась простая да не очень задача по верификации токена Facebook-авторизации. Надеюсь, вам было полезно!

Это текст из личного блога, опубликованный с разрешения автора.

If you have found a spelling error, please, notify us by selecting that text and pressing Ctrl+Enter.

Курс Project Manager від Powercode academy.
Онлайн-курс Project Manager. З нуля за 3,5 місяці до нової позиції Без знання коду, англійської та стресу.
Зарееструватися

Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.

Ваша жалоба отправлена модератору

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: