scope && !$this->config->ipTtl()) { /* requested to grant ip access, but that is not enabled */ $payload->scope = Scope::Cookie; } if (!$this->getTotp()->verify($payload->token, null, 1) && !$this->backupCodeManager->verifyAndConsume($payload->token) ) { return null; } /* token is correct (TOTP or Backup) */ /* if server nonce is found and is valid */ $nonceItem = $this->nonceCache->getItem($this->makeCacheKey($payload->nonce)); if (!$nonceItem->isHit() || !$nonceItem->get()) { return null; } /* mark nonce as spent */ $nonceItem->set(false); /* invalid */ $nonceItem->expiresAfter(self::NONCE_TTL); /* keep briefly */ $this->nonceCache->save($nonceItem); /* the code and the nonce are both good from here on */ if ($payload->register && $payload->json) { return $this->startRegistration($payload); } return $this->sessionIssuer->issue( $payload->id, $payload->scope, $request, $payload->json, ); } /** * The registration hand-off: authorisation is already proven, so this issues * the ceremony options back to the page instead of a session. * * `SessionIssuer` is deliberately not involved — the session is granted only * once the new credential has actually been verified, at `register-finish`. * * **JSON only.** The checkbox is submitted through the same `X-Preauth` AJAX * path as an ordinary login, so a failed attempt comes back as JSON carrying * a fresh nonce. On the plain form-post path it would be HTML, the script's * `response.json()` would throw, and — worse — the fresh nonce would be lost, * so the user's retry would fail against a nonce that had already been spent. * A non-JSON submission is therefore treated as an ordinary login; WebAuthn * needs scripting regardless, so it is the checkbox that is the enhancement * here, not the underlying login. */ private function startRegistration(Payload $payload): ?Response { try { $payloadOut = $this->passkeys->beginRegistration($payload->id); } catch (Throwable) { /* a ceremony that cannot start must not become a 500 on the login * page; falling through to the caller's failure path is the same * treatment a wrong code gets */ return null; } $response = new Response( (string) json_encode(['register' => $payloadOut]), Response::HTTP_OK, ['Content-Type' => 'application/json'], ); $response->headers->set(AppConstants::PASSKEY_CEREMONY_MARKER, '1'); return $response; } }