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