Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
"psr/container": "2.*",
"psr/log": "3.0.2 as 1.1.4",
"ramsey/uuid-doctrine": "2.*",
"scheb/2fa-bundle": "^7.11.0",
"scheb/2fa-backup-code": "^7.11.0",
"scheb/2fa-google-authenticator": "^7.11.0",
"scheb/2fa-totp": "^7.11.0",
"stof/doctrine-extensions-bundle": "^1.7",
"symfony/apache-pack": "^1.0",
"symfony/asset": "7.2.*",
Expand Down
1 change: 1 addition & 0 deletions config/bundles.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
BabDev\PagerfantaBundle\BabDevPagerfantaBundle::class => ['all' => true],
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
];
23 changes: 23 additions & 0 deletions config/packages/scheb_2fa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# config/packages/scheb_2fa.yaml
scheb_two_factor:
backup_codes:
enabled: true
totp:
enabled: true # If TOTP authentication should be enabled, default false
server_name: bewelcome.com # Server name used in QR code
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. The server would be bewelcome.org. But I suppose it is better to use a parameter here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for now switched to .org
not sure where variable should be defined.

issuer: BeWelcome # Issuer name used in QR code
leeway: 60 # Acceptable time drift in seconds, must be less or equal than the TOTP period
parameters: # Additional parameters added in the QR code
# image: 'https://my-service/img/logo.png'
image: 'https://miro.medium.com/v2/resize:fill:128:128/1*65AfOY_oNSTe2G1bFMwQ4A.jpeg'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be served through our own server. Could this be done relative to the base url?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to know which file. I got this one while doing image search for a small logo

template: security/2fa_form.html.twig
google:
enabled: true
server_name: bewelcome.com # Server name used in QR code
issuer: BeWelcome # Issuer name used in QR code
leeway: 60 # Acceptable time drift in seconds, must be less or equal than the TOTP period
template: security/2fa_form.html.twig
security_tokens:
# - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
# Symfony 7.2 default enable_authenticator_manager, per profiler _security_skipped_authenticators
- Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
6 changes: 6 additions & 0 deletions config/packages/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ security:

# access_denied_handler: App\Security\AccessDeniedHandler

two_factor:
auth_form_path: 2fa_login
check_path: 2fa_login_check

access_control:
- { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
- { path: ^/$, roles: PUBLIC_ACCESS }
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/about, roles: PUBLIC_ACCESS }
Expand Down Expand Up @@ -98,6 +103,7 @@ security:
- { path: ^/members/avatar/, roles: PUBLIC_ACCESS }
- { path: ^/deleteprofile, roles: PUBLIC_ACCESS }
- { path: ^/password/check, roles: PUBLIC_ACCESS }
- { path: ^/logout, roles: PUBLIC_ACCESS }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/api, roles: PUBLIC_ACCESS }
- { path: ^/, roles: ROLE_USER }
Expand Down
9 changes: 9 additions & 0 deletions config/routes/scheb_2fa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# config/routes/scheb_2fa.yaml
2fa_login:
path: /2fa
# "scheb_two_factor.form_controller" references the controller service provided by the bundle.
# You don't HAVE to use it, but - except you have very special requirements - it is recommended.
controller: "scheb_two_factor.form_controller::form"

2fa_login_check:
path: /2fa_check
91 changes: 90 additions & 1 deletion src/Entity/Member.php
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduced new columns for the member entity but there is no migration. Additionally the two factor auth should be optional which means that there needs to be a preference to enable it and the columns should be in a table of their own (we already have way too many columns on member).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho, that should come after validating things are functional. more database optimisation.

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherAwareInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
use Scheb\TwoFactorBundle\Model\Totp\TotpConfigurationInterface;
use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface;
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface;
use Symfony\Component\Security\Core\User\UserInterface;

/**
* @SuppressWarnings("PHPMD")
Expand All @@ -37,7 +42,8 @@ class Member
\Serializable,
UserInterface,
PasswordHasherAwareInterface,
PasswordAuthenticatedUserInterface
PasswordAuthenticatedUserInterface,
TwoFactorInterface
{
public const ROLE_ADMIN_ACCEPTER = 'ROLE_ADMIN_ACCEPTER';
public const ROLE_ADMIN_ADMIN = 'ROLE_ADMIN_ADMIN';
Expand Down Expand Up @@ -277,6 +283,21 @@ class Member

private ?Language $preferredLanguage = null;

/**
* @ORM\Column(type="json")
*/
private array $backupCodes = [];

/**
* @ORM\Column(type="string", nullable=true)
*/
private ?string $totpSecret;

/**
* @ORM\Column(type="string", nullable=true)
*/
private ?string $googleAuthenticatorSecret;

public function __construct()
{
$this->addresses = new ArrayCollection();
Expand Down Expand Up @@ -1550,4 +1571,72 @@ public function getTranslatedFields(): Collection
{
return $this->translatedFields;
}


/**
* Check if it is a valid backup code.
*/
public function isBackupCode(string $code): bool
{
return in_array($code, $this->backupCodes);
}

/**
* Invalidate a backup code
*/
public function invalidateBackupCode(string $code): void
{
$key = array_search($code, $this->backupCodes);
if ($key !== false){
unset($this->backupCodes[$key]);
}
}

/**
* Add a backup code
*/
public function addBackUpCode(string $backUpCode): void
{
if (!in_array($backUpCode, $this->backupCodes)) {
$this->backupCodes[] = $backUpCode;
}
}

public function isTotpAuthenticationEnabled(): bool
{
return $this->totpSecret ? true : false;
}

public function getTotpAuthenticationUsername(): string
{
return $this->username;
}

public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface
{
// You could persist the other configuration options in the user entity to make it individual per user.
$period = 20;
$digits = 6;
return null !== $this->totpSecret ? new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, $period, $digits) : null;
}

public function isGoogleAuthenticatorEnabled(): bool
{
return null !== $this->googleAuthenticatorSecret;
}

public function getGoogleAuthenticatorUsername(): string
{
return $this->username;
}

public function getGoogleAuthenticatorSecret(): ?string
{
return $this->googleAuthenticatorSecret;
}

public function setGoogleAuthenticatorSecret(?string $googleAuthenticatorSecret): void
{
$this->googleAuthenticatorSecret = $googleAuthenticatorSecret;
}
}
39 changes: 39 additions & 0 deletions templates/security/2fa_form.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{% extends "base.html.twig" %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is lacking translations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similary, after validating things are functional (and not breaking anything) as this initial work and there will certainly be polishing later. But no point to polish something not working.


{% block body %}
<h1>Two-Factor Authentication</h1>

{% if authenticationError %}
<p class="error">{{ authenticationError|trans(authenticationErrorData, 'SchebTwoFactorBundle') }}</p>
{% endif %}

<form class="form" action="{{ checkPathUrl ? checkPathUrl: path(checkPathRoute) }}" method="post">
<p class="widget">
<label for="_auth_code">Enter authentication code for provider <code>{{ twoFactorProvider }}</code>:</label>
<input id="_auth_code" type="text" name="{{ authCodeParameterName }}" autocomplete="one-time-code" autofocus />
<button type="submit" name="submit-button">Send Code</button> or <a href="{{ logoutPath }}">Cancel 2FA</a>
</p>

{% if twoFactorProvider == "email" %}
<p>Hint: The current authentication code is: <code>{{ app.user.emailAuthCode }}</code></p>
{% endif %}

{% if displayTrustedOption %}
<p><label for="_trusted"><input id="_trusted" type="checkbox" name="{{ trustedParameterName }}" /> {{ "trusted"|trans({}, 'SchebTwoFactorBundle') }}</label></p>
{% endif %}

{% if availableTwoFactorProviders|length > 1 %}
<hr/>
<p>Choose authentication method:
{% for provider in availableTwoFactorProviders %}
<a href="{{ path("2fa_login", {"preferProvider": provider}) }}">{{ provider }}</a>
{% if not loop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}

{% if isCsrfProtectionEnabled %}
<input type="hidden" name="{{ csrfParameterName }}" value="{{ csrf_token(csrfTokenId) }}">
{% endif %}
</form>
{% endblock %}