Ограниченный вход (Limited Login) от фейсбук
Недавно фейбук предложил вариант реализации ограниченного логина (Limited Login) для разработчиков приложений. Особенность нового способа авторизации в том, что данные конечного пользователя, использующего вход в Facebook через приложение, не используются для персонализации или измерения эффективности рекламы Facebook.
Для пользователей выгода очевидна — конфиденциальность. Фейсбук при этом может выполнять последние требования мобильных операционных систем. А вот у бекенд разработчиков веб приложений с iOS фронтендом появилась новая проблема — необходимость валидации фейсбук токена авторизации (AuthenticationToken) завернутого в OpenID Connect JWT токен.
На момент написания статьи необходимость обязательного использования ограниченного вход в Facebook относится только к iOS приложениям. Это связано с новыми условиями использования iOS 14.5 и принудительным использованием инфраструктуры прозрачности отслеживания приложений.
Что касается iOS, разработчикам разрешено использовать обычный вход в FB на iOS с версией 14.4 или ниже. Но с версии 14.5 разработчики должны показывать всплывающее окно ATT и выбирать способ входа в систему в зависимости от ответа пользователя.
Как работает данная технология в фейсбук можно прочитать в описании Limited Login технологии для разработчиков от фейсбук.
Валидация Limited Login OIDC фейсбук токена
Процесс валидации токена также подробно описан в описании валидации токена для разработчиков от фейсбук. Но примеров реализации валидации нового токена ни фейсбук не предоставляет, что вызывает некоторые проблемы с имплементацией, если вы ранее не сталкивались с такими понятиями как: OpenID, JWK, PEM и JWT.
Успешный вход в фейсбук с использованием технологии ограниченного входа возвращает AuthenticationToken, который представляет собой JWT токен, всю необходимую информацию о пользователе и данные, которые требуются для ее валидации.
Чтобы убедиться в валидности данных необходимо проверить срок действия токена и достоверность подписи данных. Также важно проверить, что токен выдан именно фейсбуком и именно для вашего фейсбук приложения.
Только убедившись в подлинности токена можно доверять предоставленным данным.
Из документации фейсбука и логики работы используемых технологий, можно сделать вывод, что реализация процесса получения данных пользователя из AuthenticationToken для дальнейшего использования выглядит примерно так:
- Получаем связку паблик ключей для Facebook OIDC в JWK формате;
- Конвертируем ключ в в PEM формат требуемый для валидации JWT токена;
- Проверяем валидность и подлинность JWT токена;
- Используем достоверные данные пользователя.
Реализация валидации OIDC фейсбук токена на 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.
Для реализации конвертации публичного ключа фейсбука из JWK в PEM формат понадобится библиотека phpseclib. В примере используется ее вторая версия, которую можно легко установить с помощью композера:
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 токен, мы можем теперь получить паблик ключ фейсбука и конвертировать его в PEM формат. Осталось декодировать токен, выполнить валидацию и проверить его подлинность.
Декодировать токен проще всего при помощи firebase библиотеки firebase/php-jwt, которую можно легко установить через композер:
composer require firebase/php-jwt:5.0.0
Реализуем для декодирования и валидации токена FacebookAuthenticationTokenVerifier.
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" }
Вот такая вот оказалась простая не простая задача по верификации токена фейсбук авторизации.