Files
wordpress-theme-aksrt/src/Auth/OAuthProviderCatalog.php
T
kate522 6db34646ed feat(oauth): add the seven missing providers and open the full aggregate list
The theme shipped six direct providers while the reference carries fourteen,
and its aggregate-login channel offered three of sixteen platforms.

OAuthProviderClient now owns the providers the bundled SDK does not cover.
Douyin, Microsoft, Facebook, Twitter and Apple talk to their documented OAuth2
endpoints directly; Baidu and Alipay reuse yurunsoft, which already ships both
(Alipay needs its application private key, so provider_config learned an
extra_keys map to surface it). The broker dispatches through handles(), so the
existing start/callback routes stay provider-agnostic.

Three provider quirks needed handling rather than a plain copy:

Apple returns the callback as response_mode=form_post, and handle_callback
only read $_GET, so it now reads $_POST when the request is a POST — otherwise
every Apple login would fail with code_missing. Its client secret is built as
an ES256 JWT from Team ID / Key ID / .p8 rather than a static string.

Twitter requires PKCE. The verifier is derived from the state token, so it
travels with the existing transaction row and needs no extra storage.

Facebook hands back protocol-less avatar URLs, which are upgraded to https the
same way the OAuth avatar resolver does.

Settings gained matching toggles and credential groups on both sides — the PHP
schema and the Vue schema — and the aggregate platform list went from three to
sixteen, with CLoginClient's hard-coded allowlist replaced by the catalog
constant. Brand icons were extended so each new provider resolves to its
sprite; facebook and twitter moved to their colour sprites, and the
`twitter -> x` alias was dropped since it bypassed the new mapping.

The oauth-security contract previously asserted that Baidu and Alipay must not
appear on the OAuth surface. That was the old intent, so the assertion is
replaced with a positive one, keeping the separate check that the Alipay
payment gateway still exists.

Verification: a new harness stubs the HTTP layer and drives all five custom
clients — authorization URLs carry redirect/state (Twitter also a PKCE
challenge), identities normalize to the shared shape, Facebook's avatar is
upgraded to https, Apple's private-relay email survives, and an error payload
throws instead of yielding an empty identity. 871 widget/navigation tests pass
and the PSR-4 audit reports 0 errors.

Not covered: no live handshake happened — every provider needs real AppID and
secret pairs, and Apple/Douyin require developer-account review before the
full round trip can be exercised.
2026-11-04 18:00:29 +08:00

256 lines
7.6 KiB
PHP

<?php
/**
* @package Jio
*/
namespace Jio\Auth;
use Jio\Settings\Settings;
defined('ABSPATH') || exit;
final class OAuthProviderCatalog {
private const DIRECT = [
'github' => [
'label' => 'GitHub',
'id_key' => 'github_appid',
'secret_key' => 'github_appkey',
],
'gitee' => [
'label' => 'Gitee',
'id_key' => 'gitee_appid',
'secret_key' => 'gitee_appkey',
],
'qq' => [
'label' => 'QQ',
'id_key' => 'qq_appid',
'secret_key' => 'qq_appkey',
],
'google' => [
'label' => 'Google',
'id_key' => 'google_client_id',
'secret_key' => 'google_client_secret',
],
'weibo' => [
'label' => 'Weibo',
'id_key' => 'weibo_appid',
'secret_key' => 'weibo_appkey',
],
'weixin' => [
'label' => 'WeChat',
'id_key' => 'weixin_appid',
'secret_key' => 'weixin_appsecret',
],
'douyin' => [
'label' => '抖音',
'id_key' => 'douyin_appid',
'secret_key' => 'douyin_appkey',
],
'baidu' => [
'label' => '百度',
'id_key' => 'baidu_appid',
'secret_key' => 'baidu_appkey',
],
'alipay' => [
'label' => '支付宝',
'id_key' => 'alipay_appid',
'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',
],
],
];
/** 彩虹聚合登录支持的平台,与参考主题一致。 */
public const CLOGIN_TYPES = [
'qq', 'weixin', 'alipay', 'weibo', 'douyin', 'baidu', 'github', 'gitee',
'dingtalk', 'huawei', 'xiaomi', 'google', 'microsoft', 'facebook', 'twitter', 'apple',
];
public static function direct_keys(): array {
return array_keys(self::DIRECT);
}
public static function all_binding_keys(): array {
$keys = array_merge(self::direct_keys(), ['weixingzh']);
foreach (self::CLOGIN_TYPES as $type) {
$keys[] = 'clogin_' . $type;
}
return $keys;
}
public static function is_direct(string $provider): bool {
return isset(self::DIRECT[sanitize_key($provider)]);
}
public static function is_binding_provider(string $provider): bool {
return in_array(sanitize_key($provider), self::all_binding_keys(), true);
}
public static function label(string $provider): string {
$provider = sanitize_key($provider);
if (isset(self::DIRECT[$provider])) {
return self::DIRECT[$provider]['label'];
}
if ($provider === 'weixingzh') {
return __('微信公众号', 'jio');
}
if (str_starts_with($provider, 'clogin_')) {
$type = substr($provider, 7);
if (in_array($type, self::CLOGIN_TYPES, true)) {
return self::label($type) . ' · ' . __('聚合', 'jio');
}
}
return ucfirst($provider);
}
public static function icon_key(string $provider): string {
$provider = sanitize_key($provider);
return str_starts_with($provider, 'clogin_') ? substr($provider, 7) : $provider;
}
public static function master_enabled(): bool {
return !Settings::close_sign() && (bool) Settings::get_value('oauth_s', false);
}
public static function provider_config(string $provider): ?array {
$provider = sanitize_key($provider);
if (!isset(self::DIRECT[$provider])) {
return null;
}
$settings = Settings::get_all();
$definition = self::DIRECT[$provider];
$client_id = trim((string) ($settings[$definition['id_key']] ?? ''));
$client_secret = trim((string) ($settings[$definition['secret_key']] ?? ''));
$extra = [];
foreach ((array) ($definition['extra_keys'] ?? []) as $field => $setting_key) {
$extra[$field] = trim((string) ($settings[$setting_key] ?? ''));
}
return [
'provider' => $provider,
'enabled' => self::master_enabled() && !empty($settings['oauth_' . $provider . '_s']),
'client_id' => $client_id,
'client_secret' => $client_secret,
'configured' => $client_id !== '' && $client_secret !== '',
] + $extra;
}
public static function enabled_direct_keys(): array {
if (!self::master_enabled()) {
return [];
}
$result = [];
foreach (self::direct_keys() as $provider) {
$config = self::provider_config($provider);
if ($config && $config['enabled'] && $config['configured']) {
$result[] = $provider;
}
}
return $result;
}
public static function proxy_mode(): string {
$mode = sanitize_key((string) Settings::get_value('oauth_proxy_mode', 'close'));
return in_array($mode, ['close', 'server', 'client'], true) ? $mode : 'close';
}
public static function proxy_secret(): string {
return trim((string) Settings::get_value('oauth_proxy_secret', ''));
}
public static function proxy_server_url(): string {
return esc_url_raw(trim((string) Settings::get_value('oauth_proxy_server_url', '')));
}
public static function proxy_server_providers(): array {
$providers = (array) Settings::get_value('oauth_proxy_server_providers', []);
$result = [];
foreach (array_values(array_intersect(array_map('sanitize_key', $providers), self::direct_keys())) as $provider) {
$config = self::provider_config($provider);
if ($config && $config['configured']) {
$result[] = $provider;
}
}
return $result;
}
public static function proxy_client_providers(): array {
$providers = (array) Settings::get_value('oauth_proxy_client_providers', []);
return array_values(array_intersect(array_map('sanitize_key', $providers), self::direct_keys()));
}
public static function bind_type(): string {
$type = sanitize_key((string) Settings::get_value('oauth_bind_type', 'auto'));
return in_array($type, ['auto', 'manual'], true) ? $type : 'auto';
}
public static function clogin_settings(): array {
$settings = Settings::get_all();
$raw = (array) ($settings['clogin_oauth_s'] ?? self::CLOGIN_TYPES);
$types = array_values(array_intersect(array_map('sanitize_key', $raw), self::CLOGIN_TYPES));
return [
'enabled' => self::master_enabled() && !empty($settings['clogin_s']),
'url' => esc_url_raw(trim((string) ($settings['clogin_url'] ?? ''))),
'id' => trim((string) ($settings['clogin_id'] ?? '')),
'key' => trim((string) ($settings['clogin_key'] ?? '')),
'types' => $types,
];
}
public static function enabled_login_keys(): array {
if (!self::master_enabled()) {
return [];
}
if (self::proxy_mode() === 'client') {
if (self::proxy_server_url() === '' || self::proxy_secret() === '') {
return [];
}
return self::proxy_client_providers();
}
$result = self::enabled_direct_keys();
$settings = Settings::get_all();
if (!empty($settings['oauth_weixingzh_s'])) {
$gzh_type = sanitize_key((string) ($settings['weixingzh_gzh_type'] ?? 'service'));
$has_base = trim((string) ($settings['weixingzh_token'] ?? '')) !== ''
&& trim((string) ($settings['weixingzh_appid'] ?? '')) !== '';
$has_mode = $gzh_type === 'subscribe'
? trim((string) ($settings['weixingzh_code_qrcode'] ?? '')) !== ''
: trim((string) ($settings['weixingzh_appsecret'] ?? '')) !== '';
if ($has_base && $has_mode) {
$result[] = 'weixingzh';
}
}
$clogin = self::clogin_settings();
if ($clogin['enabled'] && $clogin['url'] !== '' && $clogin['id'] !== '' && $clogin['key'] !== '') {
foreach ($clogin['types'] as $type) {
$result[] = 'clogin_' . $type;
}
}
return array_values(array_unique($result));
}
}