verifiedClaims($identityToken); $this->validateNonce($claims, $nonce); return (new SocialiteUser)->setRaw($claims)->map([ 'id' => Arr::get($claims, 'sub'), 'nickname' => null, 'name' => null, 'email' => Arr::get($claims, 'email'), 'avatar' => null, ]); } public function subjectFromToken(string $identityToken): string { $subject = Arr::get($this->verifiedClaims($identityToken), 'sub'); if (! is_string($subject) || $subject === '') { abort(401, 'SOCIAL_TOKEN_INVALID'); } return $subject; } /** * @return array */ private function verifiedClaims(string $identityToken): array { try { $claims = (array) JWT::decode( $identityToken, JWK::parseKeySet($this->jwkSetForToken($identityToken)), ); $this->validateStandardClaims($claims); return $claims; } catch (HttpExceptionInterface $exception) { throw $exception; } catch (ConnectionException) { abort(503, 'SOCIAL_PROVIDER_UNAVAILABLE'); } catch (Throwable) { abort(401, 'SOCIAL_TOKEN_INVALID'); } } /** * @return array */ private function jwkSetForToken(string $identityToken): array { $keyId = $this->tokenKeyId($identityToken); $jwkSet = $this->jwkSet(); if ($this->jwkSetContains($jwkSet, $keyId)) { return $jwkSet; } Cache::forget(self::JWKS_CACHE_KEY); return $this->jwkSet(); } /** * @return array */ private function jwkSet(): array { return Cache::remember( self::JWKS_CACHE_KEY, now()->addHours(6), function (): array { $url = (string) config('services.apple.jwks_url'); if ($url === '') { abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED'); } $response = Http::acceptJson() ->connectTimeout(3) ->timeout(10) ->retry([100, 500], throw: false) ->get($url); $jwkSet = $response->successful() ? $response->json() : null; if (! is_array($jwkSet) || ! isset($jwkSet['keys']) || ! is_array($jwkSet['keys'])) { abort(503, 'SOCIAL_PROVIDER_UNAVAILABLE'); } return $jwkSet; }, ); } private function tokenKeyId(string $identityToken): string { $segments = explode('.', $identityToken); if (count($segments) !== 3) { abort(401, 'SOCIAL_TOKEN_INVALID'); } $header = JWT::jsonDecode(JWT::urlsafeB64Decode($segments[0])); $keyId = $header->kid ?? null; if (! is_string($keyId) || $keyId === '') { abort(401, 'SOCIAL_TOKEN_INVALID'); } return $keyId; } /** * @param array $jwkSet */ private function jwkSetContains(array $jwkSet, string $keyId): bool { return collect($jwkSet['keys'] ?? [])->contains( fn (mixed $key): bool => is_array($key) && ($key['kid'] ?? null) === $keyId, ); } /** * @param array $claims */ private function validateStandardClaims(array $claims): void { $issuer = (string) config('services.apple.issuer'); $tokenIssuer = Arr::get($claims, 'iss'); if (! is_string($tokenIssuer) || $issuer === '' || ! hash_equals($issuer, $tokenIssuer)) { abort(401, 'SOCIAL_TOKEN_INVALID'); } $allowedClientIds = collect(explode(',', (string) config('services.apple.client_ids'))) ->map(fn (string $clientId): string => trim($clientId)) ->filter() ->values() ->all(); $tokenAudiences = array_filter( Arr::wrap(Arr::get($claims, 'aud')), fn (mixed $audience): bool => is_string($audience), ); if ($allowedClientIds === []) { abort(503, 'SOCIAL_PROVIDER_NOT_CONFIGURED'); } if (array_intersect($allowedClientIds, $tokenAudiences) === []) { abort(401, 'SOCIAL_TOKEN_INVALID'); } } /** * @param array $claims */ private function validateNonce(array $claims, string $nonce): void { if (! $this->socialAuthenticationNonce->consume( SocialProvider::Apple, $nonce, Arr::get($claims, 'nonce'), )) { abort(401, 'SOCIAL_TOKEN_INVALID'); } } }