U2fService::createSignRequest()   C
last analyzed

Complexity

Conditions 10
Paths 13

Size

Total Lines 70

Duplication

Lines 11
Ratio 15.71 %

Importance

Changes 0
Metric Value
dl 11
loc 70
rs 6.7878
c 0
b 0
f 0
cc 10
nc 13
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Copyright 2015 SURFnet B.V.
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
namespace Surfnet\StepupRa\RaBundle\Service;
20
21
use GuzzleHttp\Client;
22
use Psr\Log\LoggerInterface;
23
use RuntimeException as CoreRuntimeException;
24
use Surfnet\StepupBundle\Http\JsonHelper;
25
use Surfnet\StepupRa\RaBundle\Command\CreateU2fSignRequestCommand;
26
use Surfnet\StepupRa\RaBundle\Command\VerifyU2fAuthenticationCommand;
27
use Surfnet\StepupRa\RaBundle\Service\U2f\AuthenticationVerificationResult;
28
use Surfnet\StepupRa\RaBundle\Service\U2f\SignRequestCreationResult;
29
use Surfnet\StepupU2fBundle\Dto\SignRequest;
30
use Symfony\Component\Validator\ConstraintViolationInterface;
31
use Symfony\Component\Validator\ConstraintViolationListInterface;
32
use Symfony\Component\Validator\Validator\ValidatorInterface;
33
34
/**
35
 * @SuppressWarnings(PHPMD.CyclomaticComplexity) -- We're verifying a JSON format. Not much to do towards reducing the
36
 *     complexity.
37
 */
38
final class U2fService
39
{
40
    /**
41
     * @var \GuzzleHttp\Client
42
     */
43
    private $guzzleClient;
44
45
    /**
46
     * @var \Symfony\Component\Validator\Validator\ValidatorInterface
47
     */
48
    private $validator;
49
50
    /**
51
     * @var \Psr\Log\LoggerInterface
52
     */
53
    private $logger;
54
55
    public function __construct(Client $guzzleClient, ValidatorInterface $validator, LoggerInterface $logger)
56
    {
57
        $this->guzzleClient = $guzzleClient;
58
        $this->validator    = $validator;
59
        $this->logger       = $logger;
60
    }
61
62
    /**
63
     * @param CreateU2fSignRequestCommand $command
64
     * @return SignRequestCreationResult
65
     */
66
    public function createSignRequest(CreateU2fSignRequestCommand $command)
67
    {
68
        $this->logger->info('Create U2F sign request');
69
70
        $body = [
71
            'requester' => ['institution' => $command->institution, 'identity' => $command->identityId],
72
            'key_handle' => ['value' => $command->keyHandle],
73
        ];
74
75
        $response = $this->guzzleClient->post('api/u2f/create-sign-request', ['json' => $body, 'http_errors' => false]);
76
        $statusCode = $response->getStatusCode();
77
78
        try {
79
            $result = JsonHelper::decode((string) $response->getBody());
80
        } catch (CoreRuntimeException $e) {
81
            $this->logger->error('U2F sign request creation failed; JSON decoding failed.');
82
83
            return SignRequestCreationResult::apiError();
84
        }
85
86
        $hasErrors = isset($result['errors'])
87
            && is_array($result['errors'])
88
            && $result['errors'] === array_filter($result['errors'], 'is_string');
89
90 View Code Duplication
        if ($hasErrors && $statusCode >= 400 && $statusCode < 600) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
91
            $this->logger->critical(
92
                sprintf(
93
                    'U2F sign request creation failed; HTTP %d with errors "%s"',
94
                    $statusCode,
95
                    join(', ', $result['errors'])
96
                )
97
            );
98
99
            return SignRequestCreationResult::apiError();
100
        }
101
102
        $actualKeys = array_keys($result);
103
        $expectedKeys = ['app_id', 'challenge', 'key_handle', 'version'];
104
        if ($statusCode != 200 || array_diff($actualKeys, $expectedKeys) !== array_diff($expectedKeys, $actualKeys)) {
105
            $this->logger->critical(
106
                sprintf(
107
                    'U2F API behaving nonconformingly, returned response or status code (%d) unexpected',
108
                    $statusCode
109
                )
110
            );
111
112
            return SignRequestCreationResult::apiError();
113
        }
114
115
        $signRequest = new SignRequest();
116
        $signRequest->appId = $result['app_id'];
117
        $signRequest->challenge = $result['challenge'];
118
        $signRequest->keyHandle = $result['key_handle'];
119
        $signRequest->version = $result['version'];
120
121
        $violations = $this->validator->validate($signRequest);
122
        if (count($violations) > 0) {
123
            $this->logger->critical(
124
                sprintf(
125
                    'U2F API behaving nonconformingly, returned sign request does not validate',
126
                    $statusCode
127
                ),
128
                ['errors' => $this->mapViolationsToErrorStrings($violations, 'sign_request')]
129
            );
130
131
            return SignRequestCreationResult::apiError();
132
        }
133
134
        return SignRequestCreationResult::success($signRequest);
135
    }
136
137
    /**
138
     * @param VerifyU2fAuthenticationCommand $command
139
     * @return AuthenticationVerificationResult
140
     */
141
    public function verifyAuthentication(VerifyU2fAuthenticationCommand $command)
142
    {
143
        $this->logger->info('Create U2F sign request');
144
145
        $body = [
146
            'requester' => ['institution' => $command->institution, 'identity' => $command->identityId],
147
            'authentication' => [
148
                'request' => [
149
                    'key_handle' => $command->signRequest->keyHandle,
150
                    'version'    => $command->signRequest->version,
151
                    'challenge'  => $command->signRequest->challenge,
152
                    'app_id'     => $command->signRequest->appId,
153
                ],
154
                'response' => [
155
                    'error_code'     => $command->signResponse->errorCode,
156
                    'key_handle'     => $command->signResponse->keyHandle,
157
                    'client_data'    => $command->signResponse->clientData,
158
                    'signature_data' => $command->signResponse->signatureData,
159
                ],
160
            ],
161
        ];
162
163
        $response = $this->guzzleClient->post(
164
            'api/u2f/verify-authentication',
165
            ['json' => $body, 'http_errors' => false]
166
        );
167
        $statusCode = $response->getStatusCode();
168
169
        try {
170
            $result = JsonHelper::decode((string) $response->getBody());
171
        } catch (CoreRuntimeException $e) {
172
            $this->logger->error('U2F authentication verification failed; JSON decoding failed.');
173
174
            return AuthenticationVerificationResult::apiError();
175
        }
176
177
        $hasErrors = isset($result['errors'])
178
            && is_array($result['errors'])
179
            && $result['errors'] === array_filter($result['errors'], 'is_string');
180
181 View Code Duplication
        if ($hasErrors && $statusCode >= 400 && $statusCode < 600) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
182
            $this->logger->critical(
183
                sprintf(
184
                    'U2F authentication verification failed; HTTP %d with errors "%s"',
185
                    $statusCode,
186
                    join(', ', $result['errors'])
187
                )
188
            );
189
190
            return AuthenticationVerificationResult::apiError();
191
        }
192
193
        $hasStatus = isset($result['status']) && is_string($result['status']);
194
195
        if ($statusCode == 200 && $hasStatus && $result['status'] === 'SUCCESS') {
196
            return AuthenticationVerificationResult::success();
197
        }
198
199
        if ($statusCode >= 400 && $statusCode < 500 && $hasStatus) {
200
            return AuthenticationVerificationResult::error($result['status']);
201
        }
202
203
        $this->logger->critical(
204
            sprintf(
205
                'U2F API behaving nonconformingly, returned response or status code (%d) unexpected',
206
                $statusCode
207
            )
208
        );
209
210
        return AuthenticationVerificationResult::apiError();
211
    }
212
213
    /**
214
     * @param ConstraintViolationListInterface $violations
215
     * @param string $rootName
216
     * @return string[]
217
     */
218
    private function mapViolationsToErrorStrings(ConstraintViolationListInterface $violations, $rootName)
219
    {
220
        $errors = [];
221
222
        foreach ($violations as $violation) {
223
            /** @var ConstraintViolationInterface $violation */
224
            $errors[] = sprintf('%s.%s: %s', $rootName, $violation->getPropertyPath(), $violation->getMessage());
225
        }
226
227
        return $errors;
228
    }
229
}
230