Skip to content

Commit fb90a9a

Browse files
codedmonkeyscheb
authored andcommitted
Add validator for a user's 2fa authentication code (#295)
1 parent 06936f1 commit fb90a9a

File tree

15 files changed

+685
-0
lines changed

15 files changed

+685
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"psr/container": ">=1.1",
3737
"squizlabs/php_codesniffer": "^4.0",
3838
"symfony/mailer": "^6.4 || ^7.0",
39+
"symfony/validator": "^6.4 || ^7.0",
3940
"symfony/yaml": "^6.4 || ^7.0",
4041
"vimeo/psalm": "^6.0"
4142
},

src/bundle/Resources/config/two_factor_provider_google.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
88
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorTwoFactorProvider;
99
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleTotpFactory;
10+
use Scheb\TwoFactorBundle\Security\TwoFactor\Validator\Constraints\UserGoogleTotpCodeValidator;
1011
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
1112
use Symfony\Component\DependencyInjection\Loader\Configurator\ReferenceConfigurator;
1213
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
@@ -44,6 +45,12 @@
4445
service('scheb_two_factor.security.google.form_renderer'),
4546
])
4647

48+
->set('scheb_two_factor.security.totp.validator.user_google_totp_code', UserGoogleTotpCodeValidator::class)
49+
->args([
50+
service('security.token_storage'),
51+
service('scheb_two_factor.security.google_authenticator'),
52+
])
53+
4754
->alias('scheb_two_factor.security.google.form_renderer', 'scheb_two_factor.security.google.default_form_renderer')
4855

4956
->alias(GoogleAuthenticatorInterface::class, 'scheb_two_factor.security.google_authenticator')

src/bundle/Resources/config/two_factor_provider_totp.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
88
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorTwoFactorProvider;
99
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpFactory;
10+
use Scheb\TwoFactorBundle\Security\TwoFactor\Validator\Constraints\UserTotpCodeValidator;
1011
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
1112
use Symfony\Component\DependencyInjection\Loader\Configurator\ReferenceConfigurator;
1213
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
@@ -45,6 +46,12 @@
4546
service('scheb_two_factor.security.totp.form_renderer'),
4647
])
4748

49+
->set('scheb_two_factor.security.totp.validator.user_totp_code', UserTotpCodeValidator::class)
50+
->args([
51+
service('security.token_storage'),
52+
service('scheb_two_factor.security.totp_authenticator'),
53+
])
54+
4855
->alias('scheb_two_factor.security.totp.form_renderer', 'scheb_two_factor.security.totp.default_form_renderer')
4956

5057
->alias(TotpAuthenticatorInterface::class, 'scheb_two_factor.security.totp_authenticator')
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Scheb\TwoFactorBundle\Security\TwoFactor\Validator\Constraints;
6+
7+
use Attribute;
8+
use Symfony\Component\Validator\Constraint;
9+
10+
/**
11+
* Validator constraint for the current user's Google Authenticator TOTP code.
12+
*
13+
* @final
14+
*/
15+
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
16+
class UserGoogleTotpCode extends Constraint
17+
{
18+
public const INVALID_TOTP_CODE_ERROR = '6de3acd0-12f5-40eb-a776-2525c4566649';
19+
20+
protected const ERROR_NAMES = [self::INVALID_TOTP_CODE_ERROR => 'INVALID_TOTP_CODE_ERROR'];
21+
22+
public string $message = 'code_invalid';
23+
public string $translationDomain = 'SchebTwoFactorBundle';
24+
public string $service = 'scheb_two_factor.security.totp.validator.user_google_totp_code';
25+
26+
public function __construct(array|null $options = null, string|null $message = null, string|null $translationDomain = null, string|null $service = null, array|null $groups = null, mixed $payload = null)
27+
{
28+
parent::__construct($options, $groups, $payload);
29+
30+
$this->message = $message ?? $this->message;
31+
$this->translationDomain = $translationDomain ?? $this->translationDomain;
32+
$this->service = $service ?? $this->service;
33+
}
34+
35+
public function validatedBy(): string
36+
{
37+
return $this->service;
38+
}
39+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Scheb\TwoFactorBundle\Security\TwoFactor\Validator\Constraints;
6+
7+
use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface;
8+
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
9+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
10+
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
11+
use Symfony\Component\Validator\Constraint;
12+
use Symfony\Component\Validator\ConstraintValidator;
13+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
14+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
15+
use function get_debug_type;
16+
use function is_string;
17+
use function sprintf;
18+
19+
/**
20+
* Validator for the `UserGoogleTotpCode` constraint.
21+
*
22+
* @final
23+
*
24+
* @psalm-suppress PropertyNotSetInConstructor
25+
*/
26+
class UserGoogleTotpCodeValidator extends ConstraintValidator
27+
{
28+
public function __construct(
29+
private readonly TokenStorageInterface $tokenStorage,
30+
private readonly GoogleAuthenticatorInterface $googleAuthenticator,
31+
) {
32+
}
33+
34+
public function validate(mixed $value, Constraint $constraint): void
35+
{
36+
if (!$constraint instanceof UserGoogleTotpCode) {
37+
throw new UnexpectedTypeException($constraint, UserGoogleTotpCode::class);
38+
}
39+
40+
if (null === $value || '' === $value) {
41+
$this->context->buildViolation($constraint->message)
42+
->setCode(UserGoogleTotpCode::INVALID_TOTP_CODE_ERROR)
43+
->setTranslationDomain($constraint->translationDomain)
44+
->addViolation();
45+
46+
return;
47+
}
48+
49+
if (!is_string($value)) {
50+
throw new UnexpectedTypeException($value, 'string');
51+
}
52+
53+
$token = $this->tokenStorage->getToken();
54+
55+
if (null === $token) {
56+
throw new AuthenticationCredentialsNotFoundException('Could not find Token object for the current user.');
57+
}
58+
59+
$user = $token->getUser();
60+
61+
if (!$user instanceof TwoFactorInterface) {
62+
throw new ConstraintDefinitionException(sprintf('The "%s" class must implement the "%s" interface.', get_debug_type($user), TwoFactorInterface::class));
63+
}
64+
65+
if ($this->googleAuthenticator->checkCode($user, $value)) {
66+
return;
67+
}
68+
69+
$this->context->buildViolation($constraint->message)
70+
->setCode(UserGoogleTotpCode::INVALID_TOTP_CODE_ERROR)
71+
->setTranslationDomain($constraint->translationDomain)
72+
->addViolation();
73+
}
74+
}

src/google-authenticator/composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
"scheb/2fa-bundle": "self.version",
1717
"spomky-labs/otphp": "^11.0"
1818
},
19+
"suggest": {
20+
"symfony/validator": "Needed if you want to use the Google Authenticator TOTP validator constraint"
21+
},
1922
"autoload": {
2023
"psr-4": {
2124
"Scheb\\TwoFactorBundle\\": ""
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Scheb\TwoFactorBundle\Security\TwoFactor\Validator\Constraints;
6+
7+
use Attribute;
8+
use Symfony\Component\Validator\Constraint;
9+
10+
/**
11+
* Validator constraint for the current user's TOTP code.
12+
*
13+
* @final
14+
*/
15+
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
16+
class UserTotpCode extends Constraint
17+
{
18+
public const INVALID_TOTP_CODE_ERROR = 'f79333fd-6eef-4394-ab6b-54827e2865b6';
19+
20+
protected const ERROR_NAMES = [self::INVALID_TOTP_CODE_ERROR => 'INVALID_TOTP_CODE_ERROR'];
21+
22+
public string $message = 'code_invalid';
23+
public string $translationDomain = 'SchebTwoFactorBundle';
24+
public string $service = 'scheb_two_factor.security.totp.validator.user_totp_code';
25+
26+
public function __construct(array|null $options = null, string|null $message = null, string|null $translationDomain = null, string|null $service = null, array|null $groups = null, mixed $payload = null)
27+
{
28+
parent::__construct($options, $groups, $payload);
29+
30+
$this->message = $message ?? $this->message;
31+
$this->translationDomain = $translationDomain ?? $this->translationDomain;
32+
$this->service = $service ?? $this->service;
33+
}
34+
35+
public function validatedBy(): string
36+
{
37+
return $this->service;
38+
}
39+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Scheb\TwoFactorBundle\Security\TwoFactor\Validator\Constraints;
6+
7+
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface;
8+
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
9+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
10+
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
11+
use Symfony\Component\Validator\Constraint;
12+
use Symfony\Component\Validator\ConstraintValidator;
13+
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
14+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
15+
use function get_debug_type;
16+
use function is_string;
17+
use function sprintf;
18+
19+
/**
20+
* Validator for the `UserTotpCode` constraint.
21+
*
22+
* @final
23+
*
24+
* @psalm-suppress PropertyNotSetInConstructor
25+
*/
26+
class UserTotpCodeValidator extends ConstraintValidator
27+
{
28+
public function __construct(
29+
private readonly TokenStorageInterface $tokenStorage,
30+
private readonly TotpAuthenticatorInterface $totpAuthenticator,
31+
) {
32+
}
33+
34+
public function validate(mixed $value, Constraint $constraint): void
35+
{
36+
if (!$constraint instanceof UserTotpCode) {
37+
throw new UnexpectedTypeException($constraint, UserTotpCode::class);
38+
}
39+
40+
if (null === $value || '' === $value) {
41+
$this->context->buildViolation($constraint->message)
42+
->setCode(UserTotpCode::INVALID_TOTP_CODE_ERROR)
43+
->setTranslationDomain($constraint->translationDomain)
44+
->addViolation();
45+
46+
return;
47+
}
48+
49+
if (!is_string($value)) {
50+
throw new UnexpectedTypeException($value, 'string');
51+
}
52+
53+
$token = $this->tokenStorage->getToken();
54+
55+
if (null === $token) {
56+
throw new AuthenticationCredentialsNotFoundException('Could not find Token object for the current user.');
57+
}
58+
59+
$user = $token->getUser();
60+
61+
if (!$user instanceof TwoFactorInterface) {
62+
throw new ConstraintDefinitionException(sprintf('The "%s" class must implement the "%s" interface.', get_debug_type($user), TwoFactorInterface::class));
63+
}
64+
65+
if ($this->totpAuthenticator->checkCode($user, $value)) {
66+
return;
67+
}
68+
69+
$this->context->buildViolation($constraint->message)
70+
->setCode(UserTotpCode::INVALID_TOTP_CODE_ERROR)
71+
->setTranslationDomain($constraint->translationDomain)
72+
->addViolation();
73+
}
74+
}

src/totp/composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
"scheb/2fa-bundle": "self.version",
1717
"spomky-labs/otphp": "^11.0"
1818
},
19+
"suggest": {
20+
"symfony/validator": "Needed if you want to use the TOTP validator constraint"
21+
},
1922
"autoload": {
2023
"psr-4": {
2124
"Scheb\\TwoFactorBundle\\": ""
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Scheb\TwoFactorBundle\Tests\Security\TwoFactor\Validator\Constraints;
6+
7+
use Scheb\TwoFactorBundle\Security\TwoFactor\Validator\Constraints\UserGoogleTotpCode;
8+
9+
class UserGoogleTotpCodeDummy
10+
{
11+
#[UserGoogleTotpCode]
12+
public string $a;
13+
14+
#[UserGoogleTotpCode(message: 'myMessage', translationDomain: 'myDomain', service: 'my_service')]
15+
public string $b;
16+
17+
#[UserGoogleTotpCode(['groups' => ['my_group'], 'payload' => 'some attached data'])]
18+
public string $c;
19+
}

0 commit comments

Comments
 (0)