155 lines
4.8 KiB
PHP
155 lines
4.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Firebase\JWT\JWT;
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
use Illuminate\Http\Client\PendingRequest;
|
|
use Illuminate\Http\Client\RequestException;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
|
use Throwable;
|
|
|
|
class AppleOAuthTokenClient
|
|
{
|
|
public function __construct(private AppleIdentityTokenVerifier $identityTokenVerifier) {}
|
|
|
|
public function exchangeAuthorizationCode(string $authorizationCode, string $expectedSubject): string
|
|
{
|
|
try {
|
|
$response = $this->request()
|
|
->post($this->configuredUrl('token_url'), [
|
|
'client_id' => $this->clientId(),
|
|
'client_secret' => $this->clientSecret(),
|
|
'code' => $authorizationCode,
|
|
'grant_type' => 'authorization_code',
|
|
]);
|
|
} catch (HttpExceptionInterface $exception) {
|
|
throw $exception;
|
|
} catch (ConnectionException) {
|
|
abort(503, 'SOCIAL_PROVIDER_UNAVAILABLE');
|
|
}
|
|
|
|
if (! $response->successful()) {
|
|
$error = $response->json('error');
|
|
|
|
if ($error === 'invalid_grant') {
|
|
abort(401, 'SOCIAL_AUTHORIZATION_CODE_INVALID');
|
|
}
|
|
|
|
abort(503, 'SOCIAL_PROVIDER_UNAVAILABLE');
|
|
}
|
|
|
|
$refreshToken = $response->json('refresh_token');
|
|
$identityToken = $response->json('id_token');
|
|
|
|
if (! is_string($refreshToken) || $refreshToken === '' || ! is_string($identityToken)) {
|
|
abort(503, 'SOCIAL_PROVIDER_UNAVAILABLE');
|
|
}
|
|
|
|
$returnedSubject = $this->identityTokenVerifier->subjectFromToken($identityToken);
|
|
|
|
if (! hash_equals($expectedSubject, $returnedSubject)) {
|
|
abort(401, 'SOCIAL_TOKEN_INVALID');
|
|
}
|
|
|
|
return $refreshToken;
|
|
}
|
|
|
|
public function revoke(string $refreshToken): void
|
|
{
|
|
try {
|
|
$response = $this->request()
|
|
->retry(
|
|
[100, 500],
|
|
when: fn (Throwable $exception): bool => $exception instanceof ConnectionException
|
|
|| ($exception instanceof RequestException && $exception->response->serverError()),
|
|
throw: false,
|
|
)
|
|
->post($this->configuredUrl('revoke_url'), [
|
|
'client_id' => $this->clientId(),
|
|
'client_secret' => $this->clientSecret(),
|
|
'token' => $refreshToken,
|
|
'token_type_hint' => 'refresh_token',
|
|
]);
|
|
} catch (HttpExceptionInterface $exception) {
|
|
throw $exception;
|
|
} catch (Throwable) {
|
|
abort(503, 'SOCIAL_REVOCATION_FAILED');
|
|
}
|
|
|
|
if (! $response->successful()) {
|
|
abort(503, 'SOCIAL_REVOCATION_FAILED');
|
|
}
|
|
}
|
|
|
|
private function clientSecret(): string
|
|
{
|
|
$teamId = (string) config('services.apple.team_id');
|
|
$keyId = (string) config('services.apple.key_id');
|
|
$privateKey = $this->privateKey();
|
|
$issuer = (string) config('services.apple.issuer');
|
|
|
|
if ($teamId === '' || $keyId === '' || $issuer === '' || $privateKey === '') {
|
|
abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED');
|
|
}
|
|
|
|
try {
|
|
$issuedAt = now()->timestamp;
|
|
|
|
return JWT::encode([
|
|
'aud' => $issuer,
|
|
'exp' => $issuedAt + 300,
|
|
'iat' => $issuedAt,
|
|
'iss' => $teamId,
|
|
'sub' => $this->clientId(),
|
|
], $privateKey, 'ES256', $keyId);
|
|
} catch (Throwable) {
|
|
abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED');
|
|
}
|
|
}
|
|
|
|
private function clientId(): string
|
|
{
|
|
$clientId = (string) config('services.apple.client_id');
|
|
|
|
if ($clientId === '') {
|
|
abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED');
|
|
}
|
|
|
|
return $clientId;
|
|
}
|
|
|
|
private function configuredUrl(string $key): string
|
|
{
|
|
$url = (string) config('services.apple.'.$key);
|
|
|
|
if ($url === '') {
|
|
abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED');
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
private function privateKey(): string
|
|
{
|
|
$base64PrivateKey = (string) config('services.apple.private_key_base64');
|
|
|
|
if ($base64PrivateKey !== '') {
|
|
$decodedPrivateKey = base64_decode($base64PrivateKey, true);
|
|
|
|
return is_string($decodedPrivateKey) ? $decodedPrivateKey : '';
|
|
}
|
|
|
|
return str_replace('\\n', "\n", (string) config('services.apple.private_key'));
|
|
}
|
|
|
|
private function request(): PendingRequest
|
|
{
|
|
return Http::asForm()
|
|
->acceptJson()
|
|
->connectTimeout(3)
|
|
->timeout(10);
|
|
}
|
|
}
|