feat: google login
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user