Skip to content

Commit 4c9f238

Browse files
committed
Generate TOTP 2FA QR-Code
1 parent 291439d commit 4c9f238

File tree

6 files changed

+227
-15
lines changed

6 files changed

+227
-15
lines changed

.env.example

+2
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,5 @@ MAILER_MAX_BATCH_SIZE=25
4848
#
4949
MFA_TOTP_DIGITS=6
5050
MFA_TOTP_PERIOD=30
51+
MFA_TOTP_TIME_WINDOW=10
52+
MFA_ISSUER=Web-Toolkit

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"geoip2/geoip2": "~2.0",
3434
"ramsey/uuid": "^4.7",
3535
"phpmailer/phpmailer": "^6.8",
36-
"spomky-labs/otphp": "^11.2"
36+
"spomky-labs/otphp": "^11.2",
37+
"endroid/qr-code": "^4.8"
3738
}
3839
}

composer.lock

+180-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App/Controller/Account/SecurityController.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@ public function load(ServerRequestInterface $request): ResponseInterface
3232
$this->create($account);
3333
}
3434

35+
$otp = $this->securityService->generateTOTPSecret();
36+
3537
return new HtmlResponse(
3638
$this->template->render('account/security',
3739
[
3840
'account' => $this->accountService->findAccountById($account->getId()),
39-
'totpToken' => $this->securityService->generateTOTPSecret(),
41+
'totpToken' => $otp,
42+
'totpQrCode' => $this->securityService->generateTOTPQrCodeBase64($this->securityService->generateTOTPFromSecret($otp)),
4043
'twoFactors' => $this->securityService->getTwoFactorByAccountID($account->getId())
4144
]));
4245
}

src/App/Service/Account/SecurityService.php

+35-10
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@
44

55
use App\Model\Authentication\TwoFactor;
66
use App\Service\Authentication\AccountService;
7+
use App\Software;
78
use App\Table\Account\TwoFactorTable;
9+
use Endroid\QrCode\Builder\Builder;
10+
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
11+
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
12+
use Endroid\QrCode\Writer\SvgWriter;
813
use OTPHP\TOTP;
14+
use ParagonIE\ConstantTime\Base32;
915

10-
class SecurityService
16+
readonly class SecurityService
1117
{
1218

1319
public function __construct(
14-
private readonly AccountService $accountService,
15-
private readonly TwoFactorTable $twoFactorTable
20+
private AccountService $accountService,
21+
private TwoFactorTable $twoFactorTable
1622
)
1723
{
1824
}
@@ -26,9 +32,9 @@ public function add(TwoFactor $twoFactor, int $code): void
2632

2733
$totp = $this->generateTOTPFromSecret($twoFactor->getSecret());
2834

29-
if($totp->verify($code, null, 5) === FALSE)
35+
if($this->verifyTOTPBySecret($twoFactor->getSecret(), (string)$code) === FALSE)
3036
{
31-
MESSAGES->add('danger', 'account-settings-security-two-factor-failed-code-invalid', $totp->now());
37+
MESSAGES->add('danger', 'account-settings-security-two-factor-failed-code-invalid');
3238
return;
3339
}
3440

@@ -62,7 +68,7 @@ public function verifyAccountTwoFactor(int $account, string $code): bool
6268
public function verifyTOTPBySecret(string $secret, string $code): bool
6369
{
6470
$totp = $this->generateTOTPFromSecret($secret);
65-
return $totp->verify($code);
71+
return $totp->verify($code, null, (int)$_ENV['MFA_TOTP_TIME_WINDOW'] ?? 5);
6672
}
6773

6874
public function getTwoFactorByAccountID(int $account): array|false
@@ -72,14 +78,33 @@ public function getTwoFactorByAccountID(int $account): array|false
7278

7379
public function generateTOTPSecret(): string
7480
{
75-
return trim(\ParagonIE\ConstantTime\Base32::encodeUpper(random_bytes($_ENV['MFA_TOTP_GEN_BITS'] ?? 10)), '=');
81+
return trim(Base32::encodeUpper(random_bytes(isset($_ENV['MFA_TOTP_GEN_BITS']) ? (int)$_ENV['MFA_TOTP_GEN_BITS'] : 10)), '=');
7682
}
7783

78-
private function generateTOTPFromSecret(string $secret): TOTP
84+
public function generateTOTPQrCodeBase64(TOTP $totp): string
85+
{
86+
87+
$qrcode = Builder::create()
88+
->writer(new SvgWriter())
89+
->data($totp->getProvisioningUri())
90+
->size(300)
91+
->errorCorrectionLevel(new ErrorCorrectionLevelHigh())
92+
->margin(15)
93+
->labelText('2 Factor Auth')
94+
->roundBlockSizeMode(new RoundBlockSizeModeMargin())
95+
->build();
96+
97+
return $qrcode->getString();
98+
99+
}
100+
101+
public function generateTOTPFromSecret(string $secret, string $label = ''): TOTP
79102
{
80103
$totp = TOTP::createFromSecret($secret);
81-
$totp->setDigits(6);
82-
$totp->setPeriod(30);
104+
$totp->setDigits((int)$_ENV['MFA_TOTP_DIGITS'] ?? 6);
105+
$totp->setPeriod((int)$_ENV['MFA_TOTP_PERIOD'] ?? 30);
106+
$totp->setIssuer((string)$_ENV['MFA_ISSUER'] ?? 'Web-Toolkit');
107+
$totp->setLabel(empty($label) ? $_ENV['SOFTWARE_TITLE'] : $label);
83108
return $totp;
84109
}
85110

template/account/security.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,11 @@
161161
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
162162
</div>
163163
<div class="modal-body">
164+
<div class="mb-3 text-center">
165+
<?= $totpQrCode ?>
166+
</div>
164167
<div class="mb-3">
165-
<label for="addTwoFactorModalTFAToken" class="form-label">2 Faktor Schlüssel</label>
166-
<input type="text" class="form-control disabled" id="addTwoFactorModalTFAToken" name="addTwoFactorModalTFAToken" value="<?= $totpToken ?>">
168+
<input type="hidden" class="form-control" id="addTwoFactorModalTFAToken" name="addTwoFactorModalTFAToken" value="<?= $totpToken ?>">
167169
</div>
168170
<div class="mb-3">
169171
<label for="addTwoFactorModalName" class="form-label">Name</label>

0 commit comments

Comments
 (0)