feat(auth): 统一身份 - 增加可选IAM快捷登录并保留原业务权限
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
use app\adminapi\service\iam\IamOidcClient;
|
||||
use Firebase\JWT\JWT;
|
||||
|
||||
function iamExpect(bool $ok, string $label): void
|
||||
{
|
||||
if (!$ok) { throw new RuntimeException('FAIL: ' . $label); }
|
||||
}
|
||||
function iamReject(callable $call, string $label): void
|
||||
{
|
||||
try { $call(); } catch (Throwable $error) { return; }
|
||||
throw new RuntimeException('FAIL: accepted ' . $label);
|
||||
}
|
||||
function iamB64(string $value): string { return rtrim(strtr(base64_encode($value), '+/', '-_'), '='); }
|
||||
|
||||
$key = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
|
||||
$other = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
|
||||
$details = openssl_pkey_get_details($key);
|
||||
$jwks = ['keys' => [['kty' => 'RSA', 'kid' => 'test', 'alg' => 'RS256', 'use' => 'sig',
|
||||
'n' => iamB64($details['rsa']['n']), 'e' => iamB64($details['rsa']['e'])]]];
|
||||
$config = ['issuer' => 'https://iam.example.test', 'client_id' => 'zyt', 'client_secret' => 'secret',
|
||||
'redirect_uri' => 'https://zyt.example.test/admin/iam/callback', 'api_url' => 'https://iam.example.test',
|
||||
'application_id' => 'zyt app', 'application_token' => 'application-secret'];
|
||||
$nonce = str_repeat('n', 32);
|
||||
$verifier = str_repeat('v', 64);
|
||||
$metadata = ['issuer' => $config['issuer'], 'authorization_endpoint' => $config['issuer'] . '/oauth/authorize',
|
||||
'token_endpoint' => $config['issuer'] . '/oauth/token', 'jwks_uri' => $config['issuer'] . '/oauth/jwks'];
|
||||
$claims = ['iss' => $config['issuer'], 'aud' => 'zyt', 'sub' => 'employee-7', 'nonce' => $nonce,
|
||||
'iat' => time() - 1, 'exp' => time() + 300, 'nbf' => time() - 1];
|
||||
$token = JWT::encode($claims, $key, 'RS256', 'test');
|
||||
$calls = [];
|
||||
$transport = function (string $method, string $url, array $headers, string $body) use (&$metadata, &$token, &$jwks, &$calls, $config, $verifier): array {
|
||||
$calls[] = [$method, $url, $headers, $body];
|
||||
if (str_ends_with($url, '/.well-known/openid-configuration')) { $data = $metadata; }
|
||||
elseif (str_ends_with($url, '/oauth/token')) {
|
||||
parse_str($body, $params);
|
||||
iamExpect($method === 'POST' && $params['code_verifier'] === $verifier && $params['redirect_uri'] === $config['redirect_uri'], 'token form');
|
||||
iamExpect(in_array('Authorization: Basic ' . base64_encode('zyt:secret'), $headers, true), 'client authentication');
|
||||
$data = ['id_token' => $token];
|
||||
} elseif (str_ends_with($url, '/oauth/jwks')) { $data = $jwks; }
|
||||
elseif (str_ends_with($url, '/provisioning-context')) {
|
||||
iamExpect(in_array('Authorization: Bearer application-secret', $headers, true), 'application bearer');
|
||||
iamExpect(str_contains($url, '/zyt%20app/employees/employee-7/'), 'encoded provisioning path');
|
||||
$data = ['externalAccountId' => '9'];
|
||||
} else { throw new RuntimeException('Unexpected request'); }
|
||||
return ['status' => 200, 'body' => json_encode($data, JSON_THROW_ON_ERROR)];
|
||||
};
|
||||
$client = new IamOidcClient($config, $transport);
|
||||
parse_str(parse_url($client->authorizationUrl(str_repeat('s', 32), $nonce, $verifier), PHP_URL_QUERY), $query);
|
||||
iamExpect($query['code_challenge_method'] === 'S256' && $query['code_challenge'] === iamB64(hash('sha256', $verifier, true)), 'PKCE S256');
|
||||
iamExpect($client->exchange('code', $verifier, $nonce)['sub'] === 'employee-7', 'valid token');
|
||||
iamExpect($client->provisioning('employee-7')['externalAccountId'] === '9', 'provisioning');
|
||||
|
||||
foreach (['iss' => 'https://other.example.test', 'aud' => 'other', 'nonce' => 'wrong', 'exp' => time() - 1,
|
||||
'nbf' => time() + 100, 'iat' => time() + 100, 'azp' => 'other', 'sub' => ''] as $field => $value) {
|
||||
$changed = $claims;
|
||||
$changed[$field] = $value;
|
||||
$token = JWT::encode($changed, $key, 'RS256', 'test');
|
||||
iamReject(fn() => $client->exchange('code', $verifier, $nonce), $field);
|
||||
}
|
||||
$changed = $claims;
|
||||
$changed['aud'] = ['zyt', 'other'];
|
||||
$token = JWT::encode($changed, $key, 'RS256', 'test');
|
||||
iamReject(fn() => $client->exchange('code', $verifier, $nonce), 'multiple audiences without azp');
|
||||
$changed['azp'] = 'zyt';
|
||||
$token = JWT::encode($changed, $key, 'RS256', 'test');
|
||||
iamExpect($client->exchange('code', $verifier, $nonce)['azp'] === 'zyt', 'multiple audience azp');
|
||||
$token = JWT::encode($claims, $other, 'RS256', 'test');
|
||||
iamReject(fn() => $client->exchange('code', $verifier, $nonce), 'wrong signature');
|
||||
$token = JWT::encode($claims, str_repeat('s', 64), 'HS256', 'test');
|
||||
iamReject(fn() => $client->exchange('code', $verifier, $nonce), 'algorithm confusion');
|
||||
$token = JWT::encode($claims, $key, 'RS256', 'unknown');
|
||||
iamReject(fn() => $client->exchange('code', $verifier, $nonce), 'unknown kid');
|
||||
iamReject(fn() => $client->authorizationUrl(str_repeat('s', 32), $nonce, 'short'), 'short verifier');
|
||||
|
||||
$metadata['issuer'] = 'https://other.example.test';
|
||||
iamReject(fn() => (new IamOidcClient($config, $transport))->authorizationUrl(str_repeat('s', 32), $nonce, $verifier), 'discovery issuer');
|
||||
$metadata['issuer'] = $config['issuer'];
|
||||
$metadata['jwks_uri'] = 'https://other.example.test/jwks';
|
||||
iamReject(fn() => (new IamOidcClient($config, $transport))->authorizationUrl(str_repeat('s', 32), $nonce, $verifier), 'cross-origin JWKS');
|
||||
foreach (['http://iam.example.test', 'https://user@iam.example.test', 'https://iam.example.test/#fragment'] as $invalid) {
|
||||
$bad = $config;
|
||||
$bad['issuer'] = $invalid;
|
||||
iamReject(fn() => new IamOidcClient($bad, $transport), 'invalid HTTPS configuration');
|
||||
}
|
||||
$bad = $config;
|
||||
$bad['api_url'] = 'https://other.example.test';
|
||||
iamReject(fn() => new IamOidcClient($bad, $transport), 'cross-origin provisioning');
|
||||
iamReject(fn() => (new IamOidcClient($config, fn() => ['status' => 302, 'body' => '{}']))->authorizationUrl(str_repeat('s', 32), $nonce, $verifier), 'HTTP redirect');
|
||||
iamReject(fn() => (new IamOidcClient($config, fn() => ['status' => 200, 'body' => 'invalid']))->authorizationUrl(str_repeat('s', 32), $nonce, $verifier), 'invalid JSON');
|
||||
echo "IamOidcClientTest passed (offline RSA, claims, discovery, PKCE, provisioning, HTTPS)\n";
|
||||
Reference in New Issue
Block a user