Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2fa login #3580

Open
wants to merge 57 commits into
base: fork6
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
300641b
Initial setup for 2fa
bjorvack Nov 6, 2023
ef93932
Add trusted devices to 2fa login
bjorvack Nov 6, 2023
476ee9f
Load settigns from database
bjorvack Sep 26, 2023
3cd6bb2
Clear cache after changes in 2fa
bjorvack Sep 27, 2023
98d9726
Add backupcodes
bjorvack Nov 24, 2023
01833b9
Show backup codes on 2fa enable
bjorvack Nov 24, 2023
15bbe9d
Set server name and issuer
bjorvack Nov 24, 2023
1ea2aa5
Redirect a user to his edit page when 2fa is required but not set
bjorvack Sep 27, 2023
f01f72e
Fix code styles
bjorvack Sep 27, 2023
3a96ce8
Fix phpstan issues
bjorvack Sep 27, 2023
e850b23
Use SvgWriter to create QR code
bjorvack Sep 28, 2023
82c075a
Use the /private/2fa routes
bjorvack Sep 28, 2023
f70cf25
Add missing empty line
bjorvack Sep 28, 2023
bfa4dc4
Remove 2fa on create user form
bjorvack Sep 28, 2023
cc5baa3
Add default value to setBackupCodes for readability
bjorvack Sep 28, 2023
1beb9d4
Fix QR Code generation
bjorvack Sep 28, 2023
de9fa9b
Refactor AbstractActionController
bjorvack Sep 28, 2023
16916a7
Configure labels and help text correctly
bjorvack Sep 28, 2023
bae2283
Use translation for alt text
bjorvack Sep 28, 2023
7677490
Add copy to clipboard
bjorvack Nov 6, 2023
7fb2d64
Add form validation to authorization key
bjorvack Sep 28, 2023
30e1591
Use correct redirect url generation
bjorvack Sep 28, 2023
5b28317
Refactor 2fa screen
bjorvack Sep 29, 2023
79fa194
Add toggle to authorization key
bjorvack Sep 29, 2023
2180258
Remove leading and trailing spaces on copied lines
bjorvack Sep 29, 2023
a367a4d
Temp commit
bjorvack Sep 29, 2023
b182f94
Refactor OAuth activation flow
bjorvack Oct 27, 2023
84680e6
Get existing instance of modal so it closes
bjorvack Oct 27, 2023
e8fbc3a
Update copy
bjorvack Nov 7, 2023
549b22b
Fix translations and flow issues
bjorvack Nov 7, 2023
72ac03a
Use different translations and use fetch instead of $.ajax
bjorvack Nov 7, 2023
650196e
Fix missed translation
bjorvack Nov 7, 2023
6d25ac0
Don't disable variables-dark
bjorvack Nov 7, 2023
90c5f83
Fix feedback @carakas
bjorvack Nov 7, 2023
39b52b0
Use handleSettingsForm to handle setting form changes
bjorvack Nov 7, 2023
b30e646
Fix phpstan issues
bjorvack Nov 7, 2023
837bd64
Fix standard js issues
bjorvack Nov 7, 2023
334a50f
Fic php code sniffer issues
bjorvack Nov 7, 2023
c6310d1
Add missing translation
bjorvack Nov 9, 2023
0508c82
Use the fork6 version of the toggle password
bjorvack Nov 29, 2023
ec97af9
Use snake case
bjorvack Nov 29, 2023
d533e75
Use public readonly to get user from disable 2 fa handler
bjorvack Nov 29, 2023
3ff9363
Remove unused use statement
bjorvack Nov 29, 2023
f0c5e68
Clear the cache in the change module settings handler
bjorvack Nov 29, 2023
f92c386
Refactor ModuleSettingsType to use the correct form types
bjorvack Nov 29, 2023
1f2271c
Use fallback translation domain for backend_2fa
bjorvack Dec 22, 2023
26235e6
Remove unused use statement
bjorvack Dec 22, 2023
d3e8501
Merge branch 'fork6' into 2fa-login
bjorvack Dec 22, 2023
7ef40cf
Use clear cache event on module settings change
bjorvack Dec 22, 2023
cd1ac62
Move 2fa enabled changes to settings handler
bjorvack Dec 22, 2023
8ad6b65
use /private/2fa route
bjorvack Dec 22, 2023
f9fd125
Autoload EventDispatcherInterface
bjorvack Dec 22, 2023
bfccf9c
Fix phpstan issues
bjorvack Dec 22, 2023
8755369
Remove leading slash from use statement
bjorvack Dec 22, 2023
d84fed8
Fix unit tests
bjorvack Dec 22, 2023
cf32237
Fix unit tests
bjorvack Mar 15, 2024
8b7240e
Test when the 2FA buttons should be visible
bjorvack Mar 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@
"scienta/doctrine-json-functions": "^5.0",
"twig/intl-extra": "^3.3",
"symfony/expression-language": "6.*",
"symfony/lock": "6.*"
"symfony/lock": "6.*",
"scheb/2fa-google-authenticator": "^6.9",
"endroid/qr-code": "^4.8",
"scheb/2fa-bundle": "^6.9",
"scheb/2fa-trusted-device": "^6.9",
"scheb/2fa-backup-code": "^6.9"
},
"require-dev": {
"squizlabs/php_codesniffer": "dev-master as 3.6.0",
Expand Down
2,226 changes: 1,465 additions & 761 deletions composer.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions config/bundles.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@
Pageon\DoctrineDataGridBundle\PageonDoctrineDataGridBundle::class => ['all' => true],
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true, 'test_install' => true],
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
];
19 changes: 19 additions & 0 deletions config/packages/scheb_2fa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# See the configuration reference at https://symfony.com/bundles/SchebTwoFactorBundle/6.x/configuration.html
scheb_two_factor:
security_tokens:
- Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
- Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
google:
enabled: '@=service("ForkCMS\\Modules\\Backend\\Domain\\Authentication\\TwoFactorAuthenticationFactory").enabled()'
template: '@Backend\Backend\2fa.html.twig'
server_name: '%env(SITE_DOMAIN)%'
issuer: '%env(SITE_DEFAULT_TITLE)%'

trusted_device:
enabled: '@=service("ForkCMS\\Modules\\Backend\\Domain\\Authentication\\TwoFactorAuthenticationFactory").trustedDevices()'
extend_lifetime: true
lifetime: 2592000 # 30 days in seconds
key: '@=service("ForkCMS\\Modules\\Backend\\Domain\\Authentication\\TwoFactorAuthenticationFactory").secret()'

backup_codes:
enabled: true
5 changes: 5 additions & 0 deletions config/packages/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ security:
pattern: ^/private
lazy: true
provider: backend_user_provider
two_factor:
auth_form_path: backend_2fa_login
check_path: backend_2fa_login_check
multi_factor: true
remember_me:
secret: '%kernel.secret%'
path: /private
Expand All @@ -34,6 +38,7 @@ security:
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/private/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
- { path: ^/private/\w\w/backend/authentication-login, roles: PUBLIC_ACCESS }
- { path: ^/private/\w\w/backend/authentication-logout, roles: PUBLIC_ACCESS }
- { path: ^/private, roles: ROLE_USER }
8 changes: 8 additions & 0 deletions config/routes/scheb_2fa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
backend_2fa_login:
path: /private/2fa
defaults:
_controller: ForkCMS\Modules\Backend\Controller\TwoFactorAuthenticationController::form
_method: "form"

backend_2fa_login_check:
path: /private/2fa/check
78 changes: 44 additions & 34 deletions src/Core/assets/js/Components/Ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,59 @@ import { Data } from './Data'
import { Messages } from './Messages'

export class Ajax {
_ajaxSpinner
_baseUrl
_locale

constructor (locale, baseUrl) {
// variables
const $ajaxSpinner = $('[data-role="fork-ajax-spinner"]')
this._ajaxSpinner = $('[data-role="fork-ajax-spinner"]')
this._baseUrl = baseUrl
this._locale = locale
}

makeRequest (data, successCallback, errorCallback) {
console.log(data, successCallback, errorCallback)

this._ajaxSpinner.show()

// set defaults for AJAX
$.ajaxSetup(
// eslint-disable-next-line no-undef
const formData = new URLSearchParams()
for (const key in data) {
formData.append(key, data[key])
}

return fetch(
this._baseUrl,
{
url: baseUrl,
cache: false,
type: 'POST',
dataType: 'json',
timeout: 10000,
beforeSend: (jqXHR) => {
jqXHR.setRequestHeader('X-CSRF-Token', Data.get('csrf-token'))
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-CSRF-Token': Data.get('csrf-token'),
'X-Requested-With': 'XMLHttpRequest'
},
data: {}
body: formData
}
)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText)
}

// global error handler
$(document).ajaxError((e, XMLHttpRequest, ajaxOptions) => {
// 401 means we aren't authenticated anymore, so reload the page
if (XMLHttpRequest.status === 401) window.location.reload()

// check if a custom errorhandler is used
if (typeof ajaxOptions.error === 'undefined') {
// init var
let textStatus = locale.err('SomethingWentWrong')
return response.json()
})
.then(successCallback)
.catch(error => {
if (errorCallback) {
errorCallback(error)

// get real message
if (typeof XMLHttpRequest.responseText !== 'undefined') textStatus = $.parseJSON(XMLHttpRequest.responseText).message
return
}

// show message
Messages.add('danger', textStatus, '', true)
}
})

// spinner stuff
$(document).ajaxStart(() => {
$ajaxSpinner.show()
})
$(document).ajaxStop(() => {
$ajaxSpinner.hide()
})
Messages.add('danger', error.message, '', true)
})
.finally(() => {
this._ajaxSpinner.hide()
})
}
}
35 changes: 25 additions & 10 deletions src/Core/assets/js/Components/AjaxContentEditable.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Messages } from './Messages'
import { Data } from './Data'

class EditableContend {
constructor ($editable, locale) {
Expand All @@ -21,20 +22,34 @@ class EditableContend {
this.tooltip.addClass('invisible')
this.content.attr('contenteditable', false)
if (originalContent !== this.content.text()) {
$.ajax(
const formData = new URLSearchParams()
formData.append('content', this.content.text())

fetch(
url,
{
url,
data: {
content: this.content.text()
},
success: function (XMLHttpRequest) {
Messages.add('success', XMLHttpRequest.message || locale.msg('Edited'))
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-CSRF-Token': Data.get('csrf-token'),
'X-Requested-With': 'XMLHttpRequest'
},
error: function (XMLHttpRequest) {
Messages.add('danger', $.parseJSON(XMLHttpRequest.responseText).message)
}
body: formData
}
)
.then(response => {
if (!response.ok) {
throw new Error(response.message)
}

return response.json()
})
.then(json => {
Messages.add('success', json.message || locale.msg('Edited'))
})
.catch(error => {
Messages.add('danger', error.message)
})
}
})
}
Expand Down
41 changes: 41 additions & 0 deletions src/Core/assets/js/Components/ClipBoard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Messages } from './Messages'

export class ClipBoard {
constructor (element, successMessage = null) {
this.element = element
this.successMessage = successMessage

this.element.on('click', this.copy.bind(this))
}

copy (event) {
event.preventDefault()

const target = $(event.currentTarget)
let contentToCopy

if (target.attr('data-clipboard-text')) {
contentToCopy = target.attr('data-clipboard-text')
}

if (target.attr('data-clipboard-target')) {
contentToCopy = $(target.attr('data-clipboard-target')).text()

const lines = contentToCopy.split('\n')
// Remove leading and trailing spaces on each line
contentToCopy = lines.map(line => line.trim()).join('\n')
}

if (contentToCopy === undefined || contentToCopy === '') {
return
}

navigator.clipboard.writeText(contentToCopy)

if (this.successMessage === null) {
return
}

Messages.add('success', this.successMessage)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace ForkCMS\Modules\Backend\Backend\Actions;

use ForkCMS\Core\Domain\Header\FlashMessage\FlashMessage;
use ForkCMS\Core\Domain\Header\FlashMessage\FlashMessageType;
use ForkCMS\Modules\Backend\Domain\Action\AbstractActionController;
use ForkCMS\Modules\Backend\Domain\Action\ActionServices;
use ForkCMS\Modules\Backend\Domain\User\User;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use ForkCMS\Modules\Backend\Domain\User\Command\DisableTwoFactorAuthentication as DisableTwoFactorAuthenticationCommand;
use Symfony\Component\HttpFoundation\Response;

class DisableTwoFactorAuthentication extends AbstractActionController
{
private User $user;

public function __construct(ActionServices $actionServices)
{
parent::__construct($actionServices);
}

protected function execute(Request $request): void
{
$this->user = $this->getEntityFromRequest($request, User::class);
$this->commandBus->dispatch(
new DisableTwoFactorAuthenticationCommand($this->user)
);

$this->header->addFlashMessage(
new FlashMessage(
'msg.2FAIsDisabled',
FlashMessageType::SUCCESS
)
);
}

public function getResponse(Request $request): Response
{
return new RedirectResponse(
UserEdit::getActionSlug()->generateRoute($this->router, ['slug' => $this->user->getId()])
);
}
}
53 changes: 53 additions & 0 deletions src/Modules/Backend/Backend/Actions/ModuleSettings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace ForkCMS\Modules\Backend\Backend\Actions;

use ForkCMS\Core\Domain\Header\FlashMessage\FlashMessage;
use ForkCMS\Core\Domain\Kernel\Command\ClearContainerCache;
use ForkCMS\Modules\Backend\Domain\Action\AbstractFormActionController;
use ForkCMS\Modules\Backend\Domain\ModuleSettings\ModuleSettingsType;
use ForkCMS\Modules\Backend\Domain\User\User;
use ForkCMS\Modules\Backend\Domain\User\UserRepository;
use ForkCMS\Modules\Extensions\Domain\Module\Command\ChangeModuleSettings;
use ForkCMS\Modules\Extensions\Domain\Module\Module;
use ForkCMS\Modules\Extensions\Domain\Module\ModuleName;
use RuntimeException;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

final class ModuleSettings extends AbstractFormActionController
{
protected function getFormResponse(Request $request): ?Response
{
$moduleRepository = $this->getRepository(Module::class);
$moduleName = $this->getModuleName();

return $this->handleSettingsForm(
request: $request,
formType: ModuleSettingsType::class,
formData: new ChangeModuleSettings(
$moduleRepository->find(ModuleName::core()) ?? throw new RuntimeException('Core module not found'),
$moduleRepository->find($moduleName) ?? throw new RuntimeException($moduleName . ' module not found'),
[]
),
validCallback: function (FormInterface $form): Response {
carakas marked this conversation as resolved.
Show resolved Hide resolved
$this->commandBus->dispatch($form->getData());

if (!$this->moduleSettings->get(ModuleName::fromString('Backend'), '2fa_enabled', false)) {
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't this also belong in the command? Otherwise if the command is used somewhere else this code won't be executed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed cd1ac62

/** @var UserRepository $userRepository */
$userRepository = $this->getRepository(User::class);
$users = $userRepository->findAll();

foreach ($users as $user) {
$user->disableTwoFactorAuthentication();
$userRepository->save($user);
}
}

return new RedirectResponse(self::getActionSlug()->generateRoute($this->router));
}
);
}
}
17 changes: 16 additions & 1 deletion src/Modules/Backend/Backend/Actions/UserAdd.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,25 @@ protected function getFormResponse(Request $request): ?Response
request: $request,
formType: UserType::class,
formData: new CreateUser(),
redirectResponse: new RedirectResponse(UserIndex::getActionSlug()->generateRoute($this->router)),
formOptions: [
'validation_groups' => ['Default', 'create'],
],
validCallback: function (FormInterface $form) use ($request): Response {
$this->commandBus->dispatch($form->getData());

if ($form->getData()->enableTwoFactorAuthentication) {
$request->getSession()->set('showBackupCodes', true);

return new RedirectResponse(
UserEdit::getActionSlug()->generateRoute(
$this->router,
['slug' => $form->getData()->getEntity()->getId()]
)
);
}

return new RedirectResponse(UserIndex::getActionSlug()->generateRoute($this->router));
},
flashMessageCallback: static function (FormInterface $form): FlashMessage {
return FlashMessage::success('UserAdded', ['%user%' => $form->getData()->displayName]);
}
Expand Down