From b1c3d86e4431736a690f08cf9743de698e259ae3 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 4 Nov 2026 18:33:37 +0800 Subject: [PATCH] refactor(oauth): drop the hand-written providers and keep only what the SDK ships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../admin/src/schema/sections/users/oauth.js | 85 +--- src/Admin/Schema/SchemaUsers.php | 38 -- src/Auth/OAuthProviderBroker.php | 40 +- src/Auth/OAuthProviderCatalog.php | 30 -- src/Auth/OAuthProviderClient.php | 436 ------------------ .../oauth-provider-client-harness.php | 133 ------ .../oauth-provider-client.test.cjs | 77 ---- .../oauth-security-contract.test.cjs | 25 +- 8 files changed, 55 insertions(+), 809 deletions(-) delete mode 100644 src/Auth/OAuthProviderClient.php delete mode 100644 tests/widget-system/oauth-provider-client-harness.php delete mode 100644 tests/widget-system/oauth-provider-client.test.cjs diff --git a/assets/admin/src/schema/sections/users/oauth.js b/assets/admin/src/schema/sections/users/oauth.js index ce579ea2..4f048252 100644 --- a/assets/admin/src/schema/sections/users/oauth.js +++ b/assets/admin/src/schema/sections/users/oauth.js @@ -20,13 +20,8 @@ export const oauthSchema = { oauth_weibo_s: false, oauth_weixin_s: false, oauth_weixingzh_s: false, - oauth_douyin_s: false, oauth_baidu_s: false, oauth_alipay_s: false, - oauth_microsoft_s: false, - oauth_facebook_s: false, - oauth_twitter_s: false, - oauth_apple_s: false, clogin_s: false, github_appid: '', github_appkey: '', @@ -40,24 +35,11 @@ export const oauthSchema = { weibo_appkey: '', weixin_appid: '', weixin_appsecret: '', - douyin_appid: '', - douyin_appkey: '', baidu_appid: '', baidu_appkey: '', alipay_appid: '', alipay_appkey: '', alipay_private_key: '', - microsoft_appid: '', - microsoft_appkey: '', - facebook_appid: '', - facebook_appkey: '', - twitter_appid: '', - twitter_appkey: '', - apple_appid: '', - apple_appkey: '', - apple_team_id: '', - apple_key_id: '', - apple_private_key: '', weixingzh_gzh_type: 'service', weixingzh_token: '', weixingzh_appid: '', @@ -82,8 +64,8 @@ export const oauthSchema = { { key: 'oauth_proxy_mode', label: '代理登录模式', type: 'radio', default: 'close', options: [{ label: '关闭', value: 'close' }, { label: '服务端', value: 'server' }, { label: '客户端', value: 'client' }] }, { key: 'oauth_proxy_secret', label: '代理 HMAC 密钥', type: 'input', inputType: 'password', secret: true, default: '', dependsOn: { key: 'oauth_proxy_mode', not: 'close' } }, { key: 'oauth_proxy_server_url', label: '服务端地址', type: 'input', default: '', dependsOn: { key: 'oauth_proxy_mode', value: 'client' } }, - { key: 'oauth_proxy_server_providers', label: '服务端允许平台', type: 'checkbox', default: ['github', 'gitee', 'qq', 'google'], options: [{ label: 'GitHub', value: 'github' }, { label: 'Gitee', value: 'gitee' }, { label: 'QQ', value: 'qq' }, { 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' }, { label: 'Gitee', value: 'gitee' }, { label: 'QQ', value: 'qq' }, { 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_proxy_server_providers', label: '服务端允许平台', type: 'checkbox', default: ['github', 'gitee', 'qq', 'google'], options: [{ label: 'GitHub', value: 'github' }, { label: 'Gitee', value: 'gitee' }, { label: 'QQ', value: 'qq' }, { label: 'Google', value: 'google' }, { label: '微博', value: 'weibo' }, { label: '微信', value: 'weixin' }, { label: '百度', value: 'baidu' }, { label: '支付宝', value: 'alipay' } ], 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' }, { label: 'Gitee', value: 'gitee' }, { label: 'QQ', value: 'qq' }, { label: 'Google', value: 'google' }, { label: '微博', value: 'weibo' }, { label: '微信', value: 'weixin' }, { label: '百度', value: 'baidu' }, { label: '支付宝', value: 'alipay' } ], dependsOn: { key: 'oauth_proxy_mode', value: 'client' } }, { key: 'oauth_bind_type', label: '绑定方式', type: 'radio', default: 'auto', options: [{ label: '自动绑定', value: 'auto' }, { label: '手动绑定', value: 'manual' }] }, { key: 'oauth_button_lg', label: '大按钮样式', type: 'switch', default: false }, { key: 'weixingzh_priority', label: 'PC端优先微信扫码', type: 'switch', default: false, help: '开启后 PC 端登录框优先显示微信公众号扫码' }, @@ -101,13 +83,8 @@ export const oauthSchema = { { 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: '通过第三方聚合登录平台统一接入多个平台' }, ], }, @@ -177,17 +154,6 @@ export const oauthSchema = { { key: 'weixin_appsecret', label: 'AppSecret', type: 'input', inputType: 'password', secret: true, default: '' }, ], }, - { - key: 'douyin', - title: '抖音配置', - icon: 'fa fas fa-music', - 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: '百度配置', @@ -211,53 +177,6 @@ export const oauthSchema = { { key: 'alipay_private_key', label: '应用私钥', type: 'textarea', secret: true, default: '', help: 'RSA2 签名使用的应用私钥(PKCS8 格式)' }, ], }, - { - key: 'microsoft', - title: 'Microsoft 配置', - icon: 'fa fab fa-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 配置', - icon: 'fa fab fa-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 配置', - icon: 'fa fab fa-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 配置', - icon: 'fa fab fa-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: '微信公众号配置', diff --git a/src/Admin/Schema/SchemaUsers.php b/src/Admin/Schema/SchemaUsers.php index e46ee55f..c1ef762c 100644 --- a/src/Admin/Schema/SchemaUsers.php +++ b/src/Admin/Schema/SchemaUsers.php @@ -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'], diff --git a/src/Auth/OAuthProviderBroker.php b/src/Auth/OAuthProviderBroker.php index 13e288a0..e70113cb 100644 --- a/src/Auth/OAuthProviderBroker.php +++ b/src/Auth/OAuthProviderBroker.php @@ -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')); } diff --git a/src/Auth/OAuthProviderCatalog.php b/src/Auth/OAuthProviderCatalog.php index 3f9400b2..de42065a 100644 --- a/src/Auth/OAuthProviderCatalog.php +++ b/src/Auth/OAuthProviderCatalog.php @@ -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', - ], - ], ]; /** 彩虹聚合登录支持的平台,与参考主题一致。 */ diff --git a/src/Auth/OAuthProviderClient.php b/src/Auth/OAuthProviderClient.php deleted file mode 100644 index daa3a978..00000000 --- a/src/Auth/OAuthProviderClient.php +++ /dev/null @@ -1,436 +0,0 @@ - $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 $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 $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 $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 $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 $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 $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 $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 $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 $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 - */ - 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); - } -} diff --git a/tests/widget-system/oauth-provider-client-harness.php b/tests/widget-system/oauth-provider-client-harness.php deleted file mode 100644 index 6236cc0d..00000000 --- a/tests/widget-system/oauth-provider-client-harness.php +++ /dev/null @@ -1,133 +0,0 @@ -message; } -} - -/** Canned responses keyed by URL. */ -final class HttpStub { - public static array $responses = []; - - public static function respond(string $url, int $code, array $body): void { - self::$responses[$url] = ['code' => $code, 'body' => wp_json_encode($body)]; - } -} - -function wp_remote_get(string $url, array $args = []) { - $GLOBALS['http_calls'][] = ['GET', $url, $args]; - return HttpStub::$responses[$url] ?? new WP_Error('no_stub_for_' . $url); -} - -function wp_remote_post(string $url, array $args = []) { - $GLOBALS['http_calls'][] = ['POST', $url, $args]; - return HttpStub::$responses[$url] ?? new WP_Error('no_stub_for_' . $url); -} - -function wp_remote_retrieve_response_code($response): int { return (int) ($response['code'] ?? 0); } -function wp_remote_retrieve_body($response): string { return (string) ($response['body'] ?? ''); } -function base64url_decode(string $value): string { return (string) base64_decode(strtr($value, '-_', '+/'), true); } - -require dirname(__DIR__, 2) . '/src/Auth/OAuthProviderClient.php'; - -$config = [ - 'client_id' => 'cid', - 'client_secret' => 'secret', - 'private_key' => '', - 'team_id' => 'TEAM', - 'key_id' => 'KEY', -]; - -$redirect = 'https://example.test/oauth/callback/'; -$state = 'state-abc123'; -$code = 'authcode'; - -$out = []; - -/** Auth URL assertions. */ -foreach (['douyin', 'microsoft', 'facebook', 'twitter', 'apple'] as $provider) { - $url = \Jio\Auth\OAuthProviderClient::auth_url($provider, $config, $redirect, $state); - $query = []; - parse_str((string) parse_url($url, PHP_URL_QUERY), $query); - $out['auth'][$provider] = [ - 'url' => $url, - 'has_redirect' => ($query['redirect_uri'] ?? '') === $redirect, - 'has_state' => ($query['state'] ?? '') === $state, - 'challenge' => $query['code_challenge'] ?? '', - ]; -} - -/** Identity normalisation, one canned response set per provider. */ -HttpStub::respond('https://open.douyin.com/oauth/access_token/', 200, [ - 'data' => ['access_token' => 'dytoken', 'open_id' => 'dy-open-id'], -]); -HttpStub::respond('https://open.douyin.com/oauth/userinfo/', 200, [ - 'data' => ['nickname' => '抖音用户', 'avatar_larger' => 'https://p3.douyinpic.com/a.jpg'], -]); -$out['identity']['douyin'] = \Jio\Auth\OAuthProviderClient::identity('douyin', $config, $code, $redirect, $state); - -HttpStub::respond('https://login.microsoftonline.com/common/oauth2/v2.0/token', 200, [ - 'access_token' => 'mstoken', -]); -HttpStub::respond('https://graph.microsoft.com/oidc/userinfo', 200, [ - 'sub' => 'ms-sub', 'email' => 'ms@example.test', 'name' => 'MS User', -]); -$out['identity']['microsoft'] = \Jio\Auth\OAuthProviderClient::identity('microsoft', $config, $code, $redirect, $state); - -HttpStub::respond('https://graph.facebook.com/v21.0/oauth/access_token?' . http_build_query([ - 'client_id' => 'cid', - 'client_secret' => 'secret', - 'redirect_uri' => $redirect, - 'code' => $code, -]), 200, ['access_token' => 'fbtoken']); -HttpStub::respond('https://graph.facebook.com/v21.0/me?' . http_build_query([ - 'fields' => 'id,name,email,picture.type(large)', - 'access_token' => 'fbtoken', -]), 200, [ - 'id' => 'fb-id', 'name' => 'FB User', 'email' => 'fb@example.test', - 'picture' => ['data' => ['url' => 'http://graph.facebook.com/pic.jpg']], -]); -$out['identity']['facebook'] = \Jio\Auth\OAuthProviderClient::identity('facebook', $config, $code, $redirect, $state); - -HttpStub::respond('https://api.twitter.com/2/oauth2/token', 200, ['access_token' => 'twtoken']); -HttpStub::respond('https://api.twitter.com/2/users/me?' . http_build_query([ - 'user.fields' => 'id,name,username,profile_image_url', -]), 200, [ - 'data' => [ - 'id' => 'tw-id', 'name' => 'TW User', 'username' => 'twuser', - 'profile_image_url' => 'https://pbs.twimg.com/p.jpg', - ], -]); -$out['identity']['twitter'] = \Jio\Auth\OAuthProviderClient::identity('twitter', $config, $code, $redirect, $state); - -/** Apple: build a fake id_token payload (no signature verification needed here). */ -$apple_payload = rtrim(strtr(base64_encode((string) wp_json_encode([ - 'sub' => 'apple-sub', 'email' => 'apple@privaterelay.appleid.com', 'email_verified' => 'true', -])), '+/', '-_'), '='); -HttpStub::respond('https://appleid.apple.com/auth/token', 200, [ - 'id_token' => 'eyJhbGciOiJub25lIn0.' . $apple_payload . '.', -]); -$out['identity']['apple'] = \Jio\Auth\OAuthProviderClient::identity('apple', $config, $code, $redirect, $state); - -/** Error path: a provider returning an error payload must throw, not leak it. */ -HttpStub::respond('https://open.douyin.com/oauth/access_token/', 200, ['error' => 'invalid_code']); -try { - \Jio\Auth\OAuthProviderClient::identity('douyin', $config, 'bad', $redirect, $state); - $out['error_path'] = 'NO_THROW'; -} catch (\RuntimeException $e) { - $out['error_path'] = 'threw: ' . $e->getMessage(); -} - -echo json_encode($out, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); diff --git a/tests/widget-system/oauth-provider-client.test.cjs b/tests/widget-system/oauth-provider-client.test.cjs deleted file mode 100644 index 53430cfe..00000000 --- a/tests/widget-system/oauth-provider-client.test.cjs +++ /dev/null @@ -1,77 +0,0 @@ -/** - * 新增 OAuth provider 客户端契约。 - * - * 覆盖参考主题有、而打包 SDK 未提供的五个平台:抖音、Microsoft、 - * Facebook、Twitter、Apple。断言授权 URL 的关键参数、身份归一化结果, - * 以及错误响应必须抛出而不是静默返回空身份。 - */ -'use strict'; - -const assert = require('node:assert/strict'); -const { spawnSync } = require('node:child_process'); -const path = require('node:path'); -const test = require('node:test'); - -const root = path.resolve(__dirname, '..', '..'); - -function runHarness() { - const result = spawnSync( - process.env.PHP_BINARY || 'php', - [path.join(__dirname, 'oauth-provider-client-harness.php')], - { cwd: root, encoding: 'utf8' }, - ); - assert.equal(result.status, 0, result.stderr || result.stdout); - return JSON.parse(result.stdout); -} - -test('新 provider 的授权 URL 携带 redirect/state(Twitter 额外带 PKCE)', () => { - const { auth } = runHarness(); - - for (const provider of ['douyin', 'microsoft', 'facebook', 'twitter', 'apple']) { - assert.equal(auth[provider].has_redirect, true, provider + ' redirect_uri'); - assert.equal(auth[provider].has_state, true, provider + ' state'); - } - - // 只有 Twitter 走 PKCE,其余平台不带 code_challenge。 - assert.match(auth.twitter.challenge, /^[A-Za-z0-9_-]{20,}$/); - for (const provider of ['douyin', 'microsoft', 'facebook', 'apple']) { - assert.equal(auth[provider].challenge, '', provider + ' should not send PKCE'); - } - - // Apple 必须使用 form_post,否则服务端收不到 code。 - assert.match(auth.apple.url, /appleid\.apple\.com\/auth\/authorize/); -}); - -test('各 provider 的身份归一化产出统一字段', () => { - const { identity } = runHarness(); - const required = ['provider', 'provider_id', 'username', 'name', 'email', 'email_verified', 'avatar_url']; - - for (const [provider, row] of Object.entries(identity)) { - for (const key of required) { - assert.ok(key in row, `${provider} missing ${key}`); - } - assert.ok(row.provider_id !== '', `${provider} provider_id must not be empty`); - assert.equal(row.provider, provider); - } - - assert.equal(identity.douyin.provider_id, 'dy-open-id'); - assert.equal(identity.douyin.name, '抖音用户'); - assert.match(identity.douyin.avatar_url, /^https:/); - - assert.equal(identity.microsoft.email, 'ms@example.test'); - assert.equal(identity.microsoft.email_verified, true); - - // Facebook 头像原始地址是 http://,必须升级到 https。 - assert.match(identity.facebook.avatar_url, /^https:\/\//); - - assert.equal(identity.twitter.username, 'twuser'); - assert.equal(identity.twitter.email, '', 'Twitter 不返回邮箱'); - - // Apple 的邮箱是私密转发地址,仍可用于自动绑定。 - assert.match(identity.apple.email, /@privaterelay\.appleid\.com$/); -}); - -test('provider 返回错误时抛出异常而不是产出空身份', () => { - const { error_path: errorPath } = runHarness(); - assert.match(errorPath, /^threw: /); -}); diff --git a/tests/widget-system/oauth-security-contract.test.cjs b/tests/widget-system/oauth-security-contract.test.cjs index d4bf8994..56c5b4ee 100644 --- a/tests/widget-system/oauth-security-contract.test.cjs +++ b/tests/widget-system/oauth-security-contract.test.cjs @@ -33,22 +33,35 @@ test('OAuth PHP behavior harness passes', () => { const output = execFileSync('php', [path.join(root, 'tests/widget-system/oauth-security-harness.php')], { encoding: 'utf8' }) assert.match(output, /OAuth security harness passed/) }) -test('Baidu and Alipay are wired into the OAuth surface', () => { +test('Baidu and Alipay are wired through the bundled SDK', () => { const catalog = read('src/Auth/OAuthProviderCatalog.php') - const client = read('src/Auth/OAuthProviderClient.php') + const broker = read('src/Auth/OAuthProviderBroker.php') const schema = read('src/Admin/Schema/SchemaUsers.php') for (const provider of ['baidu', 'alipay']) { assert.match(catalog, new RegExp(`'${provider}'\\s*=>`), provider) assert.match(schema, new RegExp(`oauth_${provider}_s`), provider) + // 两个平台都走 yurun SDK,而不是自建端点。 + assert.match(broker, new RegExp(`Yurun\\\\OAuthLogin\\\\${provider[0].toUpperCase()}${provider.slice(1)}\\\\OAuth2`), provider) } - // 复用打包的 yurun SDK,而不是自己重写签名。 - assert.match(client, /Yurun\\OAuthLogin\\Baidu\\OAuth2/) - assert.match(client, /Yurun\\OAuthLogin\\Alipay\\OAuth2/) // 支付宝的支付网关仍在,两者互不影响。 assert.match(read('src/Payment/Gateways/AlipayGateway.php'), /class AlipayGateway/) }) +test('未提供自建客户端的平台不进入 OAuth 表面', () => { + const catalog = read('src/Auth/OAuthProviderCatalog.php') + const schema = read('src/Admin/Schema/SchemaUsers.php') + const jsSchema = read('assets/admin/src/schema/sections/users/oauth.js') + + // 抖音 / Microsoft / Facebook / Twitter / Apple 只存在于聚合渠道,不再是直连平台。 + for (const provider of ['douyin', 'microsoft', 'facebook', 'twitter', 'apple']) { + assert.doesNotMatch(catalog, new RegExp(`'${provider}'\\s*=>\\s*\\[`), provider) + assert.doesNotMatch(schema, new RegExp(`oauth_${provider}_s`), provider) + assert.doesNotMatch(jsSchema, new RegExp(`oauth_${provider}_s`), provider) + } + assert.equal(fs.existsSync(path.join(root, 'src/Auth/OAuthProviderClient.php')), false) +}) + test('Alipay payment remains available', () => { const functions = read('functions.php') const paymentSchema = read('src/Admin/Schema/SchemaPayment.php') @@ -72,7 +85,7 @@ test('OAuth uses one-time transactions and no PHP sessions', () => { test('provider catalog is the canonical provider list', () => { const catalog = read('src/Auth/OAuthProviderCatalog.php') - const direct = ['github', 'gitee', 'qq', 'google', 'weibo', 'weixin', 'douyin', 'baidu', 'alipay', 'microsoft', 'facebook', 'twitter', 'apple'] + const direct = ['github', 'gitee', 'qq', 'google', 'weibo', 'weixin', 'baidu', 'alipay'] for (const provider of direct) { assert.match(catalog, new RegExp(`'${provider}'\\s*=>`), provider) }