Passed
Pull Request — master (#31)
by Tim
02:17
created

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