Passed
Pull Request — master (#39)
by Tim
03:06
created

RegProcess::setAuthState()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
nc 1
nop 1
cc 1
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\Locale\Translate;
12
use SimpleSAML\Logger;
13
use SimpleSAML\Module;
14
use SimpleSAML\Module\webauthn\WebAuthn\AAGUID;
15
use SimpleSAML\Module\webauthn\WebAuthn\WebAuthnRegistrationEvent;
16
use SimpleSAML\Session;
17
use SimpleSAML\Utils;
18
use SimpleSAML\XHTML\Template;
19
use Symfony\Component\HttpFoundation\Request;
20
use Symfony\Component\HttpFoundation\RedirectResponse;
21
use Symfony\Component\HttpFoundation\Response;
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 RegProcess
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
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->query->get('StateId');
102
        if ($stateId === null) {
103
            throw new Error\BadRequest('Missing required StateId query parameter.');
104
        }
105
106
        $moduleConfig = Configuration::getOptionalConfig('module_webauthn.php');
107
        $debugEnabled = $moduleConfig->getBoolean('debug', false);
108
109
        /** @var array $state */
110
        $state = $this->authState::loadState($stateId, 'webauthn:request');
111
112
        // registering a credential is only allowed for new users or after being authenticated
113
        if (count($state['FIDO2Tokens']) > 0 && $state['FIDO2AuthSuccessful'] === false) {
114
            throw new Exception("Attempt to register new token in unacceptable context.");
115
        }
116
117
        $fido2Scope = ($state['FIDO2Scope'] === null ? $state['FIDO2DerivedScope'] : $state['FIDO2Scope']);
118
        if ($fido2Scope === null) {
119
            throw new Exception("FIDO2Scope cannot be null!");
120
        }
121
122
        $regObject = new WebAuthnRegistrationEvent(
123
            $request->request->get('type'),
124
            $fido2Scope,
125
            $state['FIDO2SignupChallenge'],
126
            $state['IdPMetadata']['entityid'],
127
            base64_decode($request->request->get('attestation_object')),
128
            $request->request->get('response_id'),
129
            $request->request->get('attestation_client_data_json'),
130
            $debugEnabled
131
        );
132
133
        // at this point, we need to talk to the DB
134
        /**
135
         * STEP 19 of the validation procedure in § 7.1 of the spec: see if this credential is already registered
136
         */
137
        $store = $state['webauthn:store'];
138
        if ($store->doesCredentialExist(bin2hex($regObject->getCredentialId())) === false) {
139
            // credential does not exist yet in database, good.
140
        } else {
141
            throw new Exception("The credential with ID " . $regObject->getCredentialId() . " already exists.");
142
        }
143
144
        // THAT'S IT. This is a valid credential and can be enrolled to the user.
145
        $friendlyName = $request->request->get('tokenname');
146
147
        // if we have requested the token model, add it to the name
148
        if ($state['requestTokenModel']) {
149
            $model = Translate::noop('unknown model');
150
            $vendor = Translate::noop('unknown vendor');
151
            $aaguiddict = AAGUID::getInstance();
152
            if ($aaguiddict->hasToken($regObject->getAAGUID())) {
153
                $token = $aaguiddict->get($regObject->getAAGUID());
154
                $model = $token['model'];
155
                $vendor = $token['O'];
156
            }
157
            $friendlyName .= " ($model [$vendor])";
158
        }
159
160
        /**
161
         * STEP 20 of the validation procedure in § 7.1 of the spec: store credentialId, credential,
162
         * signCount and associate with user
163
         */
164
165
        /*
166
         * Observed with YubiKey 5: the transaction counter is 0 if the key has NEVER been used, but
167
         * the first transaction is also transaction #0.
168
         * i.e. 0 means "before first transaction OR the very first transaction has already taken place"
169
         *
170
         * The very first use of a key should not trigger the physical object cloning alert, so a
171
         * transaction counter == 0 should be allowed for the first authentication of a new key.
172
         * The best way to do this is to set the current counter value to -1 when registering a key
173
         * with a transaction counter of 0.
174
         */
175
        $currentCounterValue = -1;
176
        if ($regObject->getCounter() > 0) {
177
            $currentCounterValue = $regObject->getCounter();
178
        }
179
180
        $store->storeTokenData(
181
            $state['FIDO2Username'],
182
            $regObject->getCredentialId(),
183
            $regObject->getCredential(),
184
            $currentCounterValue,
185
            $friendlyName
186
        );
187
188
        // make sure $state gets the news, the token is to be displayed to the user on the next page
189
        $state['FIDO2Tokens'][] = [
190
            0 => $regObject->getCredentialId(),
191
            1 => $regObject->getCredential(),
192
            2 => $currentCounterValue,
193
            3 => $friendlyName
194
        ];
195
196
        $id = $this->authState::saveState($state, 'webauthn:request');
197
        if ($debugEnabled === true) {
198
            $response = new RunnableResponse(
199
                function ($regObject, $id) {
200
                    echo $regObject->getDebugBuffer();
201
                    echo $regObject->getValidateBuffer();
202
                    echo "<form id='regform' method='POST' action='" .
203
                        Module::getModuleURL('webauthn/webauthn.php?StateId=' . urlencode($id)) . "'>";
204
                    echo "<button type='submit'>Return to previous page.</button>";
205
                },
206
                [$regObject, $id]
207
            );
208
        } elseif (array_key_exists('Registration', $state)) {
209
            $response = new RedirectResponse(Module::getModuleURL('webauthn/webauthn.php?StateId=' . urlencode($id)));
210
        } else {
211
            $response = new RunnableResponse([Auth\ProcessingChain::class, 'resumeProcessing'], [$state]);
212
        }
213
214
        $response->headers->set('Expires', 'Thu, 19 Nov 1981 08:52:00 GMT');
215
        $response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
216
        $response->headers->set('Pragma', 'no-cache');
217
218
        /** Symfony 5 style */
219
        /**
220
        $response->setCache([
221
            'must_revalidate'  => true,
222
            'no_cache'         => true,
223
            'no_store'         => true,
224
            'no_transform'     => false,
225
            'public'           => false,
226
            'private'          => false,
227
        ]);
228
        $response->setExpires(new DateTime('Thu, 19 Nov 1981 08:52:00 GMT'));
229
        */
230
231
        return $response;
232
    }
233
}
234