117 lines
3.8 KiB
PHP
117 lines
3.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Auth;
|
|
|
|
use Illuminate\Http\Client\RequestException;
|
|
use Illuminate\Support\Facades\Http;
|
|
use RuntimeException;
|
|
|
|
class DingTalkOAuthClient
|
|
{
|
|
public function isConfigured(): bool
|
|
{
|
|
return (bool) config('dingtalk.enabled')
|
|
&& $this->configuredString('dingtalk.client_id') !== ''
|
|
&& $this->configuredString('dingtalk.client_secret') !== ''
|
|
&& $this->configuredString('dingtalk.redirect_uri') !== '';
|
|
}
|
|
|
|
public function authorizationUrl(string $state): string
|
|
{
|
|
$this->ensureConfigured();
|
|
|
|
return rtrim($this->configuredString('dingtalk.authorize_url'), '?') . '?' . http_build_query([
|
|
'redirect_uri' => $this->configuredString('dingtalk.redirect_uri'),
|
|
'response_type' => 'code',
|
|
'client_id' => $this->configuredString('dingtalk.client_id'),
|
|
'scope' => 'openid',
|
|
'prompt' => 'consent',
|
|
'state' => $state,
|
|
], '', '&', PHP_QUERY_RFC3986);
|
|
}
|
|
|
|
/**
|
|
* @return array{access_token: string, expires_in: int|null}
|
|
*/
|
|
public function exchangeAuthorizationCode(string $code): array
|
|
{
|
|
$this->ensureConfigured();
|
|
|
|
try {
|
|
$response = Http::asJson()
|
|
->acceptJson()
|
|
->timeout($this->timeout())
|
|
->post($this->configuredString('dingtalk.token_url'), [
|
|
'clientId' => $this->configuredString('dingtalk.client_id'),
|
|
'clientSecret' => $this->configuredString('dingtalk.client_secret'),
|
|
'code' => $code,
|
|
'grantType' => 'authorization_code',
|
|
])
|
|
->throw();
|
|
} catch (RequestException $exception) {
|
|
throw new RuntimeException('DingTalk token exchange failed.', 0, $exception);
|
|
}
|
|
|
|
$accessToken = trim((string) $response->json('accessToken'));
|
|
if ($accessToken === '') {
|
|
throw new RuntimeException('DingTalk token response did not contain an access token.');
|
|
}
|
|
|
|
return [
|
|
'access_token' => $accessToken,
|
|
'expires_in' => is_numeric($response->json('expireIn')) ? (int) $response->json('expireIn') : null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{union_id: string, open_id: string|null, nickname: string|null}
|
|
*/
|
|
public function currentUser(string $accessToken): array
|
|
{
|
|
try {
|
|
$response = Http::acceptJson()
|
|
->withHeaders(['x-acs-dingtalk-access-token' => $accessToken])
|
|
->timeout($this->timeout())
|
|
->get($this->configuredString('dingtalk.user_url'))
|
|
->throw();
|
|
} catch (RequestException $exception) {
|
|
throw new RuntimeException('DingTalk user lookup failed.', 0, $exception);
|
|
}
|
|
|
|
$unionId = trim((string) $response->json('unionId'));
|
|
if ($unionId === '') {
|
|
throw new RuntimeException('DingTalk user response did not contain a union id.');
|
|
}
|
|
|
|
return [
|
|
'union_id' => $unionId,
|
|
'open_id' => $this->nullableString($response->json('openId')),
|
|
'nickname' => $this->nullableString($response->json('nick')),
|
|
];
|
|
}
|
|
|
|
protected function ensureConfigured(): void
|
|
{
|
|
if (! $this->isConfigured()) {
|
|
throw new RuntimeException('DingTalk login is not configured.');
|
|
}
|
|
}
|
|
|
|
protected function configuredString(string $key): string
|
|
{
|
|
return trim((string) config($key, ''));
|
|
}
|
|
|
|
protected function timeout(): int
|
|
{
|
|
return max(3, min(30, (int) config('dingtalk.timeout', 10)));
|
|
}
|
|
|
|
protected function nullableString(mixed $value): ?string
|
|
{
|
|
$value = trim((string) $value);
|
|
|
|
return $value === '' ? null : $value;
|
|
}
|
|
}
|