AbstractProvider   A
last analyzed

Complexity

Total Complexity 18

Size/Duplication

Total Lines 207
Duplicated Lines 0 %

Test Coverage

Coverage 85.14%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 76
dl 0
loc 207
ccs 63
cts 74
cp 0.8514
rs 10
c 3
b 0
f 0
wmc 18

9 Methods

Rating   Name   Duplication   Size   Complexity  
A makeAuthUrl() 0 5 1
A createAccessToken() 0 3 1
A checkNonce() 0 13 3
A getRequiredRequestParameter() 0 7 2
A makeAuthUrlV2() 0 13 2
A createConsumer() 0 5 1
A discover() 0 20 2
A getAccessTokenByRequestParameters() 0 43 3
A splitNonce() 0 18 3
1
<?php
2
/**
3
 * SocialConnect project
4
 * @author: Patsura Dmitry https://github.com/ovr <[email protected]>
5
 */
6
declare(strict_types=1);
7
8
namespace SocialConnect\OpenID;
9
10
use SocialConnect\Common\Exception\Unsupported;
11
use SocialConnect\OpenID\Exception\Unauthorized;
12
use SocialConnect\Provider\AbstractBaseProvider;
13
use SocialConnect\Provider\Consumer;
14
use SocialConnect\Provider\Exception\InvalidAccessToken;
15
use SocialConnect\Provider\Exception\InvalidResponse;
16
17
abstract class AbstractProvider extends AbstractBaseProvider
18
{
19
    /**
20
     * @return string
21
     */
22
    abstract public function getOpenIdUrl();
23
24
    /**
25
     * @var int
26
     */
27
    protected $version;
28
29
    /**
30
     * @var string
31
     */
32
    protected $loginEntrypoint;
33
34
    /**
35
     * @param bool $immediate
36
     * @return string
37
     */
38 1
    protected function makeAuthUrlV2($immediate)
39
    {
40
        $params = [
41
            'openid.ns' => 'http://specs.openid.net/auth/2.0',
42 1
            'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup',
43 1
            'openid.return_to' => $this->getRedirectUrl(),
44 1
            'openid.realm' => $this->getRedirectUrl()
45
        ];
46
47 1
        $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1';
48 1
        $params['openid.identity'] = $params['openid.claimed_id'] = 'http://specs.openid.net/auth/2.0/identifier_select';
49
50 1
        return $this->loginEntrypoint . '?' . http_build_query($params, '', '&');
51
    }
52
53
    /**
54
     * @param string $url
55
     * @return string
56
     * @throws InvalidResponse
57
     * @throws \Psr\Http\Client\ClientExceptionInterface
58
     */
59 2
    protected function discover(string $url)
60
    {
61 2
        $response = $this->executeRequest(
62 2
            $this->httpStack->createRequest('GET', $url)
63
        );
64
65 2
        $contentType = $response->getHeaderLine('Content-Type');
66 2
        if (strpos($contentType, 'application/xrds+xml;charset=utf-8') === false) {
67
            throw new InvalidResponse(
68
                'Unexpected Content-Type',
69
                $response
70
            );
71
        }
72
73 2
        $xml = new \SimpleXMLElement($response->getBody()->getContents());
74
75 2
        $this->version = 2;
76 2
        $this->loginEntrypoint = (string)$xml->XRD->Service->URI;
77
78 2
        return $this->getOpenIdUrl();
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84 1
    public function makeAuthUrl(): string
85
    {
86 1
        $this->discover($this->getOpenIdUrl());
87
88 1
        return $this->makeAuthUrlV2(false);
89
    }
90
91
    /**
92
     * @param string $identity
93
     * @return string
94
     */
95
    abstract protected function parseUserIdFromIdentity($identity): string;
96
97 1
    protected function getRequiredRequestParameter(array $requestParameters, string $key)
98
    {
99 1
        if (isset($requestParameters[$key])) {
100 1
            return $requestParameters[$key];
101
        }
102
103
        throw new Unauthorized("There is no required parameter called: '{$key}'");
104
    }
105
106
    /**
107
     * @param string $nonce
108
     * @return int
109
     * @throws Unauthorized
110
     */
111 1
    protected function splitNonce(string $nonce): int
112
    {
113 1
        $result = preg_match('/(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z(.*)/', $nonce, $matches);
114 1
        if ($result !== 1) {
115
            throw new Unauthorized('Unexpected nonce format');
116
        }
117
118 1
        list(,$year, $month, $day, $hour, $min, $sec) = $matches;
119
120
        try {
121 1
            $timestamp = new \DateTime('now', new \DateTimeZone('UTC'));
122 1
            $timestamp->setDate((int) $year, (int) $month, (int) $day);
123 1
            $timestamp->setTime((int) $hour, (int) $min, (int) $sec);
124
        } catch (\Throwable $e) {
125
            throw new Unauthorized('Timestamp from nonce is not valid', $e->getCode(), $e);
126
        }
127
128 1
        return $timestamp->getTimestamp();
129
    }
130
131
    /**
132
     * @param string $nonce
133
     * @return void
134
     * @throws Unauthorized
135
     */
136 1
    protected function checkNonce(string $nonce): void
137
    {
138 1
        $stamp = $this->splitNonce($nonce);
139
140 1
        $skew = 60;
141 1
        $now = time();
142
143 1
        if ($stamp <= $now - $skew) {
144
            throw new Unauthorized("Timestamp from nonce is earlier then time() - {$skew}s");
145
        }
146
147 1
        if ($stamp >= $now + $skew) {
148
            throw new Unauthorized("Timestamp from nonce is older then time() + {$skew}s");
149
        }
150
    }
151
152
    /**
153
     * @link http://openid.net/specs/openid-authentication-2_0.html#verification
154
     *
155
     * @param array $requestParameters
156
     * @return AccessToken
157
     * @throws InvalidAccessToken
158
     * @throws InvalidResponse
159
     * @throws Unauthorized
160
     * @throws \Psr\Http\Client\ClientExceptionInterface
161
     */
162 1
    public function getAccessTokenByRequestParameters(array $requestParameters)
163
    {
164 1
        $nonce = $this->getRequiredRequestParameter($requestParameters, 'openid_response_nonce');
165 1
        $this->checkNonce($nonce);
166
167
        $params = [
168 1
            'openid.assoc_handle' => $this->getRequiredRequestParameter($requestParameters, 'openid_assoc_handle'),
169 1
            'openid.signed' => $this->getRequiredRequestParameter($requestParameters, 'openid_signed'),
170 1
            'openid.sig' => $this->getRequiredRequestParameter($requestParameters, 'openid_sig'),
171 1
            'openid.ns' => $this->getRequiredRequestParameter($requestParameters, 'openid_ns'),
172 1
            'openid.op_endpoint' => $requestParameters['openid_op_endpoint'],
173 1
            'openid.claimed_id' => $requestParameters['openid_claimed_id'],
174 1
            'openid.identity' => $requestParameters['openid_identity'],
175 1
            'openid.return_to' => $this->getRedirectUrl(),
176
            'openid.response_nonce' => $nonce,
177
            'openid.mode' => 'check_authentication'
178
        ];
179
180 1
        if (isset($requestParameters['openid_claimed_id'])) {
181 1
            $claimedId = $requestParameters['openid_claimed_id'];
182
        } else {
183
            $claimedId = $requestParameters['openid_identity'];
184
        }
185
186 1
        $this->discover($claimedId);
187
188 1
        $response = $this->executeRequest(
189 1
            $this->httpStack->createRequest('POST', $this->loginEntrypoint)
190 1
                ->withHeader('Content-Type', 'application/x-www-form-urlencoded')
191 1
                ->withBody($this->httpStack->createStream(http_build_query($params, '', '&')))
192
        );
193
194 1
        $content = $response->getBody()->getContents();
195 1
        if (preg_match('/is_valid\s*:\s*true/i', $content)) {
196 1
            return new AccessToken(
197 1
                $requestParameters['openid_identity'],
198 1
                $this->parseUserIdFromIdentity(
199 1
                    $requestParameters['openid_identity']
200
                )
201
            );
202
        }
203
204
        throw new InvalidAccessToken;
205
    }
206
207
    /**
208
     * {@inheritDoc}
209
     */
210 7
    protected function createConsumer(array $parameters): Consumer
211
    {
212 7
        return new Consumer(
213 7
            $this->getRequiredStringParameter('applicationId', $parameters),
214
            ''
215
        );
216
    }
217
218
    /**
219
     * {@inheritDoc}
220
     */
221
    public function createAccessToken(array $information)
222
    {
223
        throw new Unsupported('It\'s useful to use this method for OpenID, are you sure?');
224
    }
225
}
226