114 lines
4.4 KiB
PHP
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);
|
|
}
|
|
}
|