Add: qr code for new secrets

This commit is contained in:
Jan Schuppik 2025-08-01 20:45:34 +02:00
parent 3b31e63810
commit b6b4315337
2 changed files with 100 additions and 7 deletions

View File

@ -6,7 +6,6 @@ use Exception;
use Icinga\Application\Logger;
use Icinga\Authentication\Auth;
use Icinga\Authentication\Totp;
use Icinga\Data\Filter\Filter;
use Icinga\Forms\PreferenceForm;
use Icinga\User\Preferences;
use Icinga\Web\Form;
@ -135,10 +134,12 @@ class TotpForm extends PreferenceForm
'Please enter the verification code from your TOTP application to verify the new secret.'
),
'class' => 'autofocus content-centered',
'style' => 'width: 200px;',
'style' => 'width: 120px;',
'autocomplete' => 'off',
]
);
$this->addElement(
'submit',
'btn_verify_totp',
@ -159,6 +160,26 @@ class TotpForm extends PreferenceForm
]
);
}
$this->addElement(
'hidden',
'qr_code_image',
[
'required' => false,
'ignore' => false,
'autoInsertNotEmptyValidator' => false,
'decorators' => [
[
'HtmlTag', [
'tag' => 'img',
'src' => $this->totp->createQRCode(),
]
]
]
]
);
$this->addDisplayGroup(
['totp_verification_code', 'btn_verify_totp'],
'verify_buttons',
@ -167,7 +188,10 @@ class TotpForm extends PreferenceForm
'FormElements',
[
'HtmlTag',
['tag' => 'div', 'class' => 'control-group form-controls']
[
'tag' => 'div',
'class' => 'control-group form-controls'
]
]
]
]

View File

@ -2,6 +2,10 @@
namespace Icinga\Authentication;
use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Data\QRMatrix;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use Icinga\Clock\PsrClock;
use Icinga\Common\Database;
use Icinga\Exception\ConfigurationError;
@ -47,7 +51,6 @@ class Totp
*/
const COLUMN_MODIFIED_TIME = 'mtime';
/**
* State indicating that a secret check is required
*/
@ -61,6 +64,14 @@ class Totp
*/
const STATE_APPROVED_TEMPORARY_SECRET = 'approve_temporary_secret';
/**
* The label for the TOTP application
*
* This label is used when generating the TOTP secret and QR code.
* It helps identify the application in the user's TOTP app.
*/
const TOTP_LABEL = 'IcingaWeb2';
/**
* The username for which the TOTP is configured
*
@ -355,6 +366,44 @@ class Totp
return $this->totpObject->now();
}
/**
* Creates a QR code for the TOTP secret.
* This method generates a QR code that can be scanned by TOTP apps to set up the user's secret.
*
* @return string The rendered QR code as a string
*/
public function createQRCode(): ?string
{
if ($this->temporaryTotpObject === null) {
return null;
}
$urlOTPAUTH = sprintf(
'otpauth://totp/%1$s:%2$s?secret=%3$s&issuer=%1$s',
urlencode(self::TOTP_LABEL),
urlencode($this->username),
urlencode($this->temporarySecret),
);
$options = new QROptions();
$options->scale = 5;
// $options->svgLogo = __DIR__.'/github.svg'; // logo from: https://github.com/simple-icons/simple-icons
// $options->svgLogoScale = 0.25;
// $options->svgLogoCssClass = 'dark';
//
// $options->addLogoSpace = true;
// $options->logoSpaceWidth = 19;
// $options->logoSpaceHeight = 19;
// $options->logoSpaceStartX = 25;
// $options->logoSpaceStartY = 25;
// $options->eccLevel = EccLevel::H;
return (new QRCode($options))->render($urlOTPAUTH);
}
/**
* Returns the TOTP secret for the current user.
* This method retrieves the secret used for generating TOTP codes.
@ -464,20 +513,40 @@ class Totp
return $this;
}
$this->temporaryTotpObject = $this->temporarySecret !== null
? extTOTP::createFromSecret($this->temporarySecret, $this->clock)
? $this->createTotpObject($this->temporarySecret)
: null;
$this->totpObject = $this->secret !== null
? extTOTP::createFromSecret($this->secret, $this->clock)
? $this->createTotpObject($this->secret)
: null;
} else {
$this->temporaryTotpObject = extTOTP::generate($this->clock);
$this->temporaryTotpObject = $this->createTotpObject();
$this->temporarySecret = $this->temporaryTotpObject->getSecret();
}
return $this;
}
/**
* Creates a TOTP object with the given secret.
* If no secret is provided, a new TOTP object is generated.
* This method sets the label and issuer for the TOTP object.
*
* @param string|null $secret The TOTP secret to use, or null to generate a new one
* @return extTOTP The created TOTP object
*/
private function createTotpObject(string $secret = null): extTOTP
{
$totpObject = ($secret === null)
? extTOTP::generate($this->clock)
: extTOTP::createFromSecret($secret, $this->clock);
$totpObject->setLabel(self::TOTP_LABEL);
$totpObject->setIssuer($this->username);
return $totpObject;
}
/**
* Makes the temporary TOTP object permanent.
* This method updates the main TOTP object and clears the temporary state.