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\Application\Logger;
use Icinga\Authentication\Auth; use Icinga\Authentication\Auth;
use Icinga\Authentication\Totp; use Icinga\Authentication\Totp;
use Icinga\Data\Filter\Filter;
use Icinga\Forms\PreferenceForm; use Icinga\Forms\PreferenceForm;
use Icinga\User\Preferences; use Icinga\User\Preferences;
use Icinga\Web\Form; 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.' 'Please enter the verification code from your TOTP application to verify the new secret.'
), ),
'class' => 'autofocus content-centered', 'class' => 'autofocus content-centered',
'style' => 'width: 200px;', 'style' => 'width: 120px;',
'autocomplete' => 'off', 'autocomplete' => 'off',
] ]
); );
$this->addElement( $this->addElement(
'submit', 'submit',
'btn_verify_totp', '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( $this->addDisplayGroup(
['totp_verification_code', 'btn_verify_totp'], ['totp_verification_code', 'btn_verify_totp'],
'verify_buttons', 'verify_buttons',
@ -167,7 +188,10 @@ class TotpForm extends PreferenceForm
'FormElements', 'FormElements',
[ [
'HtmlTag', 'HtmlTag',
['tag' => 'div', 'class' => 'control-group form-controls'] [
'tag' => 'div',
'class' => 'control-group form-controls'
]
] ]
] ]
] ]

View File

@ -2,6 +2,10 @@
namespace Icinga\Authentication; 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\Clock\PsrClock;
use Icinga\Common\Database; use Icinga\Common\Database;
use Icinga\Exception\ConfigurationError; use Icinga\Exception\ConfigurationError;
@ -47,7 +51,6 @@ class Totp
*/ */
const COLUMN_MODIFIED_TIME = 'mtime'; const COLUMN_MODIFIED_TIME = 'mtime';
/** /**
* State indicating that a secret check is required * State indicating that a secret check is required
*/ */
@ -61,6 +64,14 @@ class Totp
*/ */
const STATE_APPROVED_TEMPORARY_SECRET = 'approve_temporary_secret'; 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 * The username for which the TOTP is configured
* *
@ -355,6 +366,44 @@ class Totp
return $this->totpObject->now(); 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. * Returns the TOTP secret for the current user.
* This method retrieves the secret used for generating TOTP codes. * This method retrieves the secret used for generating TOTP codes.
@ -464,20 +513,40 @@ class Totp
return $this; return $this;
} }
$this->temporaryTotpObject = $this->temporarySecret !== null $this->temporaryTotpObject = $this->temporarySecret !== null
? extTOTP::createFromSecret($this->temporarySecret, $this->clock) ? $this->createTotpObject($this->temporarySecret)
: null; : null;
$this->totpObject = $this->secret !== null $this->totpObject = $this->secret !== null
? extTOTP::createFromSecret($this->secret, $this->clock) ? $this->createTotpObject($this->secret)
: null; : null;
} else { } else {
$this->temporaryTotpObject = extTOTP::generate($this->clock); $this->temporaryTotpObject = $this->createTotpObject();
$this->temporarySecret = $this->temporaryTotpObject->getSecret(); $this->temporarySecret = $this->temporaryTotpObject->getSecret();
} }
return $this; 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. * Makes the temporary TOTP object permanent.
* This method updates the main TOTP object and clears the temporary state. * This method updates the main TOTP object and clears the temporary state.