├── src
├── Resources
│ ├── config
│ │ ├── plugin.png
│ │ ├── routes.xml
│ │ ├── services.xml
│ │ └── config.xml
│ ├── app
│ │ ├── administration
│ │ │ └── src
│ │ │ │ ├── override
│ │ │ │ ├── sw-profile
│ │ │ │ │ ├── page
│ │ │ │ │ │ └── sw-profile-index
│ │ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ │ └── sw-profile-index.html.twig
│ │ │ │ │ └── view
│ │ │ │ │ │ └── sw-profile-index-general
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── sw-profile-index-general.html.twig
│ │ │ │ ├── sw-users-permissions
│ │ │ │ │ └── page
│ │ │ │ │ │ └── sw-users-permissions-user-detail
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── sw-users-permissions-user-detail.html.twig
│ │ │ │ ├── sw-customer
│ │ │ │ │ └── component
│ │ │ │ │ │ └── sw-customer-base-info
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── sw-customer-base-info.twig
│ │ │ │ └── sw-login
│ │ │ │ │ └── view
│ │ │ │ │ └── sw-login-login
│ │ │ │ │ ├── sw-login-login.html.twig
│ │ │ │ │ └── index.js
│ │ │ │ ├── component
│ │ │ │ └── rl-user-otp
│ │ │ │ │ ├── rl-user-otp.scss
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── rl-user-otp.html.twig
│ │ │ │ ├── api
│ │ │ │ ├── index.js
│ │ │ │ └── rl-2fa.js
│ │ │ │ ├── snippet
│ │ │ │ ├── en-GB.json
│ │ │ │ ├── de-DE.json
│ │ │ │ ├── nl-NL.json
│ │ │ │ ├── pl-PL.json
│ │ │ │ └── fr-FR.json
│ │ │ │ └── main.js
│ │ └── storefront
│ │ │ └── src
│ │ │ ├── main.js
│ │ │ ├── scss
│ │ │ └── base.scss
│ │ │ └── plugin
│ │ │ └── rl2fa-verification.plugin.js
│ ├── views
│ │ ├── storefront
│ │ │ └── page
│ │ │ │ ├── account
│ │ │ │ └── profile
│ │ │ │ │ ├── 2fa
│ │ │ │ │ ├── disable.html.twig
│ │ │ │ │ └── setup.html.twig
│ │ │ │ │ └── index.html.twig
│ │ │ │ └── 2fa
│ │ │ │ └── verification.html.twig
│ │ └── administration
│ │ │ └── index.html.twig
│ └── snippet
│ │ ├── messages.en-GB.json
│ │ ├── messages.de-DE.json
│ │ ├── messages.nl-NL.json
│ │ ├── messages.pl-PL.json
│ │ └── messages.fr-FR.json
├── RuneLaenenTwoFactorAuth.php
├── Service
│ ├── TimebasedOneTimePasswordServiceInterface.php
│ ├── ConfigurationService.php
│ └── TimebasedOneTimePasswordService.php
├── Event
│ ├── StorefrontTwoFactorAuthEvent.php
│ └── StorefrontTwoFactorCancelEvent.php
├── Helper
│ └── ContextHelper.php
├── Controller
│ ├── QrCodeController.php
│ ├── TwoFactorAuthenticationApiController.php
│ ├── StorefrontTwoFactorAuthController.php
│ └── TwoFactorAuthenticationController.php
├── Extension
│ └── FileExtension.php
└── Subscriber
│ ├── ApiOauthTokenSubscriber.php
│ └── CustomerLoginSubscriber.php
├── bin
└── ecs-fix.sh
├── .gitignore
├── ecs.php
├── .github
├── workflows
│ └── shopware-cli.yml
└── CONTRIBUTING.md
├── grumphp.yml
├── LICENSE
├── README.md
└── composer.json
/src/Resources/config/plugin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/runelaenen/shopware6-two-factor-auth/HEAD/src/Resources/config/plugin.png
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/override/sw-profile/page/sw-profile-index/index.js:
--------------------------------------------------------------------------------
1 | import template from './sw-profile-index.html.twig';
2 |
3 | export default { template };
4 |
--------------------------------------------------------------------------------
/bin/ecs-fix.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | echo "Fixing ecs errors"
3 | php ../../../dev-ops/analyze/vendor/bin/ecs check --fix --config=../../../vendor/shopware/platform/easy-coding-standard.yml . --fix
4 |
--------------------------------------------------------------------------------
/src/Resources/app/storefront/src/main.js:
--------------------------------------------------------------------------------
1 | window.PluginManager.register(
2 | 'Rl2faVerificationPlugin',
3 | () => import('./plugin/rl2fa-verification.plugin'),
4 | '[data-rl2fa-verification-plugin]'
5 | );
6 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/override/sw-users-permissions/page/sw-users-permissions-user-detail/index.js:
--------------------------------------------------------------------------------
1 | import template from './sw-users-permissions-user-detail.html.twig';
2 |
3 | export default { template };
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
3 | src/Resources/app/storefront/dist/storefront/js
4 | src/Resources/app/administration/.tmp
5 | src/Resources/public/administration/.vite
6 | src/Resources/public/administration/assets
7 | var/cache
8 | .DS_Store
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/component/rl-user-otp/rl-user-otp.scss:
--------------------------------------------------------------------------------
1 | .rl-2fa-qr-code {
2 | text-align: center;
3 |
4 | &--secret {
5 | color: gray;
6 | display: block;
7 | }
8 | }
9 |
10 | .mt-1 {
11 | margin-top: 1rem;
12 | }
13 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/override/sw-profile/view/sw-profile-index-general/index.js:
--------------------------------------------------------------------------------
1 | import template from './sw-profile-index-general.html.twig';
2 |
3 | export default {
4 | template,
5 |
6 | methods: {
7 | onSave() {
8 | this.$emit('rl-2fa-save');
9 | },
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/src/RuneLaenenTwoFactorAuth.php:
--------------------------------------------------------------------------------
1 |
7 |
8 | {% parent() %}
9 | {% endblock %}
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/api/index.js:
--------------------------------------------------------------------------------
1 | import Rl2faService from './rl-2fa';
2 |
3 | const { Application } = Shopware;
4 |
5 | Application.addServiceProvider('rl2faService', (container) => {
6 | const initContainer = Application.getContainer('init');
7 |
8 | return new Rl2faService(initContainer.httpClient, container.loginService);
9 | });
10 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/override/sw-users-permissions/page/sw-users-permissions-user-detail/sw-users-permissions-user-detail.html.twig:
--------------------------------------------------------------------------------
1 | {% block sw_setting_user_detail_card_integrations %}
2 |
7 |
8 | {% parent() %}
9 | {% endblock %}
--------------------------------------------------------------------------------
/src/Resources/config/routes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/Service/TimebasedOneTimePasswordServiceInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/Event/StorefrontTwoFactorAuthEvent.php:
--------------------------------------------------------------------------------
1 | salesChannelContext;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Event/StorefrontTwoFactorCancelEvent.php:
--------------------------------------------------------------------------------
1 | salesChannelContext;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Helper/ContextHelper.php:
--------------------------------------------------------------------------------
1 | import('vendor/shopware/platform/easy-coding-standard.php');
9 | $parameters = $containerConfigurator->parameters();
10 | $parameters->set(Option::SKIP, [
11 | AssignmentInConditionSniff::class . '.FoundInWhileCondition',
12 | ]);
13 | };
14 |
--------------------------------------------------------------------------------
/.github/workflows/shopware-cli.yml:
--------------------------------------------------------------------------------
1 | name: shopware-cli
2 | on:
3 | pull_request:
4 | push:
5 |
6 | permissions:
7 | contents: write
8 |
9 | jobs:
10 | checks:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v3
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Install Shopware CLI
19 | uses: shopware/shopware-cli-action@v1
20 |
21 | - name: Validation
22 | run: shopware-cli extension validate --full --reporter github .
23 |
24 | zip:
25 | uses: shopware/github-actions/.github/workflows/build-zip.yml@main
26 | with:
27 | extensionName: RuneLaenenTwoFactorAuth
--------------------------------------------------------------------------------
/grumphp.yml:
--------------------------------------------------------------------------------
1 | grumphp:
2 | tasks:
3 | ecs:
4 | config: 'ecs.php'
5 | clear-cache: true
6 | paths:
7 | - 'src'
8 | eslint:
9 | bin: 'vendor/shopware/platform/src/Administration/Resources/app/administration/node_modules/.bin/eslint'
10 | config: 'vendor/shopware/platform/src/Administration/Resources/app/administration/.eslintrc.js'
11 | whitelist_patterns:
12 | - /src\/Resources\/app\/administration\/(.*)/
13 | triggered_by:
14 | - js
15 | - vue
16 | environment:
17 | paths:
18 | - '../../../dev-ops/analyze/vendor/bin/'
19 |
--------------------------------------------------------------------------------
/src/Resources/app/storefront/src/scss/base.scss:
--------------------------------------------------------------------------------
1 | .account-profile-2fa {
2 | margin-bottom: 3rem;
3 | }
4 |
5 | .rl-2fa-setup-step {
6 | border: 1px solid #008490;
7 | margin-bottom: $spacer;
8 |
9 | &--title {
10 | display: block;
11 | background: #008490;
12 | color: #fff;
13 | padding: 5px;
14 | }
15 |
16 | &--content {
17 | padding: $spacer;
18 | }
19 |
20 | &:last-of-type {
21 | margin-bottom: 0;
22 | }
23 | }
24 |
25 | .rl-2fa-qr-code {
26 | text-align: center;
27 |
28 | &--image {
29 | max-width: 80%;
30 | }
31 |
32 | &--secret {
33 | color: gray;
34 | display: block;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Service/ConfigurationService.php:
--------------------------------------------------------------------------------
1 | systemConfig->getString(self::CONFIGURATION_KEY . 'administrationCompany', $salesChannelId);
20 | }
21 |
22 | public function isStorefrontEnabled(?string $salesChannelId = null): bool
23 | {
24 | return $this->systemConfig->getBool(self::CONFIGURATION_KEY . 'storefrontEnabled', $salesChannelId);
25 | }
26 |
27 | public function getStorefrontCompany(?string $salesChannelId = null): string
28 | {
29 | return $this->systemConfig->getString(self::CONFIGURATION_KEY . 'storefrontCompany', $salesChannelId);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Rune Laenen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Resources/views/storefront/page/account/profile/2fa/disable.html.twig:
--------------------------------------------------------------------------------
1 | {% block page_account_profile_2fa_disable %}
2 |
{{ "rl-2fa.account.disable.title"|trans|sw_sanitize }}
3 |
4 |
5 | {{ "rl-2fa.account.disable.description"|trans|sw_sanitize }}
6 |
7 |
8 |
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/src/Controller/QrCodeController.php:
--------------------------------------------------------------------------------
1 | false, '_routeScope' => ['administration']],
21 | methods: ['GET'],
22 | )]
23 | public function qrCode(Request $request): Response
24 | {
25 | $qrUrl = $request->query->getString('qrUrl');
26 | $renderer = new ImageRenderer(new RendererStyle(400), new SvgImageBackEnd());
27 | $qrCode = (new Writer($renderer))->writeString($qrUrl);
28 |
29 | return new Response($qrCode, Response::HTTP_OK, ['Content-Type' => 'image/svg+xml']);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/api/rl-2fa.js:
--------------------------------------------------------------------------------
1 | const { ApiService } = Shopware.Classes;
2 |
3 | export default class Rl2fa extends ApiService {
4 | constructor(httpClient, loginService, apiEndpoint = '_action/rl-2fa') {
5 | super(httpClient, loginService, apiEndpoint);
6 | }
7 |
8 | getSecret(holder) {
9 | const apiRoute = `${this.getApiBasePath()}/generate-secret`;
10 |
11 | return this.httpClient
12 | .get(apiRoute, {
13 | params: { holder },
14 | headers: this.getBasicHeaders(),
15 | })
16 | .then((response) => {
17 | return ApiService.handleResponse(response);
18 | });
19 | }
20 |
21 | validateSecret(secret, code) {
22 | const apiRoute = `${this.getApiBasePath()}/validate-secret`;
23 |
24 | return this.httpClient
25 | .post(
26 | apiRoute,
27 | { secret, code },
28 | { headers: this.getBasicHeaders() }
29 | )
30 | .then((response) => {
31 | return ApiService.handleResponse(response);
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/override/sw-customer/component/sw-customer-base-info/sw-customer-base-info.twig:
--------------------------------------------------------------------------------
1 | {% block sw_customer_base_metadata_active %}
2 | {% parent() %}
3 |
4 |
5 | {% block sw_customer_base_metadata_2fa_label %}
6 |
7 | {{ $tc('rl-2fa.settings.user-detail.title') }}
8 |
9 | {% endblock %}
10 |
11 | {% block sw_customer_base_metadata_2fa_content %}
12 |
16 | {{ $tc('sw-customer.baseInfo.contentActive', twoFactorAuthenticationActive ? 1 : 2) }}
17 |
18 | {% endblock %}
19 |
20 | {% block sw_customer_base_metadata_2fa_editor %}
21 |
22 |
26 | {{ $tc('rl-2fa.settings.user-detail.enabled.disable') }}
27 |
28 |
29 | {% endblock %}
30 |
31 | {% endblock %}
--------------------------------------------------------------------------------
/src/Resources/views/administration/index.html.twig:
--------------------------------------------------------------------------------
1 | {% extends '@Administration/administration/index.html.twig' %}
2 |
3 | {% block administration_login_scripts %}
4 |
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/override/sw-login/view/sw-login-login/sw-login-login.html.twig:
--------------------------------------------------------------------------------
1 | {% block sw_login_login %}
2 |
3 | {% parent %}
4 |
5 |
6 |
7 |
10 |
11 | {{ $tc('sw-login.index.headlineForm') }}
12 |
13 |
20 |
21 |
22 |
28 | {{ $tc('sw-login.index.buttonLogin') }}
29 |
30 |
31 |
32 |
33 | {% endblock %}
--------------------------------------------------------------------------------
/src/Resources/config/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | Administration 2FA
7 |
8 |
9 | administrationCompany
10 |
11 | Company name for QR code in 2FA app
12 | Shopware Administration
13 |
14 |
15 |
16 |
17 | Storefront 2FA
18 |
19 |
20 | storefrontEnabled
21 |
22 | true
23 |
24 |
25 |
26 | storefrontCompany
27 |
28 | Company name for QR code in 2FA app
29 | Webshop
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/snippet/en-GB.json:
--------------------------------------------------------------------------------
1 | {
2 | "rl-2fa": {
3 | "settings": {
4 | "user-detail": {
5 | "title": "Two factor authentication",
6 | "enabled": {
7 | "title": "Two factor authentication is enabled",
8 | "description": "Congrats! You're secure! To disable 2FA for your account, click the button below.",
9 | "disable": "Disable 2FA"
10 | },
11 | "not-enabled": {
12 | "title": "Two factor authentication is not enabled",
13 | "description": "Two factor authentication is not enabled for your account. Click the button below to set it up.",
14 | "get-started": "Click here to get started"
15 | },
16 | "generating": {
17 | "scan-code": "Scan the code on the left with your 2FA app.",
18 | "description": "Fill in your One Time Password and click 'Validate & enable' to check the code and enable 2FA for your admin account.",
19 | "error-title": "Something went wrong",
20 | "validate-save": "Validate & save"
21 | }
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/snippet/de-DE.json:
--------------------------------------------------------------------------------
1 | {
2 | "rl-2fa": {
3 | "settings": {
4 | "user-detail": {
5 | "title": "Zwei-Faktor-Authentifizierung",
6 | "enabled": {
7 | "title": "Zwei-Faktor-Authentifizierung ist aktiv",
8 | "description": "Glückwunsch, dein Account ist sicher! Um 2FA zu deaktivieren klicke auf den nachstehenden Button.",
9 | "disable": "2FA deaktivieren"
10 | },
11 | "not-enabled": {
12 | "title": "Zwei-Faktor-Authentifizierung ist inaktiv",
13 | "description": "Zwei-Faktor-Authentifizierung ist inaktiv! Zum Aktivieren klicke auf den nachstehenden Button.",
14 | "get-started": "2FA aktivieren"
15 | },
16 | "generating": {
17 | "scan-code": "Scanne den Code in deiner 2FA-App.",
18 | "description": "Trage dein Einmal-Passwort ein und klicke auf Validieren & Speichern um 2FA für deinen Account zu aktivieren.",
19 | "error-title": "Es ist ein Fehler aufgetreten",
20 | "validate-save": "Validieren & Speichern"
21 | }
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/override/sw-profile/page/sw-profile-index/sw-profile-index.html.twig:
--------------------------------------------------------------------------------
1 | {% block sw_profile_index_router_view %}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
30 |
31 |
32 | {% endblock %}
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/snippet/nl-NL.json:
--------------------------------------------------------------------------------
1 | {
2 | "rl-2fa": {
3 | "settings": {
4 | "user-detail": {
5 | "title": "Twee-factorauthenticatie",
6 | "enabled": {
7 | "title": "Twee-factorauthenticatie is ingeschakeld",
8 | "description": "Proficiat! Je account is veilig! Klik op onderstaande knop om 2FA uit te zetten.",
9 | "disable": "Zet 2FA uit"
10 | },
11 | "not-enabled": {
12 | "title": "Twee-factorauthenticatie is uitgeschakeld",
13 | "description": "Twee-factorauthenticatie is niet actief voor jouw account. Klik op onderstaande knop om 2FA aan te zetten.",
14 | "get-started": "2FA instellen"
15 | },
16 | "generating": {
17 | "scan-code": "Scan de code links met jouw 2FA app.",
18 | "description": "Vul je One Time Password token in, en klik op 'Valideer & sla op' om de token na te kijken. Als de code correct is wordt 2FA aangezet voor jouw account.",
19 | "error-title": "Er ging iets mis.",
20 | "validate-save": "Valideer & sla op"
21 | }
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/snippet/pl-PL.json:
--------------------------------------------------------------------------------
1 | {
2 | "rl-2fa": {
3 | "settings": {
4 | "user-detail": {
5 | "title": "Uwierzytelnianie dwuskładnikowe",
6 | "enabled": {
7 | "title": "Uwierzytelnianie dwuskładnikowe jest włączone",
8 | "description": "Gratulacje! Jesteś bezpieczny! Aby wyłączyć 2FA dla swojego konta, kliknij przycisk poniżej.",
9 | "disable": "Wyłącz 2FA"
10 | },
11 | "not-enabled": {
12 | "title": "Uwierzytelnianie dwuskładnikowe nie jest włączone",
13 | "description": "Uwierzytelnianie dwuskładnikowe nie jest włączone dla Twojego konta. Kliknij przycisk poniżej, aby je skonfigurować.",
14 | "get-started": "Kliknij, aby rozpocząć"
15 | },
16 | "generating": {
17 | "scan-code": "Zeskanuj kod po lewej stronie za pomocą aplikacji 2FA.",
18 | "description": "Wprowadź swoje hasło jednorazowe i kliknij „Zweryfikuj i włącz”, aby sprawdzić kod i włączyć 2FA dla swojego konta administratora.",
19 | "error-title": "Coś poszło nie tak",
20 | "validate-save": "Zweryfikuj i zapisz"
21 | }
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Service/TimebasedOneTimePasswordService.php:
--------------------------------------------------------------------------------
1 | google2fa = new Google2FA();
19 | }
20 |
21 | /**
22 | * @throws IncompatibleWithGoogleAuthenticatorException|InvalidCharactersException|SecretKeyTooShortException
23 | */
24 | public function createSecret(): string
25 | {
26 | return $this->google2fa->generateSecretKey();
27 | }
28 |
29 | public function getQrCodeUrl(string $company, string $holder, string $secret): string
30 | {
31 | return $this->google2fa->getQRCodeUrl($company, $holder, $secret);
32 | }
33 |
34 | /**
35 | * @throws IncompatibleWithGoogleAuthenticatorException|InvalidCharactersException|SecretKeyTooShortException
36 | */
37 | public function verifyCode(string $secret, string $code): bool
38 | {
39 | return $this->google2fa->verifyKey($secret, $code);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/snippet/fr-FR.json:
--------------------------------------------------------------------------------
1 | {
2 | "rl-2fa": {
3 | "settings": {
4 | "user-detail": {
5 | "title": "Authentification à deux facteurs (2FA)",
6 | "enabled": {
7 | "title": "L'authentification à deux facteurs est activée",
8 | "description": "Félicitations! Vous êtes en sécurité! Pour désactiver 2FA pour votre compte, cliquez sur le bouton ci-dessous.",
9 | "disable": "Désactiver 2FA"
10 | },
11 | "not-enabled": {
12 | "title": "L'authentification à deux facteurs n'est pas activée",
13 | "description": "L'authentification à deux facteurs n'est pas activée pour votre compte. Cliquez sur le bouton ci-dessous pour le configurer.",
14 | "get-started": "Cliquez ici pour commencer"
15 | },
16 | "generating": {
17 | "scan-code": "Scannez le code à gauche avec votre application 2FA.",
18 | "description": "Entrez votre mot de passe à usage unique (OTP) et cliquez sur 'Valider & activer' pour vérifier le code et activer 2FA pour votre compte administrateur.",
19 | "error-title": "Un problème est survenu",
20 | "validate-save": "Valider & sauvegarder"
21 | }
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## How to contribute
2 |
3 | #### **Did you find a bug?**
4 |
5 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/runelaenen/shopware6-two-factor-auth/issues).
6 |
7 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/runelaenen/shopware6-two-factor-auth/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible. Images or screen-captures are very welcome.
8 |
9 | #### **Is this your first time contributing to an open source project on GitHub?**
10 |
11 | Great! And welcome to the team!
12 |
13 | It's not difficult, but there are some rules to adhere to:
14 |
15 | * Work on your own fork. Fork the repo and push your changes to this repo. Create PR when you're done.
16 |
17 | * Use feature branches. Don't work directly on the `master` branch, but create a different branch for every issue you tackle. For example `feature/issue-22` or `feature/fixes-nasty-bug-x`
18 |
19 | #### **Did you write an update that fixes a bug?**
20 |
21 | * Open a new GitHub pull request with the change.
22 |
23 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
24 |
25 |
26 |
27 |
28 |
29 | This plugin is a volunteer effort. We encourage you to pitch in and join the team!
30 |
31 | Thanks! :blue_heart: :blue_heart: :blue_heart:
32 |
--------------------------------------------------------------------------------
/src/Extension/FileExtension.php:
--------------------------------------------------------------------------------
1 | pathToBundleMainJs(...)),
26 | ];
27 | }
28 |
29 | public function pathToBundleMainJs(string $bundle, string $type = 'administration'): ?string
30 | {
31 | try {
32 | $content = $this->operator->read(\sprintf(
33 | '/bundles/%s/%s/.vite/manifest.json',
34 | strtolower($bundle),
35 | strtolower($type)
36 | ));
37 | $content = json_decode($content, true, 512, \JSON_THROW_ON_ERROR);
38 |
39 | return $type . '/' . ($content['main.js']['file'] ?? '');
40 | } catch (\Throwable) {
41 | return null;
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Two Factor Authentication for Shopware 6
2 | [](//packagist.org/packages/runelaenen/shopware6-two-factor-auth)
3 | [](//packagist.org/packages/runelaenen/shopware6-two-factor-auth)
4 | [](//packagist.org/packages/runelaenen/shopware6-two-factor-auth)
5 |
6 | 
7 |
8 | Add extra security to your Shopware 6 shop by enabling Two Factor Authentication.
9 |
10 | Adds an extra prompt to admin- or customer-accounts in your Shopware 6 website.
11 |
12 | ## Features
13 | - 'Google Authenticator' provider
14 | - Storefront customer 2FA
15 | - Admin user 2FA
16 | - Local QR code generation
17 | - Fully localized:
18 | - English
19 | - German
20 | - French
21 | - Dutch
22 | - Polish
23 |
24 | ## Providers
25 | At the moment only Google Authenticator (compatible) apps are supported.
26 | For example Google Authenticator, Authy, LastPass, Bitwarden, ...
27 |
28 | ## Installation guide
29 |
30 | This plugin can only be installed using Composer.
31 |
32 | ```
33 | # Install plugin using composer
34 | composer require runelaenen/shopware6-two-factor-auth
35 |
36 | # Refresh plugins & install & activate plugin
37 | bin/console plugin:refresh
38 | bin/console plugin:install --activate RuneLaenenTwoFactorAuth
39 |
40 | # Build javascript files
41 | bin/build-js.sh
42 | ```
43 |
44 | ## Development
45 | Keep in mind that 2FA authentication will not work in the development Administration watcher mode.
46 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "runelaenen/shopware6-two-factor-auth",
3 | "description": "Two Factor Authentication plugin",
4 | "type": "shopware-platform-plugin",
5 | "license": "MIT",
6 | "version": "7.0.0",
7 | "autoload": {
8 | "psr-4": {
9 | "RuneLaenen\\TwoFactorAuth\\": "src/"
10 | }
11 | },
12 | "extra": {
13 | "shopware-plugin-class": "RuneLaenen\\TwoFactorAuth\\RuneLaenenTwoFactorAuth",
14 | "plugin-icon": "src/Resources/config/plugin.png",
15 | "label": {
16 | "de-DE": "Two Factor Authentication plugin",
17 | "en-GB": "Two Factor Authentication plugin"
18 | },
19 | "description": {
20 | "de-DE": "Add extra security to your Shopware 6 shop by enabling Two Factor Authentication. Adds an extra prompt to admin- or customer-accounts in your Shopware 6 website.",
21 | "en-GB": "Add extra security to your Shopware 6 shop by enabling Two Factor Authentication. Adds an extra prompt to admin- or customer-accounts in your Shopware 6 website."
22 | },
23 | "manufacturerLink": {
24 | "de-DE": "https://kraftware.be/",
25 | "en-GB": "https://kraftware.be/"
26 | },
27 | "supportLink": {
28 | "de-DE": "https://github.com/runelaenen/shopware6-two-factor-auth/issues",
29 | "en-GB": "https://github.com/runelaenen/shopware6-two-factor-auth/issues"
30 | }
31 | },
32 | "require": {
33 | "shopware/core": "~6.7.0",
34 | "pragmarx/google2fa": "^8.0",
35 | "bacon/bacon-qr-code": "^3.0"
36 | },
37 | "require-dev": {
38 | "phpro/grumphp-shim": "^1.3",
39 | "shopware/platform": "*",
40 | "symplify/easy-coding-standard": "^9.4",
41 | "kubawerlos/php-cs-fixer-custom-fixers": "^3.1"
42 | },
43 | "authors": [
44 | {
45 | "name": "Kraftware",
46 | "email": "hello@kraftware.be",
47 | "homepage": "https://www.kraftware.be"
48 | }
49 | ]
50 | }
51 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/main.js:
--------------------------------------------------------------------------------
1 | import enGB from './snippet/en-GB.json';
2 | import deDE from './snippet/de-DE.json';
3 | import frFR from './snippet/fr-FR.json';
4 | import nlNL from './snippet/nl-NL.json';
5 | import plPL from './snippet/pl-PL.json';
6 |
7 | import './api/index';
8 | Shopware.Component.register(
9 | 'rl-user-otp',
10 | () => import('./component/rl-user-otp')
11 | );
12 | Shopware.Component.override(
13 | 'sw-login-login',
14 | () => import('./override/sw-login/view/sw-login-login')
15 | );
16 | if (Shopware.Component.getComponentRegistry().has('sw-profile-index')) {
17 | Shopware.Component.override(
18 | 'sw-profile-index',
19 | () => import('./override/sw-profile/page/sw-profile-index')
20 | );
21 | }
22 | if (Shopware.Component.getComponentRegistry().has('sw-profile-index-general')) {
23 | Shopware.Component.override(
24 | 'sw-profile-index-general',
25 | () => import('./override/sw-profile/view/sw-profile-index-general')
26 | );
27 | }
28 | if (
29 | Shopware.Component.getComponentRegistry().has(
30 | 'sw-users-permissions-user-detail'
31 | )
32 | ) {
33 | Shopware.Component.override(
34 | 'sw-users-permissions-user-detail',
35 | () =>
36 | import(
37 | './override/sw-users-permissions/page/sw-users-permissions-user-detail'
38 | )
39 | );
40 | }
41 | if (Shopware.Component.getComponentRegistry().has('sw-customer-base-info')) {
42 | Shopware.Component.override(
43 | 'sw-customer-base-info',
44 | () => import('./override/sw-customer/component/sw-customer-base-info')
45 | );
46 | }
47 |
48 | Shopware.Locale.extend('de-DE', deDE);
49 | Shopware.Locale.extend('en-GB', enGB);
50 | Shopware.Locale.extend('fr-FR', frFR);
51 | Shopware.Locale.extend('nl-NL', nlNL);
52 | Shopware.Locale.extend('pl-PL', plPL);
53 |
--------------------------------------------------------------------------------
/src/Resources/snippet/messages.en-GB.json:
--------------------------------------------------------------------------------
1 | {
2 | "rl-2fa": {
3 | "login": {
4 | "title": "Two Factor Authentication",
5 | "description": "Please verify your identity using your 2FA app.",
6 | "code-placeholder": "Code",
7 | "proceed": "Log in",
8 | "cancel": "Cancel"
9 | },
10 | "account": {
11 | "title": "Two Factor Authentication",
12 | "active": "Good! Your account is currently protected with Two Factor Authentication!",
13 | "not-active": "Your account is currently not protected with Two Factor Authentication!",
14 | "description": "Make sure your webshop account is protected securily by adding Two Factor Authentication (2FA). With 2FA, your account is secured in multiple layers: your username, your password, and a new 'one time password' layer. This second step is added to reconfirm your identity, and links your account to your mobile phone. Click the button below to get started on setting up 2FA for your account. It's easy and free!",
15 | "start-setup": "Set up 2FA for my account",
16 | "disable-2fa": "Disable Two Factor Authentication",
17 | "disabled-2fa": "Two Factor Authentication was successfully disabled.",
18 | "setup": {
19 | "title": "Two Factor Authentication Setup",
20 | "description": "Scan the QR code below with your Google Authenticator app. Enter the generated code in the field below to verify whether your 2FA code was correctly set up.",
21 | "step-1": "Step 1. Scan code",
22 | "step-2": "Step 2. Verify token",
23 | "verification-placeholder": "One Time Password Token",
24 | "verify": "Verify & activate 2FA"
25 | },
26 | "disable": {
27 | "title": "Disable Two Factor Authentication",
28 | "description": "Please verify that it is you by confirming your password.",
29 | "password": "Password",
30 | "submit": "Confirm password & disable Two Factor Authentication"
31 | },
32 | "error": {
33 | "not-enabled": "2FA is not enabled for this Sales Channel.",
34 | "no-customer": "Customer not found in session.",
35 | "incorrect-password": "The provided password was wrong. Two Factor Authentication is not disabled.",
36 | "empty-input": "Secret or code was empty.",
37 | "incorrect-code": "The provided code was not correct."
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Resources/snippet/messages.de-DE.json:
--------------------------------------------------------------------------------
1 | {
2 | "rl-2fa": {
3 | "login": {
4 | "title": "Zwei-Faktor-Authentifizierung",
5 | "description": "Bitte bestätigen Sie die Identität mit Ihrer 2FA-App.",
6 | "code-placeholder": "Code",
7 | "proceed": "Anmelden",
8 | "cancel": "Abbrechen"
9 | },
10 | "account": {
11 | "title": "Zwei-Faktor-Authentifizierung",
12 | "active": "Sehr gut! Ihr Account ist zur Zeit durch 2FA gesichert!",
13 | "not-active": "Ihr Account ist zur Zeit nicht durch 2FA gesichert!",
14 | "description": "Stellen Sie sicher, dass Ihr Webshop-Zugang geschützt wird, indem Sie die Zwei-Faktor-Authentifizierung (2FA) aktivieren. Mit 2FA wird Ihr Account zusätzlich zu dem Benutzernamen und Passwort durch ein neues Einmal-Passwort geschützt. Dieser zweite Schritt dient dazu, Ihre Identität zu bestätigen und verbindet hierfür Ihr Smartphone oder App mit Ihrem Account. Klicken Sie auf den nachfolgenden Button, um 2FA für Ihren Account zu aktivieren. Es ist einfach und kostenlos!",
15 | "start-setup": "2FA für Ihren Account einrichten",
16 | "disable-2fa": "Zwei-Faktor-Authentifizierung deaktivieren",
17 | "disabled-2fa": "Zwei-Faktor-Authentifizierung wurde erfolgreich deaktiviert.",
18 | "setup": {
19 | "title": "Konfiguration Zwei-Faktor-Authentifizierung",
20 | "description": "Scannen Sie den Code mit Ihrer 2FA-App. Tragen Sie den generierten Code in das Feld ein, um zu Verifizieren, dass 2FA korrekt konfiguriert wurde.",
21 | "step-1": "Schritt 1. Code einscannen",
22 | "step-2": "Schritt 2. Token verifizieren",
23 | "verification-placeholder": "Einmal-Passwort-Token",
24 | "verify": "Verifizieren & 2FA aktivieren"
25 | },
26 | "disable": {
27 | "title": "Zwei-Faktor-Authentifizierung deaktivieren",
28 | "description": "Bitte bestätigen Sie Ihre Identität durch Eingabe Ihres Passworts.",
29 | "password": "Passwort",
30 | "submit": "Eingabe bestätigen & 2FA deaktivieren"
31 | },
32 | "error": {
33 | "not-enabled": "2FA ist für diesen Shop nicht aktiviert.",
34 | "no-customer": "Kunde wurde nicht gefunden.",
35 | "incorrect-password": "Das eingegebene Passwort war nicht korrekt. Zwei-Faktor-Authentifizierung wurde nicht deaktiviert.",
36 | "empty-input": "Eingabe war leer.",
37 | "incorrect-code": "Der eingegebene Code war nicht korrekt."
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Resources/snippet/messages.nl-NL.json:
--------------------------------------------------------------------------------
1 | {
2 | "rl-2fa": {
3 | "login": {
4 | "title": "Twee-factorenauthenticatie",
5 | "description": "Verifieer uw identiteit met behulp van uw 2FA-app.",
6 | "code-placeholder": "Code",
7 | "proceed": "Inloggen",
8 | "cancel": "Annuleren"
9 | },
10 | "account": {
11 | "title": "Twee-factorenauthenticatie",
12 | "active": "Goed! Je account is momenteel beschermd met twee-factorenauthenticatie!",
13 | "not-active": "Je account is momenteel niet beschermd met twee-factorenauthenticatie!",
14 | "description": "Zorg ervoor dat uw webwinkelaccount beveiligd is door het toevoegen van Two Factor Authentication (2FA). Met 2FA is je account in meerdere lagen beveiligd: je gebruikersnaam, je wachtwoord en een nieuwe 'one time password' laag. Deze tweede stap wordt toegevoegd om je identiteit opnieuw te bevestigen en koppelt je account aan je mobiele telefoon. Klik op de knop hieronder om te beginnen met het instellen van 2FA voor uw account. Het is eenvoudig en gratis!",
15 | "start-setup": "Stel 2FA in voor mijn account",
16 | "disable-2fa": "Schakel twee-factorenauthenticatie uit",
17 | "disabled-2fa": "Twee-factorenauthenticatie was succesvol uitgeschakeld.",
18 | "setup": {
19 | "title": "Twee-factorenauthenticatie opzetten",
20 | "description": "Scan de onderstaande QR-code met uw Google Authenticator-app. Voer de gegenereerde code in het onderstaande veld in om te controleren of uw 2FA correct is ingesteld.",
21 | "step-1": "Stap 1. Scan code",
22 | "step-2": "Stap 2. Verifieer token",
23 | "verification-placeholder": "One Time Password Token",
24 | "verify": "Verifieer & activeer 2FA"
25 | },
26 | "disable": {
27 | "title": "Twee-factorenauthenticatie uitschakelen",
28 | "description": "Controleer of u het bent door uw wachtwoord te bevestigen.",
29 | "password": "Watchtwoord",
30 | "submit": "Bevestig wachtwoord en schakel twee-factorenauthenticatie uit"
31 | },
32 | "error": {
33 | "not-enabled": "2FA is niet ingeschakeld voor dit verkoopkanaal.",
34 | "no-customer": "Klant niet gevonden in sessie.",
35 | "incorrect-password": "Het verstrekte wachtwoord was verkeerd. Twee-factorenauthenticatie is niet uitgeschakeld.",
36 | "empty-input": "Geheim of code was leeg.",
37 | "incorrect-code": "De verstrekte code was niet correct."
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/component/rl-user-otp/index.js:
--------------------------------------------------------------------------------
1 | import template from './rl-user-otp.html.twig';
2 | import './rl-user-otp.scss';
3 |
4 | /**
5 | * @component-example
6 | *
7 | */
8 | export default {
9 | template,
10 |
11 | inject: ['rl2faService'],
12 |
13 | props: {
14 | user: {
15 | type: Object,
16 | required: true,
17 | },
18 | isLoading: {
19 | type: Boolean,
20 | required: true,
21 | },
22 | onSave: {
23 | type: Function,
24 | required: true,
25 | },
26 | },
27 |
28 | data() {
29 | return {
30 | httpClient: null,
31 | isLoading2Fa: false,
32 | generatedSecret: null,
33 | generatedSecretUrl: null,
34 | oneTimePassword: '',
35 | oneTimePasswordError: '',
36 | };
37 | },
38 |
39 | created() {
40 | this.syncService = Shopware.Service('syncService');
41 | this.httpClient = this.syncService.httpClient;
42 | },
43 |
44 | methods: {
45 | generateSecret() {
46 | this.isLoading2Fa = true;
47 |
48 | this.rl2faService.getSecret(this.user.username).then((response) => {
49 | this.isLoading2Fa = false;
50 | this.generatedSecret = response.secret;
51 | this.generatedSecretUrl = response.qrUrl;
52 | });
53 | },
54 |
55 | validateAndSaveOneTimePassword() {
56 | this.isLoading2Fa = true;
57 |
58 | this.rl2faService
59 | .validateSecret(this.generatedSecret, this.oneTimePassword)
60 | .then((response) => {
61 | this.isLoading2Fa = false;
62 | if (response.status === 'OK') {
63 | this.saveOneTimePassword();
64 | }
65 | })
66 | .catch((error) => {
67 | this.isLoading2Fa = false;
68 | this.oneTimePasswordError = error.response.data.error;
69 | });
70 | },
71 |
72 | saveOneTimePassword() {
73 | if (!this.user.customFields) {
74 | this.user.customFields = {};
75 | }
76 |
77 | this.user.customFields.rl_2fa_secret = this.generatedSecret;
78 | this.onSave();
79 | },
80 |
81 | disable2FA() {
82 | if (!this.user.customFields) {
83 | this.user.customFields = {};
84 | }
85 |
86 | this.user.customFields.rl_2fa_secret = '';
87 | this.onSave();
88 | },
89 | },
90 | };
91 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/override/sw-login/view/sw-login-login/index.js:
--------------------------------------------------------------------------------
1 | import template from './sw-login-login.html.twig';
2 |
3 | const { Context, Application } = Shopware;
4 |
5 | export default {
6 | template,
7 |
8 | data() {
9 | return {
10 | rememberOtpPassword: '',
11 | showOtpForm: false,
12 | otp: '',
13 | };
14 | },
15 |
16 | methods: {
17 | loginUserWithPasswordAndOtp() {
18 | this.$emit('is-loading');
19 |
20 | return this.loginWithOtp(this.username, this.password, this.otp)
21 | .then(() => {
22 | this.handleLoginSuccess();
23 | this.$emit('is-not-loading');
24 | })
25 | .catch((response) => {
26 | this.password = '';
27 | this.otp = '';
28 | this.showOtpForm = false;
29 |
30 | this.handleLoginError(response);
31 | this.$emit('is-not-loading');
32 | });
33 | },
34 |
35 | loginWithOtp(user, pass, otp) {
36 | return Application.getContainer('init')
37 | .httpClient.post(
38 | '/oauth/token',
39 | {
40 | grant_type: 'password',
41 | client_id: 'administration',
42 | scopes: 'write',
43 | username: user,
44 | password: pass,
45 | rl_2fa_otp: otp,
46 | },
47 | {
48 | baseURL: Context.api.apiPath,
49 | }
50 | )
51 | .then((response) => {
52 | const auth = this.loginService.setBearerAuthentication({
53 | access: response.data.access_token,
54 | refresh: response.data.refresh_token,
55 | expiry: response.data.expires_in,
56 | });
57 |
58 | window.localStorage.setItem('redirectFromLogin', 'true');
59 |
60 | return auth;
61 | });
62 | },
63 |
64 | loginUserWithPassword() {
65 | this.rememberOtpPassword = this.password;
66 | this.$super('loginUserWithPassword');
67 | },
68 |
69 | handleLoginError(error) {
70 | if (error.response.data.errors[0].detail !== 'request-otp') {
71 | this.$super('handleLoginError', error);
72 | return;
73 | }
74 |
75 | this.password = this.rememberOtpPassword;
76 | this.showOtpForm = true;
77 | },
78 | },
79 | };
80 |
--------------------------------------------------------------------------------
/src/Resources/snippet/messages.pl-PL.json:
--------------------------------------------------------------------------------
1 | {
2 | "rl-2fa": {
3 | "login": {
4 | "title": "Uwierzytelnianie dwuskładnikowe",
5 | "description": "Zweryfikuj swoją tożsamość za pomocą aplikacji 2FA.",
6 | "code-placeholder": "Kod",
7 | "proceed": "Zaloguj się",
8 | "cancel": "Anuluj"
9 | },
10 | "account": {
11 | "title": "Uwierzytelnianie dwuskładnikowe",
12 | "active": "Świetnie! Twoje konto jest obecnie chronione uwierzytelnianiem dwuskładnikowym!",
13 | "not-active": "Twoje konto nie jest obecnie chronione uwierzytelnianiem dwuskładnikowym!",
14 | "description": "Upewnij się, że Twoje konto w sklepie internetowym jest bezpiecznie chronione, dodając uwierzytelnianie dwuskładnikowe (2FA). Dzięki 2FA Twoje konto jest zabezpieczone na wielu poziomach: nazwa użytkownika, hasło i nowa warstwa „jednorazowego hasła”. Ten drugi krok jest dodawany w celu ponownego potwierdzenia Twojej tożsamości i łączy Twoje konto z telefonem komórkowym. Kliknij przycisk poniżej, aby rozpocząć konfigurację 2FA dla swojego konta. To łatwe i bezpłatne!",
15 | "start-setup": "Skonfiguruj 2FA dla mojego konta",
16 | "disable-2fa": "Wyłącz uwierzytelnianie dwuskładnikowe",
17 | "disabled-2fa": "Uwierzytelnianie dwuskładnikowe zostało pomyślnie wyłączone.",
18 | "setup": {
19 | "title": "Konfiguracja uwierzytelniania dwuskładnikowego",
20 | "description": "Zeskanuj poniższy kod QR za pomocą aplikacji Google Authenticator. Wprowadź wygenerowany kod w polu poniżej, aby sprawdzić, czy kod 2FA został poprawnie skonfigurowany.",
21 | "step-1": "Krok 1. Zeskanuj kod",
22 | "step-2": "Krok 2. Zweryfikuj token",
23 | "verification-placeholder": "Token jednorazowego hasła",
24 | "verify": "Zweryfikuj i aktywuj 2FA"
25 | },
26 | "disable": {
27 | "title": "Wyłącz uwierzytelnianie dwuskładnikowe",
28 | "description": "Potwierdź swoją tożsamość, wprowadzając hasło.",
29 | "password": "Hasło",
30 | "submit": "Potwierdź hasło i wyłącz uwierzytelnianie dwuskładnikowe"
31 | },
32 | "error": {
33 | "not-enabled": "2FA nie jest włączone dla tego kanału sprzedaży.",
34 | "no-customer": "Klient nie znaleziony w sesji.",
35 | "incorrect-password": "Podane hasło było nieprawidłowe. Uwierzytelnianie dwuskładnikowe nie zostało wyłączone.",
36 | "empty-input": "Brak danych (Secret lub Code).",
37 | "incorrect-code": "Podany kod był nieprawidłowy."
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Resources/snippet/messages.fr-FR.json:
--------------------------------------------------------------------------------
1 | {
2 | "rl-2fa": {
3 | "login": {
4 | "title": "Authentification à deux facteurs (2FA)",
5 | "description": "Veuillez vérifier votre identité à l'aide de votre application 2FA.",
6 | "code-placeholder": "Code",
7 | "proceed": "Se connecter",
8 | "cancel": "Annuler"
9 | },
10 | "account": {
11 | "title": "Authentification à deux facteurs (2FA)",
12 | "active": "Félicitations! Votre compte est actuellement protégé par l'authentification à deux facteurs!",
13 | "not-active": "Votre compte n'est actuellement pas protégé par l'authentification à deux facteurs!",
14 | "description": "Assurez-vous que votre compte de boutique en ligne est protégé en toute sécurité en ajoutant l'authentification à deux facteurs (2FA). Avec 2FA, votre compte est sécurisé en plusieurs couches: votre nom d'utilisateur, votre mot de passe et une nouvelle couche de «mot de passe à usage unique». Cette deuxième étape est ajoutée pour reconfirmer votre identité et associe votre compte à votre téléphone mobile. Cliquez sur le bouton ci-dessous pour commencer à configurer 2FA pour votre compte. C'est facile et gratuit!",
15 | "start-setup": "Configurer 2FA pour mon compte",
16 | "disable-2fa": "Désactiver l'authentification à deux facteurs",
17 | "disabled-2fa": "L'authentification à deux facteurs a été désactivée avec succès.",
18 | "setup": {
19 | "title": "Configurer 2FA pour mon compte",
20 | "description": "Scannez le code QR ci-dessous avec votre application Google Authenticator. Entrez le code généré dans le champ ci-dessous pour vérifier si votre code 2FA a été correctement configuré.",
21 | "step-1": "Étape 1. Scannez le code",
22 | "step-2": "Étape 2. Vérifier le jeton",
23 | "verification-placeholder": "Jeton de mot de passe unique (OTP)",
24 | "verify": "Vérifier & activer 2FA"
25 | },
26 | "disable": {
27 | "title": "Désactiver l'authentification à deux facteurs",
28 | "description": "Veuillez vérifier qu'il s'agit bien de vous en confirmant votre mot de passe.",
29 | "password": "Mot de passe",
30 | "submit": "Confirmer le mot de passe et désactiver l'authentification à deux facteurs"
31 | },
32 | "error": {
33 | "not-enabled": "2FA n'est pas activé pour ce canal de vente.",
34 | "no-customer": "Client introuvable dans la session.",
35 | "incorrect-password": "Le mot de passe fourni était incorrect. L'authentification à deux facteurs n'est pas désactivée.",
36 | "empty-input": "Le secret ou le code était vide.",
37 | "incorrect-code": "Le code fourni n'était pas correct."
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Resources/views/storefront/page/account/profile/2fa/setup.html.twig:
--------------------------------------------------------------------------------
1 | {% block page_account_profile_2fa_setup %}
2 | {% block page_account_profile_2fa_setup_title %}
3 | {{ "rl-2fa.account.setup.title"|trans|sw_sanitize }}
4 | {% endblock %}
5 |
6 | {% block page_account_profile_2fa_setup_description %}
7 |
8 | {{ "rl-2fa.account.setup.description"|trans|sw_sanitize }}
9 |
10 | {% endblock %}
11 |
12 | {% block page_account_profile_2fa_setup_step1 %}
13 |
14 |
{{ "rl-2fa.account.setup.step-1"|trans|sw_sanitize }}
15 |
16 |
17 |

18 |
{{ secret }}
19 |
20 |
21 |
22 | {% endblock %}
23 |
24 | {% block page_account_profile_2fa_setup_step2 %}
25 |
26 | {% block page_account_profile_2fa_setup_step2_title %}
27 |
{{ "rl-2fa.account.setup.step-2"|trans|sw_sanitize }}
28 | {% endblock %}
29 |
30 |
31 |
32 |
33 | {% sw_include '@Storefront/storefront/utilities/alert.html.twig' with {
34 | type: "warning",
35 | content: ""
36 | } %}
37 |
38 |
39 |
40 | {% block page_account_profile_2fa_setup_step2_input %}
41 |
48 | {% endblock %}
49 |
50 |
51 |
52 |
53 | {% block page_account_profile_2fa_setup_step2_button %}
54 |
57 | {% endblock %}
58 |
59 |
60 | {% endblock %}
61 | {% endblock %}
62 |
--------------------------------------------------------------------------------
/src/Controller/TwoFactorAuthenticationApiController.php:
--------------------------------------------------------------------------------
1 | ['api']])]
22 | class TwoFactorAuthenticationApiController extends AbstractController
23 | {
24 | public function __construct(
25 | #[Autowire(service: 'RuneLaenen\TwoFactorAuth\Service\TimebasedOneTimePasswordService')]
26 | private readonly TimebasedOneTimePasswordServiceInterface $totpService,
27 | #[Autowire(service: 'Shopware\Storefront\Framework\Routing\Router')]
28 | private readonly RouterInterface $router,
29 | private readonly ConfigurationService $configurationService,
30 | ) {
31 | }
32 |
33 | #[Route(path: '/generate-secret', name: 'api.action.rl-2fa.generate-secret', methods: ['GET'])]
34 | public function generateSecret(Request $request): JsonResponse
35 | {
36 | $company = $this->configurationService->getAdministrationCompany(
37 | $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_ID)
38 | );
39 |
40 | $secret = $this->totpService->createSecret();
41 | $qrUrl = $this->totpService->getQrCodeUrl(
42 | $company,
43 | $request->get('holder', ''),
44 | $secret
45 | );
46 |
47 | return new JsonResponse([
48 | 'secret' => $secret,
49 | 'qrUrl' => $this->router->generate(
50 | 'rl-2fa.qr-code.secret',
51 | ['qrUrl' => $qrUrl],
52 | UrlGeneratorInterface::ABSOLUTE_URL
53 | ),
54 | ]);
55 | }
56 |
57 | #[Route(path: '/validate-secret', name: 'api.action.rl-2fa.validate-secret', methods: ['POST'])]
58 | public function validateSecret(Request $request): JsonResponse
59 | {
60 | if (empty($request->get('secret')) || empty($request->get('code'))) {
61 | return new JsonResponse([
62 | 'status' => 'error',
63 | 'error' => 'Secret or code empty',
64 | ], 400);
65 | }
66 |
67 | $verified = $this->totpService->verifyCode((string) $request->get('secret'), (string) $request->get('code'));
68 | if ($verified) {
69 | return new JsonResponse(['status' => 'OK']);
70 | }
71 |
72 | return new JsonResponse([
73 | 'status' => 'error',
74 | 'error' => 'Secret and code not correct',
75 | ], Response::HTTP_BAD_REQUEST);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Resources/app/storefront/src/plugin/rl2fa-verification.plugin.js:
--------------------------------------------------------------------------------
1 | import ElementLoadingIndicatorUtil from 'src/utility/loading-indicator/element-loading-indicator.util';
2 |
3 | export default class Rl2faVerificationPlugin extends window.PluginBaseClass {
4 | static options = {
5 | /** Selector for the submit button of the verification step */
6 | buttonSelector: '.account-profile-2fa-setup-verify',
7 |
8 | /** Selector for the code input field of the verification step */
9 | codeInputSelector: '[name=otpVerification]',
10 |
11 | /** Selector for the secret input field of the verification step */
12 | secretInputSelector: '[name=otpSecret]',
13 |
14 | /** Selector for the error message wrapper of the verification step */
15 | errorMessageWrapperSelector: '.rl2fa-setup-verification-message',
16 |
17 | /** Selector for the error message content of the verification step */
18 | errorMessageSelector:
19 | '.rl2fa-setup-verification-message .alert-content-container',
20 |
21 | /** Class to toggle visibility (display: none) */
22 | invisibleClass: 'd-none',
23 |
24 | /** Url to verify and save the account secret */
25 | verificationUrl: '',
26 | };
27 |
28 | init() {
29 | this._submitButton = this.el.querySelector(this.options.buttonSelector);
30 |
31 | this._codeInput = this.el.querySelector(this.options.codeInputSelector);
32 | this._secretInput = this.el.querySelector(
33 | this.options.secretInputSelector
34 | );
35 |
36 | this._errorMessageWrapper = this.el.querySelector(
37 | this.options.errorMessageWrapperSelector
38 | );
39 | this._errorMessage = this.el.querySelector(
40 | this.options.errorMessageSelector
41 | );
42 |
43 | this.initListeners();
44 | }
45 |
46 | initListeners() {
47 | this._submitButton.addEventListener(
48 | 'click',
49 | this.onSubmitButtonClick.bind(this)
50 | );
51 | }
52 |
53 | onSubmitButtonClick() {
54 | ElementLoadingIndicatorUtil.create(this.el);
55 | this._errorMessageWrapper.classList.add(this.options.invisibleClass);
56 |
57 | fetch(this.options.verificationUrl, {
58 | method: 'POST',
59 | headers: {
60 | 'Content-Type': 'application/json',
61 | },
62 | body: JSON.stringify({
63 | code: this._codeInput.value,
64 | secret: this._secretInput.value,
65 | }),
66 | })
67 | .then((response) => response.text())
68 | .then((response) => {
69 | const data = JSON.parse(response);
70 |
71 | if (data.status === 'OK') {
72 | window.location.reload();
73 | return;
74 | }
75 |
76 | ElementLoadingIndicatorUtil.remove(this.el);
77 |
78 | this.showErrorMessage(
79 | data.error ? data.error : 'Something went wrong!'
80 | );
81 | });
82 | }
83 |
84 | showErrorMessage(message) {
85 | this._errorMessageWrapper.classList.remove(this.options.invisibleClass);
86 | this._errorMessage.innerHTML = message;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Resources/views/storefront/page/account/profile/index.html.twig:
--------------------------------------------------------------------------------
1 | {% sw_extends '@Storefront/storefront/page/account/profile/index.html.twig' %}
2 |
3 | {% block page_account_profile_credentials %}
4 | {% block page_account_profile_2fa %}
5 | {% if config('RuneLaenenTwoFactorAuth.config.storefrontEnabled') %}
6 | {% block page_account_profile_2fa_card %}
7 |
8 |
9 | {% block page_account_profile_2fa_card_title %}
10 |
11 | {{ "rl-2fa.account.title"|trans|sw_sanitize }}
12 |
13 | {% endblock %}
14 |
15 | {% block page_account_profile_2fa_card_body %}
16 | {% if not context.customer.customFields.rl_2fa_secret or context.customer.customFields.rl_2fa_secret is empty %}
17 | {% block page_account_profile_2fa_card_not_active %}
18 | {% sw_include '@Storefront/storefront/utilities/alert.html.twig' with {
19 | type: "warning",
20 | content: "rl-2fa.account.not-active"|trans|sw_sanitize
21 | } %}
22 |
{{ "rl-2fa.account.description"|trans|sw_sanitize }}
23 |
30 | {% endblock %}
31 | {% else %}
32 | {% block page_account_profile_2fa_card_active %}
33 | {% sw_include '@Storefront/storefront/utilities/alert.html.twig' with {
34 | type: "success",
35 | content: "rl-2fa.account.active"|trans|sw_sanitize
36 | } %}
37 |
38 |
45 | {% endblock %}
46 | {% endif %}
47 | {% endblock %}
48 |
49 |
50 | {% endblock %}
51 | {% endif %}
52 | {% endblock %}
53 |
54 | {{ parent() }}
55 | {% endblock %}
56 |
--------------------------------------------------------------------------------
/src/Subscriber/ApiOauthTokenSubscriber.php:
--------------------------------------------------------------------------------
1 | 'onApiOauthTokenResponse',
35 | ];
36 | }
37 |
38 | /**
39 | * @throws OAuthServerException
40 | */
41 | public function onApiOauthTokenResponse(ResponseEvent $event): void
42 | {
43 | $request = $event->getRequest();
44 |
45 | if ($request->attributes->get('_route') !== 'api.oauth.token') {
46 | return;
47 | }
48 |
49 | if ($request->request->get('scope') === 'user-verified'
50 | || $event->getResponse()->getStatusCode() !== 200) {
51 | return;
52 | }
53 |
54 | $username = $request->request->get('username');
55 |
56 | $user = $this->userRepository->search(
57 | (new Criteria())->addFilter(new EqualsFilter('username', $username)),
58 | ContextHelper::createDefaultContext()
59 | )->first();
60 |
61 | if (!$user instanceof UserEntity
62 | || empty($user->getCustomFields()['rl_2fa_secret'])
63 | ) {
64 | return;
65 | }
66 |
67 | $otp = $request->request->get('rl_2fa_otp');
68 | if ($otp && $this->checkOtp($user->getCustomFields()['rl_2fa_secret'], $otp)) {
69 | return;
70 | }
71 |
72 | throw new OAuthServerException('This user needs an extra OTP', 1010, 'request-otp', 401, 'request-otp');
73 | }
74 |
75 | /**
76 | * @returns true if OTP is correct
77 | *
78 | * @throws OAuthServerException when the OTP is incorrect
79 | */
80 | private function checkOtp($secret, $code): bool
81 | {
82 | try {
83 | if (!$this->oneTimePasswordService->verifyCode($secret, $code)) {
84 | throw new \Exception();
85 | }
86 | } catch (\Exception $exception) {
87 | throw new OAuthServerException('Wrong OTP', 1011, 'wrong-otp', 401, null, null, $exception);
88 | }
89 |
90 | return true;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Controller/StorefrontTwoFactorAuthController.php:
--------------------------------------------------------------------------------
1 | ['storefront']])]
24 | class StorefrontTwoFactorAuthController extends StorefrontController
25 | {
26 | public function __construct(
27 | #[Autowire(service: 'RuneLaenen\TwoFactorAuth\Service\TimebasedOneTimePasswordService')]
28 | private readonly TimebasedOneTimePasswordServiceInterface $totpService,
29 | #[Autowire(service: 'event_dispatcher')]
30 | private readonly EventDispatcherInterface $dispatcher,
31 | #[Autowire(service: 'Shopware\Core\Checkout\Customer\SalesChannel\LogoutRoute')]
32 | private readonly AbstractLogoutRoute $logoutRoute,
33 | ) {
34 | }
35 |
36 | #[Route(path: '/rl-2fa/verification', name: 'frontend.rl2fa.verification', methods: ['GET', 'POST'])]
37 | public function verification(Request $request, SalesChannelContext $context): Response
38 | {
39 | $twoFactorSecret = $context->getCustomer()?->getCustomFields()['rl_2fa_secret'] ?? null;
40 |
41 | if (empty($twoFactorSecret) || !\is_string($twoFactorSecret)) {
42 | return $this->redirectToRoute('frontend.account.login.page', $request->query->all());
43 | }
44 |
45 | if ($request->getMethod() === 'POST') {
46 | $code = $request->get('otpCode');
47 |
48 | if ($this->totpService->verifyCode(
49 | $twoFactorSecret,
50 | $code
51 | )) {
52 | $this->dispatcher->dispatch(new StorefrontTwoFactorAuthEvent($context));
53 |
54 | return $this->redirectToRoute('frontend.account.home.page', $request->query->all());
55 | }
56 |
57 | $this->addFlash('danger', $this->trans('rl-2fa.account.error.incorrect-code'));
58 | }
59 |
60 | return $this->render('@RuneLaenenTwoFactorAuth/storefront/page/2fa/verification.html.twig');
61 | }
62 |
63 | #[Route(path: '/rl-2fa/verification/cancel', name: 'frontend.rl2fa.verification.cancel', methods: ['GET'])]
64 | public function cancelVerification(SalesChannelContext $context, RequestDataBag $dataBag): RedirectResponse
65 | {
66 | if ($context->getCustomer() !== null) {
67 | $this->logoutRoute->logout($context, $dataBag);
68 | }
69 | $this->dispatcher->dispatch(new StorefrontTwoFactorCancelEvent($context));
70 |
71 | return $this->redirectToRoute('frontend.account.login.page');
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Resources/app/administration/src/component/rl-user-otp/rl-user-otp.html.twig:
--------------------------------------------------------------------------------
1 | {% block rl_user_otp_user_card %}
2 |
6 |
7 |
14 | {{ $tc('rl-2fa.settings.user-detail.enabled.description') }}
15 |
16 |
21 | {{ $tc('rl-2fa.settings.user-detail.enabled.disable') }}
22 |
23 |
24 |
25 |
32 | {{ $tc('rl-2fa.settings.user-detail.not-enabled.description') }}
33 |
34 |
39 | {{ $tc('rl-2fa.settings.user-detail.not-enabled.get-started') }}
40 |
41 |
42 |
43 |
44 |
45 |
![]()
46 |
{{ generatedSecret }}
47 |
48 |
49 |
50 | {{ $tc('rl-2fa.settings.user-detail.generating.scan-code') }}
51 |
52 |
53 |
54 | {{ $tc('rl-2fa.settings.user-detail.generating.description') }}
55 |
56 |
57 |
63 | {{ oneTimePasswordError }}
64 |
65 |
71 |
75 | {{ $tc('rl-2fa.settings.user-detail.generating.validate-save') }}
76 |
77 |
78 |
79 |
80 |
81 | {% endblock %}
--------------------------------------------------------------------------------
/src/Subscriber/CustomerLoginSubscriber.php:
--------------------------------------------------------------------------------
1 | 'onCustomerLoginEvent',
36 | ControllerEvent::class => 'onController',
37 | StorefrontTwoFactorAuthEvent::class => 'removeSession',
38 | StorefrontTwoFactorCancelEvent::class => 'removeSession',
39 | ];
40 | }
41 |
42 | public function onController(ControllerEvent $event): void
43 | {
44 | if (!$this->requestStack->getSession()->has(self::SESSION_NAME)) {
45 | return;
46 | }
47 |
48 | if (!$event->isMainRequest()) {
49 | return;
50 | }
51 |
52 | if ($event->getRequest()->isXmlHttpRequest()) {
53 | return;
54 | }
55 |
56 | if (!$event->getRequest()->attributes->get(SalesChannelRequest::ATTRIBUTE_IS_SALES_CHANNEL_REQUEST)) {
57 | return;
58 | }
59 |
60 | if ($event->getRequest()->attributes->get('_esi') === true) {
61 | return;
62 | }
63 |
64 | if ($this->isVerificationRoute($event)) {
65 | return;
66 | }
67 |
68 | $queries = $event->getRequest()->query;
69 | $parameters = [];
70 |
71 | if ($queries->has('redirectTo')) {
72 | $parameters['redirect'] = $queries->all();
73 | }
74 |
75 | $url = $this->router->generate('frontend.rl2fa.verification', $parameters);
76 |
77 | $response = new RedirectResponse($url);
78 |
79 | $response->send();
80 | }
81 |
82 | public function onCustomerLoginEvent(CustomerLoginEvent $event): void
83 | {
84 | if (empty($event->getCustomer()->getCustomFields()['rl_2fa_secret'] ?? null)) {
85 | return;
86 | }
87 |
88 | $this->requestStack->getSession()->set(self::SESSION_NAME, true);
89 | }
90 |
91 | public function removeSession(): void
92 | {
93 | $this->requestStack->getSession()->remove(self::SESSION_NAME);
94 | }
95 |
96 | private function isVerificationRoute(ControllerEvent $event): bool
97 | {
98 | $route = (string) $event->getRequest()->attributes->get('_route');
99 |
100 | return \in_array($route, ['frontend.rl2fa.verification', 'frontend.rl2fa.verification.cancel'], true);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/Resources/views/storefront/page/2fa/verification.html.twig:
--------------------------------------------------------------------------------
1 | {% sw_extends '@Storefront/storefront/base.html.twig' %}
2 |
3 | {% block base_scroll_up %}{% endblock %}
4 |
5 | {% block base_body_classes %}{{ parent() }} d-flex justify-content-center{% endblock %}
6 |
7 | {% block base_body_inner %}
8 | {% block page_2fa_verification %}
9 |
10 |
11 | {% block page_2fa_verification_header %}
12 |
13 | {% sw_include '@Storefront/storefront/layout/header/logo.html.twig' %}
14 |
15 | {% endblock %}
16 |
17 |
18 | {% block page_2fa_verification_card_header %}
19 |
22 | {% endblock %}
23 | {% block page_2fa_verification_card_body %}
24 |
25 | {% block page_2fa_verification_card_body_description %}
26 |
27 | {{ "rl-2fa.login.description"|trans|sw_sanitize }}
28 |
29 | {% endblock %}
30 |
31 | {% block page_2fa_verification_card_body_flashbag %}
32 |
33 | {% for type, messages in app.flashes %}
34 | {% sw_include '@Storefront/storefront/utilities/alert.html.twig' with { type: type, list: messages } %}
35 | {% endfor %}
36 |
37 | {% endblock %}
38 |
39 | {% block page_2fa_verification_card_body_form %}
40 |
66 | {% endblock %}
67 |
68 | {% endblock %}
69 |
70 |
71 |
72 | {% endblock %}
73 | {% endblock %}
74 |
--------------------------------------------------------------------------------
/src/Controller/TwoFactorAuthenticationController.php:
--------------------------------------------------------------------------------
1 | ['storefront']])]
26 | class TwoFactorAuthenticationController extends StorefrontController
27 | {
28 | public function __construct(
29 | private readonly ConfigurationService $configurationService,
30 | #[Autowire(service: TimebasedOneTimePasswordService::class)]
31 | private readonly TimebasedOneTimePasswordServiceInterface $totpService,
32 | #[Autowire(service: Router::class)]
33 | private readonly RouterInterface $router,
34 | #[Autowire(service: 'customer.repository')]
35 | private readonly EntityRepository $customerRepository,
36 | private readonly LegacyPasswordVerifier $legacyPasswordVerifier,
37 | ) {
38 | }
39 |
40 | #[Route(
41 | path: '/rl-2fa/profile/setup',
42 | name: 'widgets.rl-2fa.profile.setup',
43 | defaults: ['XmlHttpRequest' => true],
44 | methods: ['GET'],
45 | )]
46 | public function profileSetup(SalesChannelContext $salesChannelContext): Response
47 | {
48 | $customer = $salesChannelContext->getCustomer();
49 | $salesChannelId = $salesChannelContext->getSalesChannelId();
50 |
51 | if ($customer === null || !$this->configurationService->isStorefrontEnabled($salesChannelId)) {
52 | return new Response();
53 | }
54 |
55 | $company = $this->configurationService->getStorefrontCompany($salesChannelId);
56 | $secret = $this->totpService->createSecret();
57 | $qrUrl = $this->totpService->getQrCodeUrl(
58 | $company,
59 | $customer->getFirstName() . ' ' . $customer->getLastName(),
60 | $secret
61 | );
62 |
63 | return $this->renderStorefront('@Storefront/storefront/page/account/profile/2fa/setup.html.twig', [
64 | 'secret' => $secret,
65 | 'qrUrl' => $this->router->generate(
66 | 'rl-2fa.qr-code.secret',
67 | ['qrUrl' => $qrUrl],
68 | UrlGeneratorInterface::ABSOLUTE_URL
69 | ),
70 | ]);
71 | }
72 |
73 | #[Route(
74 | path: '/rl-2fa/profile/disable',
75 | name: 'widgets.rl-2fa.profile.disable',
76 | defaults: ['XmlHttpRequest' => true],
77 | methods: ['GET'],
78 | )]
79 | public function profileDisable(SalesChannelContext $salesChannelContext): Response
80 | {
81 | $salesChannelId = $salesChannelContext->getSalesChannelId();
82 |
83 | if (!$this->configurationService->isStorefrontEnabled($salesChannelId)) {
84 | return new Response();
85 | }
86 |
87 | return $this->renderStorefront('@Storefront/storefront/page/account/profile/2fa/disable.html.twig');
88 | }
89 |
90 | #[Route(
91 | path: '/rl-2fa/profile/disable',
92 | name: 'widgets.rl-2fa.profile.disable.post',
93 | defaults: ['XmlHttpRequest' => true],
94 | methods: ['POST'],
95 | )]
96 | public function profileDisablePost(Request $request, SalesChannelContext $salesChannelContext): Response
97 | {
98 | if (!$this->configurationService->isStorefrontEnabled($salesChannelContext->getSalesChannelId())) {
99 | $this->addFlash('danger', $this->trans('rl-2fa.account.error.not-enabled'));
100 |
101 | return $this->redirectToRoute('frontend.account.profile.page');
102 | }
103 |
104 | $customer = $salesChannelContext->getCustomer();
105 | $password = $request->get('otpPassword');
106 | if (!$customer) {
107 | $this->addFlash('danger', $this->trans('rl-2fa.account.error.no-customer'));
108 |
109 | return $this->redirectToRoute('frontend.account.profile.page');
110 | }
111 |
112 | if ($customer->hasLegacyPassword()) {
113 | if (!$this->legacyPasswordVerifier->verify($password, $customer)) {
114 | $this->addFlash('danger', $this->trans('rl-2fa.account.error.incorrect-password'));
115 |
116 | return $this->redirectToRoute('frontend.account.profile.page');
117 | }
118 | } else {
119 | if (!password_verify($password, $customer->getPassword())) {
120 | $this->addFlash('danger', $this->trans('rl-2fa.account.error.incorrect-password'));
121 |
122 | return $this->redirectToRoute('frontend.account.profile.page');
123 | }
124 | }
125 |
126 | $this->customerRepository->update([
127 | [
128 | 'id' => $customer->getId(),
129 | 'customFields' => [
130 | 'rl_2fa_secret' => '',
131 | ],
132 | ],
133 | ], $salesChannelContext->getContext());
134 |
135 | $this->addFlash('info', $this->trans('rl-2fa.account.disabled-2fa'));
136 |
137 | return $this->redirectToRoute('frontend.account.profile.page');
138 | }
139 |
140 | #[Route(
141 | path: '/rl-2fa/profile/validate',
142 | name: 'widgets.rl-2fa.profile.validate',
143 | defaults: ['XmlHttpRequest' => true],
144 | methods: ['POST'],
145 | )]
146 | public function validateSecret(Request $request, SalesChannelContext $salesChannelContext): Response
147 | {
148 | if (!$this->configurationService->isStorefrontEnabled($salesChannelContext->getSalesChannel()->getId())) {
149 | return new JsonResponse([
150 | 'status' => 'error',
151 | 'error' => $this->trans('rl-2fa.account.error.not-enabled'),
152 | ], 400);
153 | }
154 |
155 | if (!$salesChannelContext->getCustomer()) {
156 | return new JsonResponse([
157 | 'status' => 'error',
158 | 'error' => $this->trans('rl-2fa.account.error.no-customer'),
159 | ], 400);
160 | }
161 |
162 | if (empty($request->get('secret')) || empty($request->get('code'))) {
163 | return new JsonResponse([
164 | 'status' => 'error',
165 | 'error' => $this->trans('rl-2fa.account.error.empty-input'),
166 | ], 400);
167 | }
168 |
169 | $verified = $this->totpService->verifyCode((string) $request->get('secret'), (string) $request->get('code'));
170 | if ($verified) {
171 | $this->customerRepository->update([
172 | [
173 | 'id' => $salesChannelContext->getCustomer()->getId(),
174 | 'customFields' => [
175 | 'rl_2fa_secret' => (string) $request->get('secret'),
176 | ],
177 | ],
178 | ], $salesChannelContext->getContext());
179 |
180 | return new JsonResponse(['status' => 'OK']);
181 | }
182 |
183 | return new JsonResponse([
184 | 'status' => 'error',
185 | 'error' => $this->trans('rl-2fa.account.error.incorrect-code'),
186 | ]);
187 | }
188 | }
189 |
--------------------------------------------------------------------------------