web-base/Core/API/TfaAPI.class.php

411 lines
14 KiB
PHP
Raw Permalink Normal View History

2022-02-20 16:53:26 +01:00
<?php
2022-11-18 18:06:46 +01:00
namespace Core\API {
2022-02-20 16:53:26 +01:00
2022-11-18 18:06:46 +01:00
use Core\Objects\Context;
use Core\Objects\TwoFactor\AuthenticationData;
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
2022-02-20 16:53:26 +01:00
abstract class TfaAPI extends Request {
2022-06-20 19:52:31 +02:00
private bool $userVerificationRequired;
2022-02-20 16:53:26 +01:00
2022-06-20 19:52:31 +02:00
public function __construct(Context $context, bool $externalCall = false, array $params = array()) {
parent::__construct($context, $externalCall, $params);
2022-02-20 16:53:26 +01:00
$this->loginRequired = true;
2022-06-20 19:52:31 +02:00
$this->apiKeyAllowed = false;
$this->userVerificationRequired = false;
2022-02-20 16:53:26 +01:00
}
protected function verifyAuthData(AuthenticationData $authData): bool {
$domain = getCurrentHostName();
2022-02-20 16:53:26 +01:00
if (!$authData->verifyIntegrity($domain)) {
return $this->createError("mismatched rpIDHash. expected: " . hash("sha256", $domain) . " got: " . bin2hex($authData->getHash()));
} else if (!$authData->isUserPresent()) {
return $this->createError("No user present");
2022-06-20 19:52:31 +02:00
} else if ($this->userVerificationRequired && !$authData->isUserVerified()) {
2022-02-20 16:53:26 +01:00
return $this->createError("user was not verified on device (PIN/Biometric/...)");
} else if ($authData->hasExtensionData()) {
return $this->createError("No extensions supported");
}
return true;
}
2023-01-16 21:47:23 +01:00
protected function verifyClientDataJSON(array $jsonData, KeyBasedTwoFactorToken $token): bool {
2022-06-20 19:52:31 +02:00
$settings = $this->context->getSettings();
2022-02-20 16:53:26 +01:00
$expectedType = $token->isConfirmed() ? "webauthn.get" : "webauthn.create";
$type = $jsonData["type"] ?? "null";
if ($type !== $expectedType) {
return $this->createError("Invalid client data json type. Expected: '$expectedType', Got: '$type'");
2023-01-16 21:47:23 +01:00
} else if (base64url_decode($token->getChallenge()) !== base64url_decode($jsonData["challenge"] ?? "")) {
2022-02-20 16:53:26 +01:00
return $this->createError("Challenge does not match");
2023-01-16 21:47:23 +01:00
}
$origin = $jsonData["origin"] ?? null;
if ($origin !== $settings->getBaseURL()) {
2022-02-20 16:53:26 +01:00
$baseUrl = $settings->getBaseURL();
2023-01-16 21:47:23 +01:00
// return $this->createError("Origin does not match. Expected: '$baseUrl', Got: '$origin'");
2022-02-20 16:53:26 +01:00
}
return true;
}
}
}
2022-11-18 18:06:46 +01:00
namespace Core\API\TFA {
2022-02-20 16:53:26 +01:00
2022-11-18 18:06:46 +01:00
use Core\API\Parameter\StringType;
use Core\API\TfaAPI;
use Core\Objects\Context;
2024-04-23 20:14:32 +02:00
use Core\Objects\RateLimiting;
use Core\Objects\RateLimitRule;
2022-11-18 18:06:46 +01:00
use Core\Objects\TwoFactor\AttestationObject;
use Core\Objects\TwoFactor\AuthenticationData;
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
use Core\Objects\TwoFactor\TimeBasedTwoFactorToken;
2022-02-20 16:53:26 +01:00
// General
class Remove extends TfaAPI {
2022-06-20 19:52:31 +02:00
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
2022-02-20 16:53:26 +01:00
"password" => new StringType("password", 0, true)
]);
}
2022-02-21 13:01:03 +01:00
public function _execute(): bool {
2022-02-20 16:53:26 +01:00
2022-06-20 19:52:31 +02:00
$currentUser = $this->context->getUser();
$token = $currentUser->getTwoFactorToken();
2022-02-20 16:53:26 +01:00
if (!$token) {
return $this->createError("You do not have an active 2FA-Token");
}
2022-06-20 19:52:31 +02:00
$sql = $this->context->getSQL();
2022-02-20 16:53:26 +01:00
$password = $this->getParam("password");
if ($password) {
2022-11-27 15:58:44 +01:00
if (!password_verify($password, $currentUser->password)) {
2022-02-20 16:53:26 +01:00
return $this->createError("Wrong password");
}
} else if ($token->isConfirmed()) {
// if the token is fully confirmed, require a password to remove it
return $this->createError("Missing parameter: password");
}
2022-11-27 15:58:44 +01:00
$this->success = $token->delete($sql) !== false;
2022-02-20 16:53:26 +01:00
$this->lastError = $sql->getLastError();
if ($this->success && $token->isConfirmed()) {
// send an email
2023-01-16 21:47:23 +01:00
$email = $currentUser->getEmail();
if ($email) {
$settings = $this->context->getSettings();
$req = new \Core\API\Template\Render($this->context);
2022-02-20 16:53:26 +01:00
$this->success = $req->execute([
2023-01-16 21:47:23 +01:00
"file" => "mail/2fa_remove.twig",
"parameters" => [
"username" => $currentUser->getFullName() ?? $currentUser->getUsername(),
"site_name" => $settings->getSiteName(),
"sender_mail" => $settings->getMailSender()
]
2022-02-20 16:53:26 +01:00
]);
2023-01-16 21:47:23 +01:00
if ($this->success) {
$body = $req->getResult()["html"];
$gpg = $currentUser->getGPG();
$siteName = $settings->getSiteName();
$req = new \Core\API\Mail\Send($this->context);
$this->success = $req->execute([
"to" => $email,
"subject" => "[$siteName] 2FA-Authentication removed",
"body" => $body,
"gpgFingerprint" => $gpg?->getFingerprint()
]);
}
$this->lastError = $req->getLastError();
}
2022-02-20 16:53:26 +01:00
}
return $this->success;
}
2024-04-07 14:23:59 +02:00
2024-04-23 12:14:28 +02:00
public static function getDescription(): string {
return "Allows users to remove their 2FA-Tokens";
2024-04-07 14:23:59 +02:00
}
2022-02-20 16:53:26 +01:00
}
// TOTP
class GenerateQR extends TfaAPI {
2022-06-20 19:52:31 +02:00
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall);
2022-02-20 16:53:26 +01:00
$this->csrfTokenRequired = false;
}
2022-02-21 13:01:03 +01:00
public function _execute(): bool {
2022-02-20 16:53:26 +01:00
2022-06-20 19:52:31 +02:00
$currentUser = $this->context->getUser();
$twoFactorToken = $currentUser->getTwoFactorToken();
2022-02-20 16:53:26 +01:00
if ($twoFactorToken && $twoFactorToken->isConfirmed()) {
return $this->createError("You already added a two factor token");
} else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
2022-06-20 19:52:31 +02:00
$sql = $this->context->getSQL();
2022-11-29 14:17:11 +01:00
$twoFactorToken = new TimeBasedTwoFactorToken(generateRandomString(32, "base32"));
2022-11-27 15:58:44 +01:00
$this->success = $twoFactorToken->save($sql) !== false;
2022-02-20 16:53:26 +01:00
$this->lastError = $sql->getLastError();
if ($this->success) {
2022-11-27 15:58:44 +01:00
$currentUser->setTwoFactorToken($twoFactorToken);
2023-01-09 14:21:11 +01:00
$this->success = $currentUser->save($sql, ["twoFactorToken"]);
2022-02-20 16:53:26 +01:00
$this->lastError = $sql->getLastError();
}
if (!$this->success) {
return false;
}
}
header("Content-Type: image/png");
$this->disableCache();
2022-06-20 19:52:31 +02:00
die($twoFactorToken->generateQRCode($this->context));
2022-02-20 16:53:26 +01:00
}
2024-04-07 14:23:59 +02:00
2024-04-23 12:14:28 +02:00
public static function getDescription(): string {
return "Allows users generate a QR-code to add a time-based 2FA-Token";
2024-04-07 14:23:59 +02:00
}
2022-02-20 16:53:26 +01:00
}
class ConfirmTotp extends VerifyTotp {
2022-06-20 19:52:31 +02:00
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall);
2024-04-07 14:23:59 +02:00
$this->loginRequired = true;
2022-02-20 16:53:26 +01:00
}
2022-02-21 13:01:03 +01:00
public function _execute(): bool {
2022-02-20 16:53:26 +01:00
2022-06-20 19:52:31 +02:00
$currentUser = $this->context->getUser();
$twoFactorToken = $currentUser->getTwoFactorToken();
2022-02-20 16:53:26 +01:00
if ($twoFactorToken->isConfirmed()) {
return $this->createError("Your two factor token is already confirmed.");
}
2022-11-27 15:58:44 +01:00
if (!parent::_execute()) {
return false;
}
2022-06-20 19:52:31 +02:00
$sql = $this->context->getSQL();
2022-11-27 15:58:44 +01:00
$this->success = $twoFactorToken->confirm($sql) !== false;
2022-02-20 16:53:26 +01:00
$this->lastError = $sql->getLastError();
if ($this->success) {
$this->context->invalidateSessions(true);
}
2022-02-20 16:53:26 +01:00
return $this->success;
}
2024-04-07 14:23:59 +02:00
2024-04-23 12:14:28 +02:00
public static function getDescription(): string {
return "Allows users to confirm their time-based 2FA-Token";
2024-04-07 14:23:59 +02:00
}
2022-02-20 16:53:26 +01:00
}
class VerifyTotp extends TfaAPI {
2022-06-20 19:52:31 +02:00
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
2022-02-20 16:53:26 +01:00
"code" => new StringType("code", 6)
]);
2022-06-20 19:52:31 +02:00
$this->loginRequired = true;
2022-02-20 16:53:26 +01:00
$this->csrfTokenRequired = false;
2024-04-23 20:14:32 +02:00
$this->rateLimiting = new RateLimiting(
null,
new RateLimitRule(5, 30, RateLimitRule::SECOND)
);
2022-02-20 16:53:26 +01:00
}
2022-02-21 13:01:03 +01:00
public function _execute(): bool {
2022-02-20 16:53:26 +01:00
2022-06-20 19:52:31 +02:00
$currentUser = $this->context->getUser();
$twoFactorToken = $currentUser->getTwoFactorToken();
2022-02-20 16:53:26 +01:00
if (!$twoFactorToken) {
return $this->createError("You did not add a two factor token yet.");
} else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
return $this->createError("Invalid 2FA-token endpoint");
}
$code = $this->getParam("code");
if (!$twoFactorToken->verify($code)) {
return $this->createError("Code does not match");
}
$twoFactorToken->authenticate();
return $this->success;
}
2024-04-07 14:23:59 +02:00
2024-04-23 12:14:28 +02:00
public static function getDescription(): string {
return "Allows users to verify time-based 2FA-Tokens";
2024-04-07 14:23:59 +02:00
}
2022-02-20 16:53:26 +01:00
}
// Key
class RegisterKey extends TfaAPI {
2022-06-20 19:52:31 +02:00
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
2022-02-20 16:53:26 +01:00
"clientDataJSON" => new StringType("clientDataJSON", 0, true, "{}"),
"attestationObject" => new StringType("attestationObject", 0, true, "")
]);
2022-06-20 19:52:31 +02:00
$this->loginRequired = true;
2022-02-20 16:53:26 +01:00
}
2022-02-21 13:01:03 +01:00
public function _execute(): bool {
2022-02-20 16:53:26 +01:00
2022-06-20 19:52:31 +02:00
$currentUser = $this->context->getUser();
2022-02-20 16:53:26 +01:00
$clientDataJSON = json_decode($this->getParam("clientDataJSON"), true);
$attestationObjectRaw = base64_decode($this->getParam("attestationObject"));
2022-06-20 19:52:31 +02:00
$twoFactorToken = $currentUser->getTwoFactorToken();
$settings = $this->context->getSettings();
2022-02-20 16:53:26 +01:00
$relyingParty = $settings->getSiteName();
2022-06-20 19:52:31 +02:00
$sql = $this->context->getSQL();
$domain = getCurrentHostName();
2022-02-20 16:53:26 +01:00
if (!$clientDataJSON || !$attestationObjectRaw) {
2023-01-18 14:37:34 +01:00
$challenge = null;
2022-02-20 16:53:26 +01:00
if ($twoFactorToken) {
2023-01-18 14:37:34 +01:00
if ($twoFactorToken->isConfirmed()) {
2022-02-20 16:53:26 +01:00
return $this->createError("You already added a two factor token");
2023-01-18 14:37:34 +01:00
} else if ($twoFactorToken instanceof KeyBasedTwoFactorToken) {
2023-01-16 21:47:23 +01:00
$challenge = $twoFactorToken->getChallenge();
2023-01-18 14:37:34 +01:00
} else {
$twoFactorToken->delete($sql);
2022-02-20 16:53:26 +01:00
}
2023-01-18 14:37:34 +01:00
}
if ($challenge === null) {
2023-01-16 21:47:23 +01:00
$twoFactorToken = KeyBasedTwoFactorToken::create();
$challenge = $twoFactorToken->getChallenge();
2022-11-27 15:58:44 +01:00
$this->success = ($twoFactorToken->save($sql) !== false);
2022-02-20 16:53:26 +01:00
$this->lastError = $sql->getLastError();
if (!$this->success) {
return false;
}
2022-11-27 15:58:44 +01:00
$currentUser->setTwoFactorToken($twoFactorToken);
2023-01-16 21:47:23 +01:00
$this->success = $currentUser->save($sql, ["twoFactorToken"]) !== false;
2022-02-20 16:53:26 +01:00
$this->lastError = $sql->getLastError();
if (!$this->success) {
return false;
}
}
$this->result["data"] = [
"challenge" => $challenge,
"relyingParty" => [
"name" => $relyingParty,
"id" => $domain
],
];
} else {
if ($twoFactorToken === null) {
return $this->createError("Request a registration first.");
} else if (!($twoFactorToken instanceof KeyBasedTwoFactorToken)) {
return $this->createError("You already got a 2FA token");
}
if (!$this->verifyClientDataJSON($clientDataJSON, $twoFactorToken)) {
return false;
}
$attestationObject = new AttestationObject($attestationObjectRaw);
$authData = $attestationObject->getAuthData();
if (!$this->verifyAuthData($authData)) {
return false;
}
$publicKey = $authData->getPublicKey();
if ($publicKey->getUsedAlgorithm() !== -7) {
return $this->createError("Unsupported key type. Expected: -7");
}
2024-04-07 18:29:33 +02:00
$twoFactorToken->authenticate();
2022-11-27 15:58:44 +01:00
$this->success = $twoFactorToken->confirmKeyBased($sql, base64_encode($authData->getCredentialID()), $publicKey) !== false;
2022-02-20 16:53:26 +01:00
$this->lastError = $sql->getLastError();
2023-01-18 14:37:34 +01:00
if ($this->success) {
$this->result["twoFactorToken"] = $twoFactorToken->jsonSerialize();
2024-04-07 18:29:33 +02:00
$this->context->invalidateSessions(true);
2023-01-18 14:37:34 +01:00
}
2022-02-20 16:53:26 +01:00
}
return $this->success;
}
2024-04-07 14:23:59 +02:00
2024-04-23 12:14:28 +02:00
public static function getDescription(): string {
return "Allows users to register a 2FA hardware-key";
2024-04-07 14:23:59 +02:00
}
2022-02-20 16:53:26 +01:00
}
class VerifyKey extends TfaAPI {
2022-06-20 19:52:31 +02:00
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
2022-02-20 16:53:26 +01:00
"credentialID" => new StringType("credentialID"),
"clientDataJSON" => new StringType("clientDataJSON"),
"authData" => new StringType("authData"),
"signature" => new StringType("signature"),
]);
2022-06-20 19:52:31 +02:00
$this->loginRequired = true;
2022-02-20 16:53:26 +01:00
$this->csrfTokenRequired = false;
2024-04-23 20:14:32 +02:00
$this->rateLimiting = new RateLimiting(
null,
new RateLimitRule(20, 60, RateLimitRule::SECOND)
);
2022-02-20 16:53:26 +01:00
}
2022-02-21 13:01:03 +01:00
public function _execute(): bool {
2022-02-20 16:53:26 +01:00
2022-06-20 19:52:31 +02:00
$currentUser = $this->context->getUser();
if (!$currentUser) {
2022-02-20 16:53:26 +01:00
return $this->createError("You are not logged in.");
}
2022-06-20 19:52:31 +02:00
$twoFactorToken = $currentUser->getTwoFactorToken();
2022-02-20 16:53:26 +01:00
if (!$twoFactorToken) {
return $this->createError("You did not add a two factor token yet.");
} else if (!($twoFactorToken instanceof KeyBasedTwoFactorToken)) {
return $this->createError("Invalid 2FA-token endpoint");
} else if (!$twoFactorToken->isConfirmed()) {
return $this->createError("2FA-Key not confirmed yet");
}
$credentialID = base64url_decode($this->getParam("credentialID"));
if ($credentialID !== $twoFactorToken->getCredentialId()) {
return $this->createError("credential ID does not match");
}
$jsonData = $this->getParam("clientDataJSON");
if (!$this->verifyClientDataJSON(json_decode($jsonData, true), $twoFactorToken)) {
return false;
}
$authDataRaw = base64_decode($this->getParam("authData"));
$authData = new AuthenticationData($authDataRaw);
if (!$this->verifyAuthData($authData)) {
return false;
}
$clientDataHash = hash("sha256", $jsonData, true);
$signature = base64_decode($this->getParam("signature"));
$this->success = $twoFactorToken->verify($signature, $authDataRaw . $clientDataHash);
if ($this->success) {
$twoFactorToken->authenticate();
} else {
$this->lastError = "Verification failed";
}
return $this->success;
}
2024-04-07 14:23:59 +02:00
2024-04-23 12:14:28 +02:00
public static function getDescription(): string {
return "Allows users to verify a 2FA hardware-key";
2024-04-07 14:23:59 +02:00
}
2022-02-20 16:53:26 +01:00
}
}