WebAuthn::loadModuleConfig()   B
last analyzed

Complexity

Conditions 7
Paths 20

Size

Total Lines 45
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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

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