Files
zyt/server/app/common/service/iam/IamOidcService.php
T

114 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
namespace app\common\service\iam;
use RuntimeException;
use think\facade\Cache;
class IamOidcService
{
private array $config;
private IamHttpClient $http;
public function __construct(?array $config = null, ?IamHttpClient $http = null)
{
$this->config = $config ?? (array) config('iam_hub');
$this->http = $http ?? new IamHttpClient(null, (int) ($this->config['http_timeout_seconds'] ?? 10));
}
public function enabled(): bool
{
return (bool) ($this->config['enabled'] ?? false)
&& trim((string) ($this->config['application_token'] ?? '')) !== ''
&& trim((string) ($this->config['oidc_client_secret'] ?? '')) !== ''
&& trim((string) ($this->config['oidc_redirect_uri'] ?? '')) !== '';
}
public function begin(): string
{
$this->assertEnabled();
$state = IamSecurity::base64UrlEncode(random_bytes(32));
$nonce = IamSecurity::base64UrlEncode(random_bytes(32));
$verifier = IamSecurity::base64UrlEncode(random_bytes(64));
$challenge = IamSecurity::base64UrlEncode(hash('sha256', $verifier, true));
Cache::set($this->stateKey($state), [
'nonce' => $nonce,
'verifier' => $verifier,
], (int) ($this->config['state_ttl_seconds'] ?? 300));
return $this->authorizationUrl($state, $nonce, $challenge);
}
public function authorizationUrl(string $state, string $nonce, string $challenge): string
{
return rtrim((string) ($this->config['oidc_issuer'] ?? ''), '/')
. '/protocol/openid-connect/auth?'
. http_build_query([
'client_id' => (string) ($this->config['oidc_client_id'] ?? ''),
'redirect_uri' => (string) ($this->config['oidc_redirect_uri'] ?? ''),
'response_type' => 'code',
'scope' => 'openid profile email',
'state' => $state,
'nonce' => $nonce,
'code_challenge' => $challenge,
'code_challenge_method' => 'S256',
], '', '&', PHP_QUERY_RFC3986);
}
/** @return array<string,mixed> */
public function callback(string $code, string $state): array
{
$this->assertEnabled();
$stateData = Cache::pull($this->stateKey($state));
if (!is_array($stateData) || empty($stateData['nonce']) || empty($stateData['verifier'])) {
throw new IamHubException('统一身份登录状态无效或已过期');
}
$tokenResult = $this->http->form($this->internalBaseUrl() . '/protocol/openid-connect/token', [
'grant_type' => 'authorization_code',
'client_id' => (string) ($this->config['oidc_client_id'] ?? ''),
'client_secret' => (string) ($this->config['oidc_client_secret'] ?? ''),
'redirect_uri' => (string) ($this->config['oidc_redirect_uri'] ?? ''),
'code' => $code,
'code_verifier' => (string) $stateData['verifier'],
]);
if ($tokenResult['status'] !== 200) {
throw new IamHubException('统一身份授权码交换失败', 502);
}
$idToken = (string) ($tokenResult['body']['id_token'] ?? '');
$accessToken = (string) ($tokenResult['body']['access_token'] ?? '');
if ($idToken === '' || $accessToken === '') {
throw new IamHubException('统一身份令牌响应不完整', 502);
}
$jwksResult = $this->http->json('GET', $this->internalBaseUrl() . '/protocol/openid-connect/certs');
if ($jwksResult['status'] !== 200) {
throw new IamHubException('统一身份签名密钥获取失败', 502);
}
return (new IamJwtVerifier(
(string) ($this->config['oidc_issuer'] ?? ''),
(string) ($this->config['oidc_client_id'] ?? '')
))->verify($idToken, $accessToken, (string) $stateData['nonce'], $jwksResult['body']);
}
private function assertEnabled(): void
{
if (!$this->enabled()) {
throw new IamHubException('统一身份登录尚未配置', 503);
}
}
private function internalBaseUrl(): string
{
$internal = trim((string) ($this->config['oidc_internal_base_url'] ?? ''));
return rtrim($internal !== '' ? $internal : (string) ($this->config['oidc_issuer'] ?? ''), '/');
}
private function stateKey(string $state): string
{
return 'iam_oidc_state_' . hash('sha256', $state);
}
}