diff --git a/cp/app/Controllers/Auth/AuthController.php b/cp/app/Controllers/Auth/AuthController.php index 27f48e8..916d919 100644 --- a/cp/app/Controllers/Auth/AuthController.php +++ b/cp/app/Controllers/Auth/AuthController.php @@ -15,6 +15,32 @@ use Psr\Http\Message\ServerRequestInterface as Request; */ class AuthController extends Controller { + private $webAuthn; + + public function __construct() { + $rpName = 'Namingo'; + $rpId = envi('APP_DOMAIN'); + $formats = [ + 'android-key', + 'android-safetynet', + 'apple', + 'fido-u2f', + 'none', + 'packed', + 'tpm' + ]; + + $this->webAuthn = new \lbuchs\WebAuthn\WebAuthn($rpName, $rpId, $formats); + $this->webAuthn->addRootCertificates(envi('APP_ROOT').'/vendor/lbuchs/webauthn/_test/rootCertificates/solo.pem'); + $this->webAuthn->addRootCertificates(envi('APP_ROOT').'/vendor/lbuchs/webauthn/_test/rootCertificates/apple.pem'); + $this->webAuthn->addRootCertificates(envi('APP_ROOT').'/vendor/lbuchs/webauthn/_test/rootCertificates/yubico.pem'); + $this->webAuthn->addRootCertificates(envi('APP_ROOT').'/vendor/lbuchs/webauthn/_test/rootCertificates/hypersecu.pem'); + $this->webAuthn->addRootCertificates(envi('APP_ROOT').'/vendor/lbuchs/webauthn/_test/rootCertificates/globalSign.pem'); + $this->webAuthn->addRootCertificates(envi('APP_ROOT').'/vendor/lbuchs/webauthn/_test/rootCertificates/googleHardware.pem'); + $this->webAuthn->addRootCertificates(envi('APP_ROOT').'/vendor/lbuchs/webauthn/_test/rootCertificates/microsoftTpmCollection.pem'); + $this->webAuthn->addRootCertificates(envi('APP_ROOT').'/vendor/lbuchs/webauthn/_test/rootCertificates/mds'); + } + /** * @param Request $request * @param Response $response @@ -57,4 +83,102 @@ class AuthController extends Controller Auth::logout(); redirect()->route('login'); } + + public function getLoginChallenge(Request $request, Response $response) + { + global $container; + + $ids = []; + $rawData = $request->getBody(); + $data = json_decode($rawData, true); + + try { + $db = $container->get('db'); + $user = $db->selectValue('SELECT id FROM users WHERE email = ?', [$data['email']]); + + if ($user) { + // User found, get the user ID + $userId = $user; + $registrations = $db->select('SELECT id FROM users_webauthn WHERE user_id = ?', [$user]); + + if ($registrations) { + foreach ($registrations as $reg) { + $ids[] = $reg['credentialId']; + } + } + + if (count($ids) === 0) { + $response->getBody()->write(json_encode(['error' => 'no registrations in session for userId ' . $userId])); + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + } else { + $response->getBody()->write(json_encode(['error' => 'No user found with the provided email.'])); + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + } catch (PDOException $e) { + $response->getBody()->write(json_encode(['error' => $e->getMessage()])); + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + + $getArgs = $this->webAuthn->getGetArgs($ids, 60*4, true, true, true, true, true, 'required'); + + $response->getBody()->write(json_encode($getArgs)); + $challenge = $this->webAuthn->getChallenge(); + $_SESSION['challenge_data'] = $challenge->getBinaryString(); + + return $response->withHeader('Content-Type', 'application/json'); + } + + public function verifyLogin(Request $request, Response $response) + { + global $container; + + $challengeData = $_SESSION['challenge_data']; + $challenge = new \lbuchs\WebAuthn\Binary\ByteBuffer($challengeData); + $credentialPublicKey = null; + + $data = json_decode($request->getBody()->getContents(), null, 512, JSON_THROW_ON_ERROR); + + try { + // Decode the incoming data + $clientDataJSON = base64_decode($data->clientDataJSON); + $authenticatorData = base64_decode($data->authenticatorData); + $signature = base64_decode($data->signature); + $userHandle = base64_decode($data->userHandle); + $id = base64_decode($data->id); + + $db = $container->get('db'); + $credential = $db->select('SELECT public_key FROM users_webauthn WHERE user_id = ?', [$id]); + + if ($credential) { + foreach ($registrations as $reg) { + $credentialPublicKey = $reg['public_key']; + break; + } + } + + if ($credentialPublicKey === null) { + throw new Exception('Public Key for credential ID not found!'); + } + + // if we have resident key, we have to verify that the userHandle is the provided userId at registration + if ($requireResidentKey && $userHandle !== hex2bin($reg->userId)) { + throw new \Exception('userId doesnt match (is ' . bin2hex($userHandle) . ' but expect ' . $reg->userId . ')'); + } + + // process the get request. throws WebAuthnException if it fails + $this->webAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, $userVerification === 'required'); + + $return = new \stdClass(); + $return->success = true; + $return->msg = $msg; + + // Send success response + $response->getBody()->write(json_encode($return)); + return $response->withHeader('Content-Type', 'application/json'); + } catch (\Exception $e) { + $response->getBody()->write(json_encode(['error' => $e->getMessage()])); + return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + } + } } \ No newline at end of file diff --git a/cp/bootstrap/app.php b/cp/bootstrap/app.php index 68ebac6..2f066fd 100644 --- a/cp/bootstrap/app.php +++ b/cp/bootstrap/app.php @@ -201,6 +201,12 @@ $csrfMiddleware = function ($request, $handler) use ($container) { if ($path && $path === '/webauthn/register/verify') { return $handler->handle($request); } + if ($path && $path === '/webauthn/login/challenge') { + return $handler->handle($request); + } + if ($path && $path === '/webauthn/login/verify') { + return $handler->handle($request); + } // If not skipped, apply the CSRF Guard return $csrf->process($request, $handler); diff --git a/cp/resources/views/auth/login.twig b/cp/resources/views/auth/login.twig index 1bf8d2c..ea458e6 100644 --- a/cp/resources/views/auth/login.twig +++ b/cp/resources/views/auth/login.twig @@ -51,16 +51,133 @@