WebAuthn::main()   F
last analyzed

Complexity

Conditions 12
Paths 385

Size

Total Lines 81
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 8
Bugs 1 Features 0
Metric Value
cc 12
eloc 51
c 8
b 1
f 0
nc 385
nop 1
dl 0
loc 81
rs 3.8208

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
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\webauthn\Controller;
6
7
use SimpleSAML\Auth;
8
use SimpleSAML\Configuration;
9
use SimpleSAML\Error;
10
use SimpleSAML\Logger;
11
use SimpleSAML\Module;
12
use SimpleSAML\Module\webauthn\Store;
13
use SimpleSAML\Module\webauthn\WebAuthn\StateData;
14
use SimpleSAML\Session;
15
use SimpleSAML\Utils;
16
use SimpleSAML\XHTML\Template;
17
use Symfony\Component\HttpFoundation\Request;
18
19
/**
20
 * Controller class for the webauthn module.
21
 *
22
 * This class serves the different views available in the module.
23
 *
24
 * @package SimpleSAML\Module\webauthn
25
 */
26
class WebAuthn
27
{
28
    public const STATE_AUTH_NOMGMT = 1; // just authenticate user
29
30
    public const STATE_AUTH_ALLOWMGMT = 2; // allow to switch to mgmt page
31
32
    public const STATE_MGMT = 4; // show token management page
33
34
35
    /** @var \SimpleSAML\Auth\State|string */
36
    protected $authState = Auth\State::class;
37
38
    /** @var \SimpleSAML\Logger|string */
39
    protected $logger = Logger::class;
40
41
42
    /**
43
     * Controller constructor.
44
     *
45
     * It initializes the global configuration and session for the controllers implemented here.
46
     *
47
     * @param \SimpleSAML\Configuration              $config The configuration to use by the controllers.
48
     * @param \SimpleSAML\Session                    $session The session to use by the controllers.
49
     *
50
     * @throws \Exception
51
     */
52
    public function __construct(
53
        protected Configuration $config,
54
        protected Session $session,
55
    ) {
56
    }
57
58
59
    /**
60
     * Inject the \SimpleSAML\Auth\State dependency.
61
     *
62
     * @param \SimpleSAML\Auth\State $authState
63
     */
64
    public function setAuthState(Auth\State $authState): void
65
    {
66
        $this->authState = $authState;
67
    }
68
69
70
    /**
71
     * Inject the \SimpleSAML\Logger dependency.
72
     *
73
     * @param \SimpleSAML\Logger $logger
74
     */
75
    public function setLogger(Logger $logger): void
76
    {
77
        $this->logger = $logger;
78
    }
79
80
81
    public static function workflowStateMachine(array $state)
82
    {
83
        // if we don't have any credentials yet, allow user to register
84
        // regardless if in inflow or standalone (redirect to standalone if need
85
        // be)
86
        // OTOH, if we are invoked for passwordless auth, we don't know the
87
        // username nor whether the user has any credentials. The only thing
88
        // we can do is authenticate -> final else
89
        if (
90
            $state['FIDO2PasswordlessAuthMode'] != true &&
91
            (!isset($state['FIDO2Tokens']) || count($state['FIDO2Tokens']) == 0)
92
        ) {
93
            return self::STATE_MGMT;
94
        }
95
        // from here on we do have a credential to work with
96
        //
97
        // user indicated he wants to manage tokens. He did so either by
98
        // visiting the Registration page, or by checking the box during
99
        // inflow.
100
        // If coming from inflow, allow management only if user is
101
        // properly authenticated, otherwise send to auth page
102
        if ($state['FIDO2WantsRegister']) {
103
            if ($state['FIDO2AuthSuccessful'] || $state['Registration']) {
104
                return self::STATE_MGMT;
105
            }
106
            return self::STATE_AUTH_ALLOWMGMT;
107
        } else { // in inflow, allow to check the management box; otherwise,
108
                 // only auth
109
            $moduleConfig = Configuration::getOptionalConfig('module_webauthn.php')->toArray();
110
            return $moduleConfig['registration']['use_inflow_registration'] ?
111
                self::STATE_AUTH_ALLOWMGMT : self::STATE_AUTH_NOMGMT;
112
        }
113
    }
114
115
116
    public static function loadModuleConfig(array $moduleConfig, StateData &$stateData): void
117
    {
118
        $stateData->store = Store::parseStoreConfig($moduleConfig['store']);
119
120
        // Set the optional scope if set by configuration
121
        if (array_key_exists('scope', $moduleConfig)) {
122
            $stateData->scope = $moduleConfig['scope'];
123
        }
124
125
        // Set the derived scope so we can compare it to the sent host at a later point
126
        $httpUtils = new Utils\HTTP();
127
        $baseurl = $httpUtils->getSelfHost();
128
        $hostname = parse_url($baseurl, PHP_URL_HOST);
129
        if ($hostname !== null) {
130
            $stateData->derivedScope = $hostname;
131
        }
132
133
        if (array_key_exists('identifyingAttribute', $moduleConfig)) {
134
            $stateData->usernameAttrib = $moduleConfig['identifyingAttribute'];
135
        } else {
136
            throw new Error\CriticalConfigurationError(
137
                'webauthn: it is required to set identifyingAttribute in config.',
138
            );
139
        }
140
141
        if (array_key_exists('attrib_displayname', $moduleConfig)) {
142
            $stateData->displaynameAttrib = $moduleConfig['attrib_displayname'];
143
        } else {
144
            throw new Error\CriticalConfigurationError(
145
                'webauthn: it is required to set attrib_displayname in config.',
146
            );
147
        }
148
149
        if (array_key_exists('minimum_certification_level', $moduleConfig['registration']['policy_2fa'])) {
150
            // phpcs:disable Generic.Files.LineLength.TooLong
151
            $stateData->requestTokenModel = ($moduleConfig['registration']['policy_2fa']['minimum_certification_level'] == Module\webauthn\WebAuthn\WebAuthnRegistrationEvent::CERTIFICATION_NOT_REQUIRED ? false : true);
152
            $stateData->minCertLevel2FA = $moduleConfig['registration']['policy_2fa']['minimum_certification_level'];
153
            $stateData->aaguidWhitelist2FA = $moduleConfig['registration']['policy_2fa']['aaguid_whitelist'] ?? [];
154
            $stateData->attFmtWhitelist2FA = $moduleConfig['registration']['policy_2fa']['attestation_format_whitelist'] ?? [];
155
            $stateData->minCertLevelPasswordless = $moduleConfig['registration']['policy_passwordless']['minimum_certification_level'];
156
            $stateData->aaguidWhitelistPasswordless = $moduleConfig['registration']['policy_passwordless']['aaguid_whitelist'] ?? [];
157
            $stateData->attFmtWhitelistPasswordless = $moduleConfig['registration']['policy_passwordless']['attestation_format_whitelist'] ?? [];
158
            // phpcs:enable Generic.Files.LineLength.TooLong
159
        } else {
160
            $stateData->requestTokenModel = false;
161
        }
162
    }
163
164
165
    /**
166
     * @param \Symfony\Component\HttpFoundation\Request $request
167
     * @return \SimpleSAML\XHTML\Template  A Symfony Response-object.
168
     */
169
    public function main(Request $request): Template
170
    {
171
        $this->logger::info('FIDO2 - Accessing WebAuthn interface');
172
173
        $stateId = $request->query->get('StateId');
174
        if ($stateId === null) {
175
            throw new Error\BadRequest('Missing required StateId query parameter.');
176
        }
177
178
        $state = $this->authState::loadState($stateId, 'webauthn:request');
179
180
        if ($this->workflowStateMachine($state) != self::STATE_AUTH_NOMGMT) {
0 ignored issues
show
Bug introduced by
It seems like $state can also be of type null; however, parameter $state of SimpleSAML\Module\webaut...:workflowStateMachine() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

180
        if ($this->workflowStateMachine(/** @scrutinizer ignore-type */ $state) != self::STATE_AUTH_NOMGMT) {
Loading history...
181
            $templateFile = 'webauthn:webauthn.twig';
182
        } else {
183
            $templateFile = 'webauthn:authentication.twig';
184
        }
185
186
        // Make, populate and layout consent form
187
        $t = new Template($this->config, $templateFile);
188
        $t->data['UserID'] = $state['FIDO2Username'];
189
        $t->data['FIDO2Tokens'] = $state['FIDO2Tokens'];
190
        // in case IdPs want to override UI and display SP-specific content
191
        $t->data['entityid'] = $state['SPMetadata']['entityid'] ?? 'WEBAUTHN-SP-NONE';
192
193
        $challenge = str_split($state['FIDO2SignupChallenge'], 2);
194
        $configUtils = new Utils\Config();
195
        $username = str_split(
196
            hash('sha512', $state['FIDO2Username'] . '|' . $configUtils->getSecretSalt()),
197
            2,
198
        );
199
200
        $challengeEncoded = [];
201
        foreach ($challenge as $oneChar) {
202
            $challengeEncoded[] = hexdec($oneChar);
203
        }
204
205
        $credentialIdEncoded = [];
206
        foreach ($state['FIDO2Tokens'] as $number => $token) {
207
            $idSplit = str_split($token[0], 2);
208
            $credentialIdEncoded[$number] = [];
209
            foreach ($idSplit as $credIdBlock) {
210
                $credentialIdEncoded[$number][] = hexdec($credIdBlock);
211
            }
212
        }
213
214
        $usernameEncoded = [];
215
        foreach ($username as $oneChar) {
216
            $usernameEncoded[] = hexdec($oneChar);
217
        }
218
219
        $frontendData = [];
220
        $frontendData['challengeEncoded'] = $challengeEncoded;
221
        $frontendData['state'] = [];
222
        foreach (['FIDO2Scope','FIDO2Username','FIDO2Displayname','requestTokenModel'] as $stateItem) {
223
            $frontendData['state'][$stateItem] = $state[$stateItem];
224
        }
225
226
        $t->data['showExitButton'] = !array_key_exists('Registration', $state);
227
        $frontendData['usernameEncoded'] = $usernameEncoded;
228
        $frontendData['attestation'] = $state['requestTokenModel'] ? "indirect" : "none";
229
        $frontendData['credentialIdEncoded'] = $credentialIdEncoded;
230
        $frontendData['FIDO2PasswordlessAuthMode'] = $state['FIDO2PasswordlessAuthMode'];
231
        $t->data['frontendData'] = json_encode($frontendData);
232
233
        $t->data['FIDO2AuthSuccessful'] = $state['FIDO2AuthSuccessful'];
234
        if ($this->workflowStateMachine($state) == self::STATE_MGMT) {
235
            $t->data['regURL'] = Module::getModuleURL('webauthn/regprocess?StateId=' . urlencode($stateId));
236
            $t->data['delURL'] = Module::getModuleURL('webauthn/managetoken?StateId=' . urlencode($stateId));
237
        }
238
239
        $t->data['authForm'] = "";
240
        if (
241
            $this->workflowStateMachine($state) == self::STATE_AUTH_ALLOWMGMT ||
242
            $this->workflowStateMachine($state) == self::STATE_AUTH_NOMGMT
243
        ) {
244
            $t->data['authURL'] = Module::getModuleURL('webauthn/authprocess?StateId=' . urlencode($stateId));
245
            $t->data['delURL'] = Module::getModuleURL('webauthn/managetoken?StateId=' . urlencode($stateId));
246
        }
247
248
        // dynamically generate the JS code needed for token registration
249
        return $t;
250
    }
251
}
252