Недавно 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-приложения.
Только убедившись в подлинности токена можно доверять предоставленным данным.
Из документации 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
. В примере используется ее вторая версия, которую можно легко установить с помощью композера:
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:
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-авторизации. Надеюсь, вам было полезно!
Это текст из личного блога, опубликованный с разрешения автора.
Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: