Files
daily-meal-api/app/Services/AppleOAuthTokenClient.php
T
leonm ddc80956a0
CI / 🧪 Tests Laravel (push) Failing after 9s
CI / 🐳 Build & Push Images (push) Has been skipped
feat: google login
2026-08-17 10:06:35 +02:00

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);
}
}