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.
78 lines
2.9 KiB
JavaScript
78 lines
2.9 KiB
JavaScript
/**
|
||
* 新增 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: /);
|
||
});
|