Conditions | 27 |
Paths | 632 |
Total Lines | 212 |
Code Lines | 101 |
Lines | 0 |
Ratio | 0 % |
Changes | 7 | ||
Bugs | 1 | Features | 1 |
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:
If many parameters/temporary variables are present:
1 | <?php |
||
81 | public function main(Request $request): Response |
||
82 | { |
||
83 | $this->logger::info('FIDO2 - Accessing WebAuthn enrollment validation'); |
||
84 | |||
85 | $stateId = $request->query->get('StateId'); |
||
86 | if ($stateId === null) { |
||
87 | throw new Error\BadRequest('Missing required StateId query parameter.'); |
||
88 | } |
||
89 | |||
90 | $moduleConfig = Configuration::getOptionalConfig('module_webauthn.php'); |
||
91 | $debugEnabled = $moduleConfig->getOptionalBoolean('debug', false); |
||
92 | |||
93 | $state = $this->authState::loadState($stateId, 'webauthn:request'); |
||
94 | |||
95 | $incomingID = bin2hex(WebAuthnAbstractEvent::base64urlDecode($request->request->get('response_id'))); |
||
96 | |||
97 | /** |
||
98 | * For passwordless auth, extract the userid from the response of the |
||
99 | * discoverable credential, look up whether the credential used is one |
||
100 | * that belongs to the claimed username |
||
101 | * |
||
102 | * Fail auth if not found, otherwise treat this auth like any other |
||
103 | * (but check later whether UV was set during auth for the token at hand) |
||
104 | */ |
||
105 | if ($state['FIDO2PasswordlessAuthMode'] === true) { |
||
106 | $usernameBuffer = ""; |
||
107 | foreach (str_split(base64_decode($request->request->get('userHandle'))) as $oneChar) { |
||
108 | $usernameBuffer .= bin2hex($oneChar); |
||
109 | } |
||
110 | $store = $state['webauthn:store']; |
||
111 | $userForToken = $store->getUsernameByHashedId($usernameBuffer); |
||
112 | if ($userForToken !== "") { |
||
113 | $tokensForUser = $store->getTokenData($userForToken); |
||
114 | $state['FIDO2Username'] = $userForToken; |
||
115 | $state['FIDO2Tokens'] = $tokensForUser; |
||
116 | } else { |
||
117 | throw new Exception("Credential ID cannot be associated to any user!"); |
||
118 | } |
||
119 | } |
||
120 | |||
121 | /** |
||
122 | * §7.2 STEP 2 - 4 : check that the credential is one of those the particular user owns |
||
123 | */ |
||
124 | $publicKey = false; |
||
125 | $previousCounter = -1; |
||
126 | $oneToken = []; |
||
127 | |||
128 | foreach ($state['FIDO2Tokens'] as $oneToken) { |
||
129 | if ($oneToken[0] == $incomingID) { |
||
130 | // Credential ID is eligible for user $state['FIDO2Username']; |
||
131 | // using publicKey $oneToken[1] with current counter value $oneToken[2] |
||
132 | $publicKey = $oneToken[1]; |
||
133 | $previousCounter = $oneToken[2]; |
||
134 | break; |
||
135 | } |
||
136 | } |
||
137 | |||
138 | if ($publicKey === false || sizeof($oneToken) === 0) { |
||
139 | throw new Exception( |
||
140 | // phpcs:ignore Generic.Files.LineLength.TooLong |
||
141 | "User attempted to authenticate with an unknown credential ID. This should already have been prevented by the browser!", |
||
142 | ); |
||
143 | } |
||
144 | |||
145 | if (!is_string($oneToken[1])) { |
||
146 | $oneToken[1] = stream_get_contents($oneToken[1]); |
||
147 | } |
||
148 | |||
149 | $authObject = new WebAuthnAuthenticationEvent( |
||
150 | $request->request->get('type'), |
||
151 | ($state['FIDO2Scope'] === null ? $state['FIDO2DerivedScope'] : $state['FIDO2Scope']), |
||
152 | $state['FIDO2SignupChallenge'], |
||
153 | base64_decode($request->request->get('authenticator_data')), |
||
154 | base64_decode($request->request->get('client_data_raw')), |
||
155 | $oneToken[0], |
||
156 | $oneToken[1], |
||
157 | (int)$oneToken[4], // algo |
||
158 | base64_decode($request->request->get('signature')), |
||
159 | $debugEnabled, |
||
160 | ); |
||
161 | |||
162 | /** Custom check: if the token was initially registered with UV, but now |
||
163 | * authenticates only UP, we don't allow this downgrade. |
||
164 | * |
||
165 | * This is not typically allowed by authenticator implementations anyway |
||
166 | * (they typically require a full reset of the key to remove UV |
||
167 | * protections) but to be safe: find out and tell user to re-enroll with |
||
168 | * the lower security level. (level upgrades are of course OK.) |
||
169 | */ |
||
170 | if ($oneToken[5] > $authObject->getPresenceLevel()) { |
||
171 | // phpcs:ignore Generic.Files.LineLength.TooLong |
||
172 | throw new Exception("Token was initially registered with higher identification guarantees than now authenticated with (was: " . $oneToken[5] . " now " . $authObject->getPresenceLevel() . "!"); |
||
173 | } |
||
174 | |||
175 | // no matter what: if we are passwordless it MUST be presence-verified |
||
176 | if ( |
||
177 | $state['FIDO2PasswordlessAuthMode'] === true && |
||
178 | $oneToken[5] !== WebAuthnAbstractEvent::PRESENCE_LEVEL_VERIFIED |
||
179 | ) { |
||
180 | throw new Exception("Attempt to authenticate without User Verification in passwordless mode!"); |
||
181 | } |
||
182 | |||
183 | // if we didn't register the key as resident, do not allow its use in |
||
184 | // passwordless mode |
||
185 | if ($state['FIDO2PasswordlessAuthMode'] === true && $oneToken[6] !== 1) { |
||
186 | throw new Exception("Attempt to authenticate with a token that is not registered for passwordless mode!"); |
||
187 | } |
||
188 | |||
189 | /** |
||
190 | * §7.2 STEP 18 : detect physical object cloning on the token |
||
191 | */ |
||
192 | $counter = $authObject->getCounter(); |
||
193 | if ($previousCounter === 0 && $counter === 0) { |
||
194 | // no cloning check, it is a brand new token |
||
195 | } elseif ($counter > $previousCounter) { |
||
196 | // Signature counter was incremented compared to last time, good |
||
197 | $store = $state['webauthn:store']; |
||
198 | $store->updateSignCount($oneToken[0], $counter); |
||
199 | } else { |
||
200 | throw new Exception( |
||
201 | // phpcs:ignore Generic.Files.LineLength.TooLong |
||
202 | "Signature counter less or equal to a previous authentication! Token cloning likely (old: $previousCounter, new: $counter).", |
||
203 | ); |
||
204 | } |
||
205 | |||
206 | // THAT'S IT. The user authenticated successfully. Remember the credential ID that was used. |
||
207 | $state['FIDO2AuthSuccessful'] = $oneToken[0]; |
||
208 | |||
209 | // See if he wants to hang around for token management operations |
||
210 | if ($request->request->get('credentialChange') === 'on') { |
||
211 | $state['FIDO2WantsRegister'] = true; |
||
212 | } else { |
||
213 | $state['FIDO2WantsRegister'] = false; |
||
214 | } |
||
215 | |||
216 | $this->authState::saveState($state, 'webauthn:request'); |
||
217 | |||
218 | if ($debugEnabled) { |
||
219 | $response = new RunnableResponse( |
||
220 | function (WebAuthnAuthenticationEvent $authObject, array $state) { |
||
221 | echo $authObject->getDebugBuffer(); |
||
222 | echo $authObject->getValidateBuffer(); |
||
223 | echo "Debug mode, not continuing to " . ($state['FIDO2WantsRegister'] ? |
||
224 | "credential registration page." : "destination."); |
||
225 | }, |
||
226 | [$authObject, $state], |
||
227 | ); |
||
228 | } else { |
||
229 | if ($state['FIDO2WantsRegister']) { |
||
230 | $response = new RedirectResponse( |
||
231 | Module::getModuleURL('webauthn/webauthn?StateId=' . urlencode($stateId)), |
||
232 | ); |
||
233 | } else { |
||
234 | $response = new RunnableResponse([Auth\ProcessingChain::class, 'resumeProcessing'], [$state]); |
||
235 | } |
||
236 | } |
||
237 | if ($state['FIDO2PasswordlessAuthMode'] === false) { |
||
238 | // take note of the current timestamp so we know |
||
239 | // a) that second-factor was done successfully in the current sesssion |
||
240 | // b) when that event occured, so as to make regular re-auths configurable |
||
241 | $this->session->setData("DateTime", 'LastSuccessfulSecondFactor', new \DateTime()); |
||
242 | $this->authState::saveState($state, 'webauthn:request'); |
||
243 | } |
||
244 | if ($state['FIDO2PasswordlessAuthMode'] === true) { |
||
245 | /** |
||
246 | * But what about SAML attributes? As an authproc, those came in by the |
||
247 | * first-factor authentication. |
||
248 | * In passwordless, we're on our own. The one thing we know is the |
||
249 | * username. |
||
250 | */ |
||
251 | $state['Attributes'][$state['FIDO2AttributeStoringUsername']] = [ $state['FIDO2Username'] ]; |
||
252 | // in case this authentication happened in the Supercharged context |
||
253 | // it may be that there is an authprocfilter for WebAuthN, too. |
||
254 | |||
255 | // If so, remove it from $state as it is stupid to touch the token |
||
256 | // twice; once in the Passwordless auth source and once as an |
||
257 | // authprocfilter |
||
258 | |||
259 | // this didn't actually work; authprocfilter self-removes instead |
||
260 | // if it found Passwordless to be successful in the same session |
||
261 | |||
262 | foreach ($state['IdPMetadata']['authproc'] as $index => $content) { |
||
263 | if ($content['class'] == "webauthn:WebAuthn") { |
||
264 | unset($state['IdPMetadata']['authproc'][$index]); |
||
265 | } |
||
266 | } |
||
267 | // set an internal "authenticated passwordless" hint somewhere else |
||
268 | // in $state, which the authproc can react upon |
||
269 | $state['Attributes']['internal:FIDO2PasswordlessAuthentication'] = [ $state['FIDO2Username'] ]; |
||
270 | |||
271 | $this->authState::saveState($state, 'webauthn:request'); |
||
272 | |||
273 | // set a cookie to remember that the user has successfully used |
||
274 | // Passwordless - on the Supercharged AuthSource, this can be used |
||
275 | // to auto-trigger the FIDO2 authentication step next time |
||
276 | setcookie("SuccessfullyUsedPasswordlessBefore", "YES", time() + (3600 * 24 * 90), '/', "", true, true); |
||
277 | |||
278 | // now properly return our final state to the framework |
||
279 | Source::completeAuth($state); |
||
280 | } |
||
281 | |||
282 | $response->setExpires(new DateTime('Thu, 19 Nov 1981 08:52:00 GMT')); |
||
283 | $response->setCache([ |
||
284 | 'must_revalidate' => true, |
||
285 | 'no_cache' => true, |
||
286 | 'no_store' => true, |
||
287 | 'no_transform' => false, |
||
288 | 'public' => false, |
||
289 | 'private' => false, |
||
290 | ]); |
||
291 | |||
292 | return $response; |
||
293 | } |
||
295 |