Passed
Push — master ( df7a06...000e40 )
by Tim
03:26
created

WebAuthn::workflowStateMachine()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 26
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
dl 0
loc 26
rs 8.4444
c 0
b 0
f 0
eloc 9
nc 5
nop 1
1
<?php
2
3
namespace SimpleSAML\Module\webauthn\Controller;
4
5
use SimpleSAML\Auth;
6
use SimpleSAML\Configuration;
7
use SimpleSAML\Error;
8
use SimpleSAML\Logger;
9
use SimpleSAML\Module;
10
use SimpleSAML\Session;
11
use SimpleSAML\Utils;
12
use SimpleSAML\XHTML\Template;
13
use Symfony\Component\HttpFoundation\Request;
14
use SimpleSAML\Module\webauthn\Store;
15
16
/**
17
 * Controller class for the webauthn module.
18
 *
19
 * This class serves the different views available in the module.
20
 *
21
 * @package SimpleSAML\Module\webauthn
22
 */
23
class WebAuthn
24
{
25
    /** @var \SimpleSAML\Configuration */
26
    protected Configuration $config;
27
28
    /** @var \SimpleSAML\Session */
29
    protected Session $session;
30
31
    /**
32
     * @var \SimpleSAML\Auth\State|string
33
     * @psalm-var \SimpleSAML\Auth\State|class-string
34
     */
35
    protected $authState = Auth\State::class;
36
37
    /**
38
     * @var \SimpleSAML\Logger|string
39
     * @psalm-var \SimpleSAML\Logger|class-string
40
     */
41
    protected $logger = Logger::class;
42
43
44
    /**
45
     * Controller constructor.
46
     *
47
     * It initializes the global configuration and session for the controllers implemented here.
48
     *
49
     * @param \SimpleSAML\Configuration              $config The configuration to use by the controllers.
50
     * @param \SimpleSAML\Session                    $session The session to use by the controllers.
51
     *
52
     * @throws \Exception
53
     */
54
    public function __construct(
55
        Configuration $config,
56
        Session $session
57
    ) {
58
        $this->config = $config;
59
        $this->session = $session;
60
    }
61
62
63
    /**
64
     * Inject the \SimpleSAML\Auth\State dependency.
65
     *
66
     * @param \SimpleSAML\Auth\State $authState
67
     */
68
    public function setAuthState(Auth\State $authState): void
69
    {
70
        $this->authState = $authState;
71
    }
72
73
74
    /**
75
     * Inject the \SimpleSAML\Logger dependency.
76
     *
77
     * @param \SimpleSAML\Logger $logger
78
     */
79
    public function setLogger(Logger $logger): void
80
    {
81
        $this->logger = $logger;
82
    }
83
84
    public const STATE_AUTH_NOMGMT = 1; // just authenticate user
85
    public const STATE_AUTH_ALLOWMGMT = 2; // allow to switch to mgmt page
86
    public const STATE_MGMT = 4; // show token management page
87
88
89
    public static function workflowStateMachine($state) {
90
        // if we don't have any credentials yet, allow user to register
91
        // regardless if in inflow or standalone (redirect to standalone if need
92
        // be)
93
        // OTOH, if we are invoked for passwordless auth, we don't know the
94
        // username nor whether the user has any credentials. The only thing
95
        // we can do is authenticate -> final else
96
        if ($state['FIDO2PasswordlessAuthMode'] != true && (!isset($state['FIDO2Tokens']) || count($state['FIDO2Tokens']) == 0)) {
97
            return self::STATE_MGMT;
98
        }
99
        // from here on we do have a credential to work with
100
        //
101
        // user indicated he wants to manage tokens. He did so either by
102
        // visiting the Registration page, or by checking the box during
103
        // inflow.
104
        // If coming from inflow, allow management only if user is
105
        // properly authenticated, otherwise send to auth page
106
        if ($state['FIDO2WantsRegister']) {
107
            if ($state['FIDO2AuthSuccessful'] || $state['Registration']) {
108
                return self::STATE_MGMT;
109
            }
110
            return self::STATE_AUTH_ALLOWMGMT;
111
        } else { // in inflow, allow to check the management box; otherwise,
112
                 // only auth
113
            $moduleConfig = Configuration::getOptionalConfig('module_webauthn.php')->toArray();
114
            return $moduleConfig['registration']['use_inflow_registration'] ? self::STATE_AUTH_ALLOWMGMT : self::STATE_AUTH_NOMGMT;
115
        }
116
    }
117
118
    public static function loadModuleConfig($moduleConfig, &$stateData): void {
119
        $stateData->store = Store::parseStoreConfig($moduleConfig['store']);
120
121
        // Set the optional scope if set by configuration
122
        if (array_key_exists('scope', $moduleConfig)) {
123
            $stateData->scope = $moduleConfig['scope'];
124
        }
125
126
        // Set the derived scope so we can compare it to the sent host at a later point
127
        $httpUtils = new Utils\HTTP();
128
        $baseurl = $httpUtils->getSelfHost();
129
        $hostname = parse_url($baseurl, PHP_URL_HOST);
130
        if ($hostname !== null) {
131
            $stateData->derivedScope = $hostname;
132
        }
133
134
        if (array_key_exists('attrib_username', $moduleConfig)) {
135
            $stateData->usernameAttrib = $moduleConfig['attrib_username'];
136
        } else {
137
            throw new Error\CriticalConfigurationError('webauthn: it is required to set attrib_username in config.');
138
        }
139
140
        if (array_key_exists('attrib_displayname', $moduleConfig)) {
141
            $stateData->displaynameAttrib = $moduleConfig['attrib_displayname'];
142
        } else {
143
            throw new Error\CriticalConfigurationError('webauthn: it is required to set attrib_displayname in config.');
144
        }
145
146
        if (array_key_exists('request_tokenmodel', $moduleConfig['registration'])) {
147
            $stateData->requestTokenModel = $moduleConfig['registration']['request_tokenmodel'];
148
        } else {
149
            $stateData->requestTokenModel = false;
150
        }
151
    }
152
153
154
    /**
155
     * @param \Symfony\Component\HttpFoundation\Request $request
156
     * @return \SimpleSAML\XHTML\Template  A Symfony Response-object.
157
     */
158
    public function main(Request $request): Template
159
    {
160
        $this->logger::info('FIDO2 - Accessing WebAuthn interface');
161
162
        $stateId = $request->query->get('StateId');
163
        if ($stateId === null) {
164
            throw new Error\BadRequest('Missing required StateId query parameter.');
165
        }
166
167
        $state = $this->authState::loadState($stateId, 'webauthn:request');
168
169
        if ( $this->workflowStateMachine($state) != self::STATE_AUTH_NOMGMT ) {
170
            $templateFile = 'webauthn:webauthn.twig';
171
        } else {
172
            $templateFile = 'webauthn:authentication.twig';
173
        }
174
175
        // Make, populate and layout consent form
176
        $t = new Template($this->config, $templateFile);
177
        $t->data['UserID'] = $state['FIDO2Username'];
178
        $t->data['FIDO2Tokens'] = $state['FIDO2Tokens'];
179
180
        $challenge = str_split($state['FIDO2SignupChallenge'], 2);
181
        $configUtils = new Utils\Config();
182
        $username = str_split(
183
            hash('sha512', $state['FIDO2Username'] . '|' . $configUtils->getSecretSalt()),
184
            2
185
        );
186
187
        $challengeEncoded = [];
188
        foreach ($challenge as $oneChar) {
189
            $challengeEncoded[] = hexdec($oneChar);
190
        }
191
192
        $credentialIdEncoded = [];
193
        foreach ($state['FIDO2Tokens'] as $number => $token) {
194
            $idSplit = str_split($token[0], 2);
195
            $credentialIdEncoded[$number] = [];
196
            foreach ($idSplit as $credIdBlock) {
197
                $credentialIdEncoded[$number][] = hexdec($credIdBlock);
198
            }
199
        }
200
201
        $usernameEncoded = [];
202
        foreach ($username as $oneChar) {
203
            $usernameEncoded[] = hexdec($oneChar);
204
        }
205
206
        $frontendData = [];
207
        $frontendData['challengeEncoded'] = $challengeEncoded;
208
        $frontendData['state'] = [];
209
        foreach (['Source', 'FIDO2Scope','FIDO2Username','FIDO2Displayname','requestTokenModel'] as $stateItem) {
210
            $frontendData['state'][$stateItem] = $state[$stateItem];
211
        }
212
213
        $t->data['showExitButton'] = !array_key_exists('Registration', $state);
214
        $frontendData['usernameEncoded'] = $usernameEncoded;
215
        $frontendData['attestation'] = $state['requestTokenModel'] ? "indirect" : "none";
216
        $frontendData['credentialIdEncoded'] = $credentialIdEncoded;
217
        $t->data['frontendData'] = json_encode($frontendData);
218
219
        $t->data['FIDO2AuthSuccessful'] = $state['FIDO2AuthSuccessful'];
220
        $frontendData['FIDO2PasswordlessAuthMode'] = $state['FIDO2PasswordlessAuthMode'];
221
        if ( $this->workflowStateMachine($state) == self::STATE_MGMT ) {
222
            $t->data['regURL'] = Module::getModuleURL('webauthn/regprocess?StateId=' . urlencode($stateId));
223
            $t->data['delURL'] = Module::getModuleURL('webauthn/managetoken?StateId=' . urlencode($stateId));
224
225
        }
226
227
        $t->data['authForm'] = "";
228
        if (
229
            $this->workflowStateMachine($state) == self::STATE_AUTH_ALLOWMGMT || $this->workflowStateMachine($state) == self::STATE_AUTH_NOMGMT
230
        ) {
231
            $t->data['authURL'] = Module::getModuleURL('webauthn/authprocess?StateId=' . urlencode($stateId));
232
        }
233
234
        // dynamically generate the JS code needed for token registration
235
        return $t;
236
    }
237
}
238