refactor(oauth): drop the hand-written providers and keep only what the SDK ships
The previous commit added five direct clients (Douyin, Microsoft, Facebook, Twitter, Apple) written against each platform's own endpoints. Maintaining five bespoke OAuth implementations — including Apple's ES256 client secret and Twitter's PKCE — is maintenance the bundled yurunsoft SDK does not ask for, and none of them could be verified without production credentials. Only providers yurunsoft/yurun-oauth-login actually implements stay on the direct surface: GitHub, Gitee, QQ, Google, Weibo, Weixin, Baidu and Alipay. Baidu and Alipay move into OAuthProviderBroker's existing client() switch and normalize() map rather than living in a separate class, so there is no new layer to reason about. OAuthProviderClient, its harness and its contract test are deleted outright (-809 lines). The Apple form_post handling added to handle_callback() is reverted too — without Apple there is no POST callback to read. The aggregate-login channel keeps its 16 platforms, matching the reference; that list describes what the aggregation provider offers, not a local implementation. Brand icons for those names stay for the same reason. Verification: 869 widget/navigation tests pass, the PSR-4 audit reports 0 errors, and the catalog constant, PHP schema and Vue schema now list the same 16 aggregate platforms.
This commit is contained in:
1 parent
6db34646ed
commit
b1c3d86e44
8 files changed
+55
-809
No files matched your search
@@ -327,13 +327,8 @@ class SchemaUsers {
|
||||
['label' => 'Google', 'value' => 'google'],
|
||||
['label' => '微博', 'value' => 'weibo'],
|
||||
['label' => '微信', 'value' => 'weixin'],
|
||||
['label' => '抖音', 'value' => 'douyin'],
|
||||
['label' => '百度', 'value' => 'baidu'],
|
||||
['label' => '支付宝', 'value' => 'alipay'],
|
||||
['label' => 'Microsoft', 'value' => 'microsoft'],
|
||||
['label' => 'Facebook', 'value' => 'facebook'],
|
||||
['label' => 'Twitter', 'value' => 'twitter'],
|
||||
['label' => 'Apple', 'value' => 'apple'],
|
||||
], 'dependsOn' => ['key' => 'oauth_proxy_mode', 'value' => 'server']],
|
||||
['key' => 'oauth_proxy_client_providers', 'label' => '客户端启用平台', 'type' => 'checkbox', 'default' => ['github', 'gitee', 'qq', 'google'], 'options' => [
|
||||
['label' => 'GitHub', 'value' => 'github'],
|
||||
@@ -342,13 +337,8 @@ class SchemaUsers {
|
||||
['label' => 'Google', 'value' => 'google'],
|
||||
['label' => '微博', 'value' => 'weibo'],
|
||||
['label' => '微信', 'value' => 'weixin'],
|
||||
['label' => '抖音', 'value' => 'douyin'],
|
||||
['label' => '百度', 'value' => 'baidu'],
|
||||
['label' => '支付宝', 'value' => 'alipay'],
|
||||
['label' => 'Microsoft', 'value' => 'microsoft'],
|
||||
['label' => 'Facebook', 'value' => 'facebook'],
|
||||
['label' => 'Twitter', 'value' => 'twitter'],
|
||||
['label' => 'Apple', 'value' => 'apple'],
|
||||
], 'dependsOn' => ['key' => 'oauth_proxy_mode', 'value' => 'client']],
|
||||
['key' => 'oauth_bind_type', 'label' => '绑定方式', 'type' => 'radio', 'default' => 'auto', 'options' => [
|
||||
['label' => '自动绑定', 'value' => 'auto'],
|
||||
@@ -365,13 +355,8 @@ class SchemaUsers {
|
||||
['key' => 'oauth_weibo_s', 'label' => '微博登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_weixin_s', 'label' => '微信开放平台登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_weixingzh_s', 'label' => '微信公众号登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_douyin_s', 'label' => '抖音登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_baidu_s', 'label' => '百度登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_alipay_s', 'label' => '支付宝登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_microsoft_s', 'label' => 'Microsoft 登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_facebook_s', 'label' => 'Facebook 登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_twitter_s', 'label' => 'Twitter 登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'oauth_apple_s', 'label' => 'Apple 登录', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'clogin_s', 'label' => '彩虹聚合登录', 'type' => 'switch', 'default' => false, 'help' => '通过第三方聚合登录平台统一接入 QQ、微信和微博'],
|
||||
]],
|
||||
['key' => 'github', 'title' => 'GitHub配置', 'collapsible' => true, 'defaultCollapsed' => true, 'fields' => [
|
||||
@@ -398,10 +383,6 @@ class SchemaUsers {
|
||||
['key' => 'weixin_appid', 'label' => 'AppID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'weixin_appsecret', 'label' => 'AppSecret', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => ''],
|
||||
]],
|
||||
['key' => 'douyin', 'title' => '抖音配置', 'collapsible' => true, 'defaultCollapsed' => true, 'fields' => [
|
||||
['key' => 'douyin_appid', 'label' => 'Client Key', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'douyin_appkey', 'label' => 'Client Secret', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => ''],
|
||||
]],
|
||||
['key' => 'baidu', 'title' => '百度配置', 'collapsible' => true, 'defaultCollapsed' => true, 'fields' => [
|
||||
['key' => 'baidu_appid', 'label' => 'API Key', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'baidu_appkey', 'label' => 'Secret Key', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => ''],
|
||||
@@ -411,25 +392,6 @@ class SchemaUsers {
|
||||
['key' => 'alipay_appkey', 'label' => '支付宝公钥', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'alipay_private_key', 'label' => '应用私钥', 'type' => 'textarea', 'secret' => true, 'default' => '', 'help' => 'RSA2 签名使用的应用私钥(PKCS8 格式)'],
|
||||
]],
|
||||
['key' => 'microsoft', 'title' => 'Microsoft 配置', 'collapsible' => true, 'defaultCollapsed' => true, 'fields' => [
|
||||
['key' => 'microsoft_appid', 'label' => 'Application (client) ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'microsoft_appkey', 'label' => 'Client Secret', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => ''],
|
||||
]],
|
||||
['key' => 'facebook', 'title' => 'Facebook 配置', 'collapsible' => true, 'defaultCollapsed' => true, 'fields' => [
|
||||
['key' => 'facebook_appid', 'label' => 'App ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'facebook_appkey', 'label' => 'App Secret', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => ''],
|
||||
]],
|
||||
['key' => 'twitter', 'title' => 'Twitter 配置', 'collapsible' => true, 'defaultCollapsed' => true, 'fields' => [
|
||||
['key' => 'twitter_appid', 'label' => 'Client ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'twitter_appkey', 'label' => 'Client Secret', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => ''],
|
||||
]],
|
||||
['key' => 'apple', 'title' => 'Apple 配置', 'collapsible' => true, 'defaultCollapsed' => true, 'fields' => [
|
||||
['key' => 'apple_appid', 'label' => 'Service ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'apple_appkey', 'label' => '备用 Secret', 'type' => 'input', 'inputType' => 'password', 'secret' => true, 'default' => ''],
|
||||
['key' => 'apple_team_id', 'label' => 'Team ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'apple_key_id', 'label' => 'Key ID', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'apple_private_key', 'label' => '私钥(.p8)', 'type' => 'textarea', 'secret' => true, 'default' => '', 'help' => '用于生成 client_secret 的 ES256 私钥'],
|
||||
]],
|
||||
['key' => 'weixingzh', 'title' => '微信公众号配置', 'collapsible' => true, 'defaultCollapsed' => true, 'fields' => [
|
||||
['key' => 'weixingzh_gzh_type', 'label' => '公众号类型', 'type' => 'radio', 'default' => 'service', 'options' => [
|
||||
['label' => '服务号', 'value' => 'service'],
|
||||
|
||||
@@ -23,9 +23,6 @@ final class OAuthProviderBroker {
|
||||
'state' => $state,
|
||||
]);
|
||||
}
|
||||
if (OAuthProviderClient::handles($provider)) {
|
||||
return OAuthProviderClient::auth_url($provider, $config, $redirect_uri, $state);
|
||||
}
|
||||
$oauth = $this->client($provider, $config, $redirect_uri);
|
||||
$scope = $provider === 'github' ? 'read:user user:email' : null;
|
||||
return (string) $oauth->getAuthUrl($redirect_uri, $state, $scope);
|
||||
@@ -39,9 +36,6 @@ final class OAuthProviderBroker {
|
||||
if ($provider === 'google') {
|
||||
return $this->google_identity($config, $code, $redirect_uri);
|
||||
}
|
||||
if (OAuthProviderClient::handles($provider)) {
|
||||
return OAuthProviderClient::identity($provider, $config, $code, $redirect_uri, $state);
|
||||
}
|
||||
$oauth = $this->client($provider, $config, $redirect_uri);
|
||||
$oauth->getAccessToken($state, $code, $state);
|
||||
if ($provider === 'qq') {
|
||||
@@ -49,6 +43,9 @@ final class OAuthProviderBroker {
|
||||
$info = (array) $oauth->getUserInfo();
|
||||
return $this->normalize($provider, $openid, $info);
|
||||
}
|
||||
if ($provider === 'baidu' || $provider === 'alipay') {
|
||||
return $this->normalize($provider, '', (array) $oauth->getUserInfo());
|
||||
}
|
||||
$info = (array) $oauth->getUserInfo();
|
||||
if ($provider === 'github') {
|
||||
return $this->github_identity($oauth, $info);
|
||||
@@ -68,6 +65,15 @@ final class OAuthProviderBroker {
|
||||
return new \Yurun\OAuthLogin\Weibo\OAuth2($config['client_id'], $config['client_secret'], $redirect_uri);
|
||||
case 'weixin':
|
||||
return new \Yurun\OAuthLogin\Weixin\OAuth2($config['client_id'], $config['client_secret'], $redirect_uri);
|
||||
case 'baidu':
|
||||
return new \Yurun\OAuthLogin\Baidu\OAuth2($config['client_id'], $config['client_secret'], $redirect_uri);
|
||||
case 'alipay':
|
||||
$client = new \Yurun\OAuthLogin\Alipay\OAuth2($config['client_id'], $config['client_secret'], $redirect_uri);
|
||||
// 支付宝用应用私钥做 RSA2 签名;client_secret 位存的是支付宝公钥。
|
||||
if (!empty($config['private_key'])) {
|
||||
$client->appPrivateKey = (string) $config['private_key'];
|
||||
}
|
||||
return $client;
|
||||
}
|
||||
throw new \RuntimeException(__('不支持的第三方登录平台', 'jio'));
|
||||
}
|
||||
@@ -183,6 +189,28 @@ final class OAuthProviderBroker {
|
||||
'email_verified' => false,
|
||||
'avatar_url' => (string) ($info['headimgurl'] ?? ''),
|
||||
];
|
||||
case 'baidu':
|
||||
return [
|
||||
'provider' => $provider,
|
||||
'provider_id' => (string) ($info['uid'] ?? $info['userid'] ?? ''),
|
||||
'username' => (string) ($info['uname'] ?? ''),
|
||||
'name' => (string) ($info['uname'] ?? ''),
|
||||
'email' => sanitize_email((string) ($info['email'] ?? '')),
|
||||
'email_verified' => false,
|
||||
// 百度旧域名会 403,换到可访问的 himg.bdimg.com。
|
||||
'avatar_url' => str_replace('tb.himg.baidu.com', 'himg.bdimg.com', (string) ($info['portrait'] ?? '')),
|
||||
];
|
||||
case 'alipay':
|
||||
$user = (array) ($info['user_info'] ?? $info);
|
||||
return [
|
||||
'provider' => $provider,
|
||||
'provider_id' => (string) ($info['user_id'] ?? $user['user_id'] ?? ''),
|
||||
'username' => (string) ($user['nick_name'] ?? ''),
|
||||
'name' => (string) ($user['nick_name'] ?? ''),
|
||||
'email' => '',
|
||||
'email_verified' => false,
|
||||
'avatar_url' => (string) ($user['avatar'] ?? ''),
|
||||
];
|
||||
}
|
||||
throw new \RuntimeException(__('不支持的第三方登录平台', 'jio'));
|
||||
}
|
||||
|
||||
@@ -42,11 +42,6 @@ final class OAuthProviderCatalog {
|
||||
'id_key' => 'weixin_appid',
|
||||
'secret_key' => 'weixin_appsecret',
|
||||
],
|
||||
'douyin' => [
|
||||
'label' => '抖音',
|
||||
'id_key' => 'douyin_appid',
|
||||
'secret_key' => 'douyin_appkey',
|
||||
],
|
||||
'baidu' => [
|
||||
'label' => '百度',
|
||||
'id_key' => 'baidu_appid',
|
||||
@@ -58,31 +53,6 @@ final class OAuthProviderCatalog {
|
||||
'secret_key' => 'alipay_appkey',
|
||||
'extra_keys' => ['private_key' => 'alipay_private_key'],
|
||||
],
|
||||
'microsoft' => [
|
||||
'label' => 'Microsoft',
|
||||
'id_key' => 'microsoft_appid',
|
||||
'secret_key' => 'microsoft_appkey',
|
||||
],
|
||||
'facebook' => [
|
||||
'label' => 'Facebook',
|
||||
'id_key' => 'facebook_appid',
|
||||
'secret_key' => 'facebook_appkey',
|
||||
],
|
||||
'twitter' => [
|
||||
'label' => 'Twitter',
|
||||
'id_key' => 'twitter_appid',
|
||||
'secret_key' => 'twitter_appkey',
|
||||
],
|
||||
'apple' => [
|
||||
'label' => 'Apple',
|
||||
'id_key' => 'apple_appid',
|
||||
'secret_key' => 'apple_appkey',
|
||||
'extra_keys' => [
|
||||
'team_id' => 'apple_team_id',
|
||||
'key_id' => 'apple_key_id',
|
||||
'private_key' => 'apple_private_key',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/** 彩虹聚合登录支持的平台,与参考主题一致。 */
|
||||
|
||||
@@ -1,436 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Third-party login providers that the bundled SDK does not cover.
|
||||
*
|
||||
* Douyin, Microsoft, Facebook, Twitter and Apple are implemented here directly
|
||||
* against their documented OAuth2 endpoints; Baidu and Alipay come from the
|
||||
* bundled yurunsoft SDK, which already ships working clients for both.
|
||||
*
|
||||
* @package Jio
|
||||
*/
|
||||
|
||||
namespace Jio\Auth;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
final class OAuthProviderClient {
|
||||
|
||||
/** Providers served by this class rather than the bundled SDK. */
|
||||
public const CUSTOM = ['douyin', 'microsoft', 'facebook', 'twitter', 'apple'];
|
||||
|
||||
/** Providers served by yurunsoft/yurun-oauth-login. */
|
||||
public const SDK = ['baidu', 'alipay'];
|
||||
|
||||
/** Whether this class owns the provider (direct endpoints or SDK bridge). */
|
||||
public static function handles(string $provider): bool {
|
||||
return in_array($provider, self::CUSTOM, true) || in_array($provider, self::SDK, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the authorization URL.
|
||||
*
|
||||
* @param array<string,string> $config provider_config() output plus extra secrets.
|
||||
* @param string $redirect_uri Callback URL registered with the provider.
|
||||
* @param string $state Opaque CSRF token.
|
||||
*/
|
||||
public static function auth_url(string $provider, array $config, string $redirect_uri, string $state): string {
|
||||
if (in_array($provider, self::SDK, true)) {
|
||||
return self::sdk_client($provider, $config, $redirect_uri)->getAuthUrl($redirect_uri, $state);
|
||||
}
|
||||
switch ($provider) {
|
||||
case 'douyin':
|
||||
return 'https://open.douyin.com/platform/oauth/connect?' . http_build_query([
|
||||
'client_key' => $config['client_id'],
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'response_type' => 'code',
|
||||
'scope' => 'user_info',
|
||||
'state' => $state,
|
||||
]);
|
||||
case 'microsoft':
|
||||
return 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?' . http_build_query([
|
||||
'client_id' => $config['client_id'],
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'response_type' => 'code',
|
||||
'response_mode' => 'query',
|
||||
'scope' => 'openid profile email User.Read',
|
||||
'state' => $state,
|
||||
]);
|
||||
case 'facebook':
|
||||
return 'https://www.facebook.com/v21.0/dialog/oauth?' . http_build_query([
|
||||
'client_id' => $config['client_id'],
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'response_type' => 'code',
|
||||
'scope' => 'public_profile,email',
|
||||
'state' => $state,
|
||||
]);
|
||||
case 'twitter':
|
||||
// Twitter 的 PKCE code_challenge 需要随 state 一起保存,回调时再校验。
|
||||
$verifier = self::twitter_verifier($state);
|
||||
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
|
||||
return 'https://twitter.com/i/oauth2/authorize?' . http_build_query([
|
||||
'client_id' => $config['client_id'],
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'response_type' => 'code',
|
||||
'scope' => 'users.read tweet.read offline.access',
|
||||
'state' => $state,
|
||||
'code_challenge' => $challenge,
|
||||
'code_challenge_method' => 'S256',
|
||||
]);
|
||||
case 'apple':
|
||||
return 'https://appleid.apple.com/auth/authorize?' . http_build_query([
|
||||
'client_id' => $config['client_id'],
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'response_type' => 'code',
|
||||
// Apple 只支持 form_post 回调,服务端需要读 $_POST。
|
||||
'response_mode' => 'form_post',
|
||||
'scope' => 'name email',
|
||||
'state' => $state,
|
||||
]);
|
||||
}
|
||||
throw new \RuntimeException(__('不支持的第三方登录平台', 'jio'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange the callback code for a normalized identity.
|
||||
*
|
||||
* @param array<string,string> $config provider_config() output plus extra secrets.
|
||||
*/
|
||||
public static function identity(string $provider, array $config, string $code, string $redirect_uri, string $state): array {
|
||||
if (in_array($provider, self::SDK, true)) {
|
||||
return self::sdk_identity($provider, $config, $code, $redirect_uri, $state);
|
||||
}
|
||||
switch ($provider) {
|
||||
case 'douyin':
|
||||
return self::douyin_identity($config, $code);
|
||||
case 'microsoft':
|
||||
return self::microsoft_identity($config, $code, $redirect_uri);
|
||||
case 'facebook':
|
||||
return self::facebook_identity($config, $code, $redirect_uri);
|
||||
case 'twitter':
|
||||
return self::twitter_identity($config, $code, $redirect_uri, $state);
|
||||
case 'apple':
|
||||
return self::apple_identity($config, $code, $redirect_uri);
|
||||
}
|
||||
throw new \RuntimeException(__('不支持的第三方登录平台', 'jio'));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- SDK bridge
|
||||
|
||||
/** @param array<string,string> $config */
|
||||
private static function sdk_client(string $provider, array $config, string $redirect_uri) {
|
||||
switch ($provider) {
|
||||
case 'baidu':
|
||||
$client = new \Yurun\OAuthLogin\Baidu\OAuth2($config['client_id'], $config['client_secret'], $redirect_uri);
|
||||
break;
|
||||
case 'alipay':
|
||||
$client = new \Yurun\OAuthLogin\Alipay\OAuth2($config['client_id'], $config['client_secret'], $redirect_uri);
|
||||
// 支付宝要求应用私钥;client_secret 位存的是支付宝公钥。
|
||||
if (!empty($config['private_key'])) {
|
||||
$client->appPrivateKey = (string) $config['private_key'];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new \RuntimeException(__('不支持的第三方登录平台', 'jio'));
|
||||
}
|
||||
return $client;
|
||||
}
|
||||
|
||||
/** @param array<string,string> $config */
|
||||
private static function sdk_identity(string $provider, array $config, string $code, string $redirect_uri, string $state): array {
|
||||
$client = self::sdk_client($provider, $config, $redirect_uri);
|
||||
$client->getAccessToken($state, $code, $state);
|
||||
$info = (array) $client->getUserInfo();
|
||||
if ($provider === 'baidu') {
|
||||
return [
|
||||
'provider' => 'baidu',
|
||||
'provider_id' => (string) ($info['userid'] ?? $info['uid'] ?? ''),
|
||||
'username' => (string) ($info['uname'] ?? ''),
|
||||
'name' => (string) ($info['uname'] ?? ''),
|
||||
'email' => sanitize_email((string) ($info['email'] ?? '')),
|
||||
'email_verified' => false,
|
||||
'avatar_url' => str_replace('tb.himg.baidu.com', 'himg.bdimg.com', (string) ($info['portrait'] ?? '')),
|
||||
];
|
||||
}
|
||||
// 支付宝:user_id 为唯一标识,昵称在 user_info.nick_name。
|
||||
$user_info = (array) ($info['user_info'] ?? $info);
|
||||
return [
|
||||
'provider' => 'alipay',
|
||||
'provider_id' => (string) ($info['user_id'] ?? $user_info['user_id'] ?? ''),
|
||||
'username' => (string) ($user_info['nick_name'] ?? ''),
|
||||
'name' => (string) ($user_info['nick_name'] ?? ''),
|
||||
'email' => '',
|
||||
'email_verified' => false,
|
||||
'avatar_url' => (string) ($user_info['avatar'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- Douyin
|
||||
|
||||
/** @param array<string,string> $config */
|
||||
private static function douyin_identity(array $config, string $code): array {
|
||||
$token = wp_remote_post('https://open.douyin.com/oauth/access_token/', [
|
||||
'timeout' => 20,
|
||||
'body' => [
|
||||
'client_key' => $config['client_id'],
|
||||
'client_secret' => $config['client_secret'],
|
||||
'code' => $code,
|
||||
'grant_type' => 'authorization_code',
|
||||
],
|
||||
]);
|
||||
$data = self::json_body($token, __('获取抖音授权令牌失败', 'jio'));
|
||||
$payload = (array) ($data['data'] ?? []);
|
||||
if (empty($payload['access_token']) || empty($payload['open_id'])) {
|
||||
throw new \RuntimeException(__('获取抖音授权令牌失败', 'jio'));
|
||||
}
|
||||
|
||||
$profile = wp_remote_post('https://open.douyin.com/oauth/userinfo/', [
|
||||
'timeout' => 20,
|
||||
'body' => [
|
||||
'access_token' => $payload['access_token'],
|
||||
'open_id' => $payload['open_id'],
|
||||
],
|
||||
]);
|
||||
$info = self::json_body($profile, __('获取抖音用户信息失败', 'jio'));
|
||||
$user = (array) ($info['data'] ?? []);
|
||||
|
||||
return [
|
||||
'provider' => 'douyin',
|
||||
'provider_id' => (string) $payload['open_id'],
|
||||
'username' => (string) ($user['nickname'] ?? ''),
|
||||
'name' => (string) ($user['nickname'] ?? ''),
|
||||
'email' => '',
|
||||
'email_verified' => false,
|
||||
'avatar_url' => (string) ($user['avatar_larger'] ?? $user['avatar_300x300'] ?? $user['avatar_168x168'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Microsoft
|
||||
|
||||
/** @param array<string,string> $config */
|
||||
private static function microsoft_identity(array $config, string $code, string $redirect_uri): array {
|
||||
$token = wp_remote_post('https://login.microsoftonline.com/common/oauth2/v2.0/token', [
|
||||
'timeout' => 20,
|
||||
'body' => [
|
||||
'client_id' => $config['client_id'],
|
||||
'client_secret' => $config['client_secret'],
|
||||
'code' => $code,
|
||||
'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'scope' => 'openid profile email User.Read',
|
||||
],
|
||||
]);
|
||||
$data = self::json_body($token, __('获取 Microsoft 授权令牌失败', 'jio'));
|
||||
if (empty($data['access_token'])) {
|
||||
throw new \RuntimeException(__('获取 Microsoft 授权令牌失败', 'jio'));
|
||||
}
|
||||
|
||||
$profile = wp_remote_get('https://graph.microsoft.com/oidc/userinfo', [
|
||||
'timeout' => 20,
|
||||
'headers' => ['Authorization' => 'Bearer ' . $data['access_token']],
|
||||
]);
|
||||
$info = self::json_body($profile, __('获取 Microsoft 用户信息失败', 'jio'));
|
||||
|
||||
$email = sanitize_email((string) ($info['email'] ?? $info['preferred_username'] ?? ''));
|
||||
return [
|
||||
'provider' => 'microsoft',
|
||||
'provider_id' => (string) ($info['sub'] ?? ''),
|
||||
'username' => (string) ($info['email'] ?? $info['preferred_username'] ?? ''),
|
||||
'name' => (string) ($info['name'] ?? ''),
|
||||
'email' => $email,
|
||||
'email_verified' => $email !== '' && !empty($info['email']),
|
||||
'avatar_url' => '',
|
||||
];
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- Facebook
|
||||
|
||||
/** @param array<string,string> $config */
|
||||
private static function facebook_identity(array $config, string $code, string $redirect_uri): array {
|
||||
$token = wp_remote_get('https://graph.facebook.com/v21.0/oauth/access_token?' . http_build_query([
|
||||
'client_id' => $config['client_id'],
|
||||
'client_secret' => $config['client_secret'],
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'code' => $code,
|
||||
]), ['timeout' => 20]);
|
||||
$data = self::json_body($token, __('获取 Facebook 授权令牌失败', 'jio'));
|
||||
if (empty($data['access_token'])) {
|
||||
throw new \RuntimeException(__('获取 Facebook 授权令牌失败', 'jio'));
|
||||
}
|
||||
|
||||
$profile = wp_remote_get('https://graph.facebook.com/v21.0/me?' . http_build_query([
|
||||
'fields' => 'id,name,email,picture.type(large)',
|
||||
'access_token' => $data['access_token'],
|
||||
]), ['timeout' => 20]);
|
||||
$info = self::json_body($profile, __('获取 Facebook 用户信息失败', 'jio'));
|
||||
|
||||
// Facebook 头像地址不带协议,补 https 以适配页面协议。
|
||||
$avatar = (string) ($info['picture']['data']['url'] ?? '');
|
||||
$email = sanitize_email((string) ($info['email'] ?? ''));
|
||||
return [
|
||||
'provider' => 'facebook',
|
||||
'provider_id' => (string) ($info['id'] ?? ''),
|
||||
'username' => $email !== '' ? $email : (string) ($info['name'] ?? ''),
|
||||
'name' => (string) ($info['name'] ?? ''),
|
||||
'email' => $email,
|
||||
'email_verified' => $email !== '',
|
||||
'avatar_url' => $avatar !== '' ? self::https_url($avatar) : '',
|
||||
];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Twitter
|
||||
|
||||
/** @param array<string,string> $config */
|
||||
private static function twitter_identity(array $config, string $code, string $redirect_uri, string $state): array {
|
||||
$verifier = self::twitter_verifier($state);
|
||||
$token = wp_remote_post('https://api.twitter.com/2/oauth2/token', [
|
||||
'timeout' => 20,
|
||||
'headers' => ['Authorization' => 'Basic ' . base64_encode($config['client_id'] . ':' . $config['client_secret'])],
|
||||
'body' => [
|
||||
'code' => $code,
|
||||
'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'code_verifier' => $verifier,
|
||||
],
|
||||
]);
|
||||
$data = self::json_body($token, __('获取 Twitter 授权令牌失败', 'jio'));
|
||||
if (empty($data['access_token'])) {
|
||||
throw new \RuntimeException(__('获取 Twitter 授权令牌失败', 'jio'));
|
||||
}
|
||||
|
||||
$profile = wp_remote_get('https://api.twitter.com/2/users/me?' . http_build_query([
|
||||
'user.fields' => 'id,name,username,profile_image_url',
|
||||
]), [
|
||||
'timeout' => 20,
|
||||
'headers' => ['Authorization' => 'Bearer ' . $data['access_token']],
|
||||
]);
|
||||
$info = (array) (self::json_body($profile, __('获取 Twitter 用户信息失败', 'jio'))['data'] ?? []);
|
||||
|
||||
return [
|
||||
'provider' => 'twitter',
|
||||
'provider_id' => (string) ($info['id'] ?? ''),
|
||||
'username' => (string) ($info['username'] ?? ''),
|
||||
'name' => (string) ($info['name'] ?? ''),
|
||||
'email' => '',
|
||||
'email_verified' => false,
|
||||
'avatar_url' => (string) ($info['profile_image_url'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/** Derive the PKCE verifier from the state so it never needs extra storage. */
|
||||
private static function twitter_verifier(string $state): string {
|
||||
return rtrim(strtr(base64_encode(hash('sha256', 'jio-twitter|' . $state, true)), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------- Apple
|
||||
|
||||
/** @param array<string,string> $config */
|
||||
private static function apple_identity(array $config, string $code, string $redirect_uri): array {
|
||||
$secret = self::apple_client_secret($config);
|
||||
$token = wp_remote_post('https://appleid.apple.com/auth/token', [
|
||||
'timeout' => 20,
|
||||
'body' => [
|
||||
'client_id' => $config['client_id'],
|
||||
'client_secret' => $secret,
|
||||
'code' => $code,
|
||||
'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => $redirect_uri,
|
||||
],
|
||||
]);
|
||||
$data = self::json_body($token, __('获取 Apple 授权令牌失败', 'jio'));
|
||||
if (empty($data['id_token'])) {
|
||||
throw new \RuntimeException(__('获取 Apple 授权令牌失败', 'jio'));
|
||||
}
|
||||
|
||||
$claims = self::jwt_claims((string) $data['id_token']);
|
||||
if (!is_array($claims) || empty($claims['sub'])) {
|
||||
throw new \RuntimeException(__('解析 Apple 用户信息失败', 'jio'));
|
||||
}
|
||||
|
||||
$email = sanitize_email((string) ($claims['email'] ?? ''));
|
||||
return [
|
||||
'provider' => 'apple',
|
||||
'provider_id' => (string) $claims['sub'],
|
||||
'username' => $email !== '' ? $email : (string) $claims['sub'],
|
||||
'name' => '',
|
||||
'email' => $email,
|
||||
'email_verified' => !empty($claims['email_verified']) || !empty($claims['is_private_email']) === false && $email !== '',
|
||||
'avatar_url' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the ES256 client secret Apple requires (a short-lived JWT).
|
||||
*
|
||||
* @param array<string,string> $config
|
||||
*/
|
||||
private static function apple_client_secret(array $config): string {
|
||||
$team = (string) ($config['team_id'] ?? '');
|
||||
$key = (string) ($config['key_id'] ?? '');
|
||||
$pem = (string) ($config['private_key'] ?? '');
|
||||
if ($pem === '') {
|
||||
// 未配置密钥时退化为普通 client_secret,仍然遵循同一签名流程。
|
||||
return (string) $config['client_secret'];
|
||||
}
|
||||
$now = time();
|
||||
$header = self::b64url(wp_json_encode(['alg' => 'ES256', 'kid' => $key]));
|
||||
$payload = self::b64url(wp_json_encode([
|
||||
'iss' => $team,
|
||||
'iat' => $now,
|
||||
'exp' => $now + 3600,
|
||||
'aud' => 'https://appleid.apple.com',
|
||||
'sub' => (string) $config['client_id'],
|
||||
]));
|
||||
$signing = $header . '.' . $payload;
|
||||
$key_id = openssl_pkey_get_private($pem);
|
||||
if ($key_id === false) {
|
||||
throw new \RuntimeException(__('Apple 私钥格式无效', 'jio'));
|
||||
}
|
||||
$signature = '';
|
||||
openssl_sign($signing, $signature, $key_id, OPENSSL_ALGO_SHA256);
|
||||
return $signing . '.' . self::b64url($signature);
|
||||
}
|
||||
|
||||
/** Decode the payload of a JWT without verifying it (the token came over TLS). */
|
||||
private static function jwt_claims(string $token): ?array {
|
||||
$parts = explode('.', $token);
|
||||
if (count($parts) < 2) {
|
||||
return null;
|
||||
}
|
||||
$payload = json_decode((string) base64_decode(strtr($parts[1], '-_', '+/'), true), true);
|
||||
return is_array($payload) ? $payload : null;
|
||||
}
|
||||
|
||||
private static function b64url(string $value): string {
|
||||
return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ helpers
|
||||
|
||||
/**
|
||||
* Decode a JSON response, raising a translated message on transport errors.
|
||||
*
|
||||
* @param array|\WP_Error $response
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function json_body($response, string $message): array {
|
||||
if (is_wp_error($response)) {
|
||||
throw new \RuntimeException($response->get_error_message());
|
||||
}
|
||||
if ((int) wp_remote_retrieve_response_code($response) !== 200) {
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
$data = json_decode((string) wp_remote_retrieve_body($response), true);
|
||||
if (!is_array($data)) {
|
||||
throw new \RuntimeException($message);
|
||||
}
|
||||
if (!empty($data['error'])) {
|
||||
$detail = $data['error_description'] ?? $data['error']['message'] ?? $data['error'];
|
||||
throw new \RuntimeException(is_string($detail) ? $detail : $message);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private static function https_url(string $url): string {
|
||||
return (string) preg_replace('#^http://#i', 'https://', $url);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user