Completed
Push — master ( bab30e...5d9c47 )
by Stefan
18s queued 14s
created

AuthProcess::main()   C

Complexity

Conditions 13
Paths 25

Size

Total Lines 111
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 111
rs 6.6166
c 1
b 0
f 0
eloc 56
cc 13
nc 25
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
namespace SimpleSAML\Module\webauthn\Controller;
4
5
use Datetime;
6
use Exception;
7
use SimpleSAML\Auth;
8
use SimpleSAML\Configuration;
9
use SimpleSAML\Error;
10
use SimpleSAML\HTTP\RunnableResponse;
11
use SimpleSAML\Logger;
12
use SimpleSAML\Module;
13
use SimpleSAML\Module\webauthn\WebAuthn\WebAuthnAbstractEvent;
14
use SimpleSAML\Module\webauthn\WebAuthn\WebAuthnAuthenticationEvent;
15
use SimpleSAML\Session;
16
use SimpleSAML\Utils;
17
use SimpleSAML\XHTML\Template;
18
use Symfony\Component\HttpFoundation\Request;
19
use Symfony\Component\HttpFoundation\RedirectResponse;
20
use Symfony\Component\HttpFoundation\Response;
21
use Symfony\Component\HttpFoundation\StreamedResponse;
22
23
/**
24
 * Controller class for the webauthn module.
25
 *
26
 * This class serves the different views available in the module.
27
 *
28
 * @package SimpleSAML\Module\webauthn
29
 */
30
class AuthProcess
31
{
32
    /** @var \SimpleSAML\Configuration */
33
    protected $config;
34
35
    /** @var \SimpleSAML\Session */
36
    protected $session;
37
38
    /**
39
     * @var \SimpleSAML\Auth\State|string
40
     * @psalm-var \SimpleSAML\Auth\State|class-string
41
     */
42
    protected $authState = Auth\State::class;
43
44
    /**
45
     * @var \SimpleSAML\Logger|string
46
     * @psalm-var \SimpleSAML\Logger|class-string
47
     */
48
    protected $logger = Logger::class;
49
50
51
    /**
52
     * Controller constructor.
53
     *
54
     * It initializes the global configuration and session for the controllers implemented here.
55
     *
56
     * @param \SimpleSAML\Configuration              $config The configuration to use by the controllers.
57
     * @param \SimpleSAML\Session                    $session The session to use by the controllers.
58
     *
59
     * @throws \Exception
60
     */
61
    public function __construct(
62
        Configuration $config,
63
        Session $session
64
    ) {
65
        $this->config = $config;
66
        $this->session = $session;
67
    }
68
69
70
    /**
71
     * Inject the \SimpleSAML\Auth\State dependency.
72
     *
73
     * @param \SimpleSAML\Auth\State $authState
74
     */
75
    public function setAuthState(Auth\State $authState): void
76
    {
77
        $this->authState = $authState;
78
    }
79
80
81
    /**
82
     * Inject the \SimpleSAML\Logger dependency.
83
     *
84
     * @param \SimpleSAML\Logger $logger
85
     */
86
    public function setLogger(Logger $logger): void
87
    {
88
        $this->logger = $logger;
89
    }
90
91
92
    /**
93
     * @param \Symfony\Component\HttpFoundation\Request $request
94
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|\SimpleSAML\HTTP\RunnableResponse|\Symfony\Component\HttpFoundation\StreamedResponse
95
     *   A Symfony Response-object.
96
     */
97
    public function main(Request $request): Response
98
    {
99
        $this->logger::info('FIDO2 - Accessing WebAuthn enrollment validation');
100
101
        $stateId = $request->request->get('StateId');
102
        if ($stateId === null) {
103
            throw new Error\BadRequest('Missing required StateId query parameter.');
104
        }
105
106
        $debugEnabled = $this->config->getValue('logging.level', Logger::NOTICE) === Logger::DEBUG;
107
108
        /** @var array $state */
109
        $state = $this->authState::loadState($stateId, 'webauthn:request');
110
111
        $incomingID = bin2hex(WebAuthnAbstractEvent::base64urlDecode($request->request->get('response_id')));
112
113
        /**
114
         * §7.2 STEP 2 - 4 : check that the credential is one of those the particular user owns
115
         */
116
        $publicKey = false;
117
        $previousCounter = -1;
118
119
        foreach ($state['FIDO2Tokens'] as $oneToken) {
120
            if ($oneToken[0] == $incomingID) {
121
                // Credential ID is eligible for user $state['FIDO2Username'];
122
                // using publicKey $oneToken[1] with current counter value $oneToken[2]
123
                $publicKey = $oneToken[1];
124
                $previousCounter = $oneToken[2];
125
                break;
126
            }
127
        }
128
129
        if ($publicKey === false) {
130
            throw new Exception(
131
                "User attempted to authenticate with an unknown credential ID. This should already have been prevented by the browser!"
132
            );
133
        }
134
135
        /** @psalm-var array $oneToken */
136
        $authObject = new WebAuthnAuthenticationEvent(
137
            $request->request->get('type'),
138
            ($state['FIDO2Scope'] === null ? $state['FIDO2DerivedScope'] : $state['FIDO2Scope']),
139
            $state['FIDO2SignupChallenge'],
140
            $state['IdPMetadata']['entityid'],
141
            base64_decode($request->request->get('authenticator_data')),
142
            base64_decode($request->request->get('client_data_raw')),
143
            $oneToken[0],
144
            $oneToken[1],
145
            base64_decode($request->request->get('signature')),
146
            $debugEnabled
147
        );
148
149
        /**
150
         * §7.2 STEP 18 : detect physical object cloning on the token
151
         */
152
        $counter = $authObject->getCounter();
153
        if (($previousCounter != 0 || $counter != 0) && $counter > $previousCounter) {
154
            // Signature counter was incremented compared to last time, good
155
            $store = $state['webauthn:store'];
156
            $store->updateSignCount($oneToken[0], $counter);
157
        } else {
158
            throw new Exception(
159
                "Signature counter less or equal to a previous authentication! Token cloning likely (old: $previousCounter, new: $counter."
160
            );
161
        }
162
163
        // THAT'S IT. The user authenticated successfully. Remember the credential ID that was used.
164
        $state['FIDO2AuthSuccessful'] = $oneToken[0];
165
166
        // See if he wants to hang around for token management operations
167
        if ($request->request->get('credentialChange') === 'on') {
168
            $state['FIDO2WantsRegister'] = true;
169
        } else {
170
            $state['FIDO2WantsRegister'] = false;
171
        }
172
173
        $this->authState::saveState($state, 'webauthn:request');
174
175
        if ($debugEnabled) {
176
            $response = new StreamedResponse();
177
            $response->setCallback(function ($authObject, $state) {
178
                echo $authObject->getDebugBuffer();
179
                echo $authObject->getValidateBuffer();
180
                echo "Debug mode, not continuing to " . ($state['FIDO2WantsRegister'] ? "credential registration page." : "destination.");
181
            });
182
        } else {
183
            if ($state['FIDO2WantsRegister']) {
184
                $response = new RedirectResponse(Module::getModuleURL('webauthn/webauthn.php?StateId=' . urlencode($stateId)));
185
            } else {
186
                $response = new RunnableResponse([Auth\ProcessingChain::class, 'resumeProcessing'], [$state]);
187
            }
188
        }
189
190
        $response->headers->set('Expires', 'Thu, 19 Nov 1981 08:52:00 GMT');
191
        $response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
192
        $response->headers->set('Pragma', 'no-cache');
193
194
        /** Symfony 5 style */
195
        /**
196
        $response->setCache([
197
            'must_revalidate'  => true,
198
            'no_cache'         => true,
199
            'no_store'         => true,
200
            'no_transform'     => false,
201
            'public'           => false,
202
            'private'          => false,
203
        ]);
204
        $response->setExpires(new DateTime('Thu, 19 Nov 1981 08:52:00 GMT'));
205
        */
206
207
        return $response;
208
    }
209
}
210