WebFingerProvider   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 144
Duplicated Lines 0 %

Test Coverage

Coverage 77.27%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 69
dl 0
loc 144
ccs 51
cts 66
cp 0.7727
rs 10
c 1
b 0
f 0
wmc 21

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
B normalizeWebfinger() 0 39 6
A isAllowedUri() 0 3 1
C fetch() 0 64 13
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Facile\OpenIDClient\Issuer\Metadata\Provider;
6
7
use function array_key_exists;
8
use function array_pop;
9
use function explode;
10
use Facile\OpenIDClient\Exception\InvalidArgumentException;
11
use Facile\OpenIDClient\Exception\RuntimeException;
12
use function Facile\OpenIDClient\parse_metadata_response;
13
use function http_build_query;
14
use function is_array;
15
use function is_string;
16
use function parse_url;
17
use function preg_match;
18
use function preg_replace;
19
use Psr\Http\Client\ClientExceptionInterface;
20
use Psr\Http\Client\ClientInterface;
21
use Psr\Http\Message\RequestFactoryInterface;
22
use Psr\Http\Message\UriFactoryInterface;
23
use function strpos;
24
use function substr;
25
26
final class WebFingerProvider implements RemoteProviderInterface, WebFingerProviderInterface
27
{
28
    private const OIDC_DISCOVERY = '/.well-known/openid-configuration';
29
30
    private const WEBFINGER = '/.well-known/webfinger';
31
32
    private const REL = 'http://openid.net/specs/connect/1.0/issuer';
33
34
    private const AAD_MULTITENANT_DISCOVERY = 'https://login.microsoftonline.com/common/v2.0$' . self::OIDC_DISCOVERY;
35
36
    /** @var ClientInterface */
37
    private $client;
38
39
    /** @var RequestFactoryInterface */
40
    private $requestFactory;
41
42
    /** @var UriFactoryInterface */
43
    private $uriFactory;
44
45
    /** @var DiscoveryProviderInterface */
46
    private $discoveryProvider;
47
48 1
    public function __construct(
49
        ClientInterface $client,
50
        RequestFactoryInterface $requestFactory,
51
        UriFactoryInterface $uriFactory,
52
        DiscoveryProviderInterface $discoveryProvider
53
    ) {
54 1
        $this->client = $client;
55 1
        $this->requestFactory = $requestFactory;
56 1
        $this->uriFactory = $uriFactory;
57 1
        $this->discoveryProvider = $discoveryProvider;
58 1
    }
59
60
    public function isAllowedUri(string $uri): bool
61
    {
62
        return true;
63
    }
64
65 1
    public function fetch(string $resource): array
66
    {
67 1
        $resource = $this->normalizeWebfinger($resource);
68 1
        $parsedUrl = parse_url(
69 1
            false !== strpos($resource, '@')
70 1
                ? 'https://' . explode('@', $resource)[1]
71 1
                : $resource
72
        );
73
74 1
        if (! is_array($parsedUrl) || ! array_key_exists('host', $parsedUrl)) {
0 ignored issues
show
introduced by
The condition is_array($parsedUrl) is always true.
Loading history...
75
            throw new RuntimeException('Unable to parse resource');
76
        }
77
78 1
        $host = $parsedUrl['host'];
79
80
        /** @var string|int|null $port */
81 1
        $port = $parsedUrl['port'] ?? null;
82
83 1
        if (((int) $port) > 0) {
84
            $host .= ':' . ((int) $port);
85
        }
86
87 1
        $webFingerUrl = $this->uriFactory->createUri('https://' . $host . self::WEBFINGER)
88 1
            ->withQuery(http_build_query(['resource' => $resource, 'rel' => self::REL]));
89
90 1
        $request = $this->requestFactory->createRequest('GET', $webFingerUrl)
91 1
            ->withHeader('accept', 'application/json');
92
93
        try {
94 1
            $data = parse_metadata_response($this->client->sendRequest($request));
95
        } catch (ClientExceptionInterface $e) {
96
            throw new RuntimeException('Unable to fetch provider metadata', 0, $e);
97
        }
98
99
        /** @var array<array-key, null|array{rel?: string, href?: string}> $links */
100 1
        $links = $data['links'] ?? [];
101 1
        $href = null;
102 1
        foreach ($links as $link) {
103 1
            if (! is_array($link)) {
104
                continue;
105
            }
106
107 1
            if (($link['rel'] ?? null) !== self::REL) {
108 1
                continue;
109
            }
110
111 1
            if (! array_key_exists('href', $link)) {
112 1
                continue;
113
            }
114
115 1
            $href = $link['href'];
116
        }
117
118 1
        if (! is_string($href) || 0 !== strpos($href, 'https://')) {
119
            throw new InvalidArgumentException('Invalid issuer location');
120
        }
121
122 1
        $metadata = $this->discoveryProvider->discovery($href);
123
124 1
        if (($metadata['issuer'] ?? null) !== $href) {
125
            throw new RuntimeException('Discovered issuer mismatch');
126
        }
127
128 1
        return $metadata;
129
    }
130
131 1
    private function normalizeWebfinger(string $input): string
132
    {
133 1
        $hasScheme = static function (string $resource): bool {
134 1
            if (false !== strpos($resource, '://')) {
135
                return true;
136
            }
137
138 1
            $authority = explode('#', (string) preg_replace('/(\/|\?)/', '#', $resource))[0];
139
140 1
            if (false === ($index = strpos($authority, ':'))) {
141 1
                return false;
142
            }
143
144
            $hostOrPort = substr($resource, $index + 1);
145
146
            return ! (bool) preg_match('/^\d+$/', $hostOrPort);
147 1
        };
148
149 1
        $acctSchemeAssumed = static function (string $input): bool {
150 1
            if (false === strpos($input, '@')) {
151
                return false;
152
            }
153
154 1
            $parts = explode('@', $input);
155
            /** @var string $host */
156 1
            $host = array_pop($parts);
157
158 1
            return ! (bool) preg_match('/[:\/?]+/', $host);
159 1
        };
160
161 1
        if ($hasScheme($input)) {
162
            $output = $input;
163 1
        } elseif ($acctSchemeAssumed($input)) {
164 1
            $output = 'acct:' . $input;
165
        } else {
166
            $output = 'https://' . $input;
167
        }
168
169 1
        return explode('#', $output)[0];
170
    }
171
}
172