Skip to content

Commit

Permalink
OTP (#493)
Browse files Browse the repository at this point in the history
dergel authored Nov 15, 2024
1 parent 57d9886 commit e831c6e
Showing 2,268 changed files with 6,880 additions and 268,862 deletions.
22 changes: 17 additions & 5 deletions boot.php
Original file line number Diff line number Diff line change
@@ -5,18 +5,30 @@
* @psalm-scope-this rex_addon
*/

// include __DIR__.'/vendor/guzzlehttp/promises/src/functions_include.php';
// include __DIR__.'/vendor/guzzlehttp/guzzle/src/functions_include.php';
//
rex_ycom_auth::addInjection(new rex_ycom_injection_otp(), 1);
rex_ycom_auth::addInjection(new rex_ycom_injection_passwordchange(), 4);
rex_ycom_auth::addInjection(new rex_ycom_injection_termsofuse(), 8);

if (rex::isBackend()) {
rex_extension::register('PACKAGES_INCLUDED', static function ($params) {
$addon = rex_addon::get('yform');
$plugin = rex_plugin::get('yform', 'manager');

if ($plugin->isAvailable()) {
// YForm <= 5
$pages = $plugin->getProperty('pages');
$ycom_tables = rex_ycom::getTables();

if (isset($pages) && is_array($pages)) {
foreach ($pages as $page) {
if (in_array($page->getKey(), $ycom_tables, true)) {
$page->setBlock('ycom');
// $page->setRequiredPermissions('ycom[]');
}
}
}
} else {
// YForm >= 5
$pages = $addon->getProperty('pages');
$ycom_tables = rex_ycom::getTables();
if (isset($pages) && is_array($pages)) {
foreach ($pages as $page) {
if (in_array($page->getKey(), $ycom_tables, true)) {
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -4,7 +4,8 @@
"onelogin/php-saml": "^3",
"apereo/phpcas": "^1",
"league/oauth2-client": "^2",
"psr/log": "^1"
"psr/log": "^1",
"spomky-labs/otphp": "^11.0"
},
"replace": {
"psr/container": "*",
731 changes: 474 additions & 257 deletions composer.lock

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions docs/03_login_logout_profile_register.md
Original file line number Diff line number Diff line change
@@ -166,6 +166,19 @@ ersetzt werden durch:
hidden|termsofuse_accepted|1
```

Man kann auch die Nutzungsbedingungen in einem eigenen Artikel hinterlegen und diesen dann erneut abfragen, z. B. wenn diese sich geändert haben.

```php
objparams|form_showformafterupdate|0

ycom_auth_load_user|userinfo|email,termsofuse_accepted
hidden|termsofuse_accepted|1
php|termsofusecheck|phplabel|<?php if (rex::isFrontend() && rex_ycom_auth::getUser()->getValue('termsofuse_accepted') == 1) { rex_response::sendRedirect('/'); } ?>
html|termofuseinfo|{{ termsandconditions }}
action|showtext|<div class="alert alert-success">{{ termofuse_accepted }}</div>|||1
action|ycom_auth_db
```

#### E-Mail-Template `access_request_de` für die Bestätigung erstellen

Diese E-Mail fordert den Nutzer dazu auf, die Anmeldung zu bestätigen. Der endgültige Link sieht bspw. aus wie folgt: <code>https://www.redaxo.org/anmeldung/bestaetigen/?rex_ycom_activation_key=ACTIVATION_KEY&rex_ycom_id=YCOM_LOGIN</code>
54 changes: 54 additions & 0 deletions docs/10_otp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# OTP (OneTimePassword) Authentifizierung (2FA)

## Allgemeine Konfiguration

Zunächst muss die Konfiguration in den YCom Einstellungen vorgenommen werden. Die entsprechenden Artikel für das OTP Setup und die OTP Überprüfung müssen angelegt werden und über die YCom Permission nur für eingeloggte User verfügbar gemacht werden.

## Benötigte Artikel

Es wird 1 REDAXO Artikel benötigt. Dieser führt das initale SetUp und auch die Verifizierung durch. Dieser Artikel muss in der Einstellungseite verlinkt sein

### Artikel für das OTP Setup

Im OTP Artikel das YForm Builder Modul verwenden und diesen YFormCode einsetzen. Rechte müssen auf "Zugriff für eingeloggte User" gesetzt sein.

```
ycom_auth_otp|setup
```

## Einleitung

Die 2-Faktor-Authentifizierung (2FA) ist eine zusätzliche Sicherheitsebene für Ihr Konto. Sie schützt Ihr Konto vor unbefugtem Zugriff, selbst wenn Ihr Passwort kompromittiert wurde.

Die 2FA ist eine Authentifizierungsmethode, bei der zwei verschiedene Faktoren verwendet werden, um die Identität einer Person zu bestätigen.

Hier werden 2 Möglichkeiten angeboten.

* One Time Passwort über E-Mail.
* One Time Passwort über (Google) Authenticator.

Die 2FA ist eine der besten Möglichkeiten, um Ihr Konto zu schützen, da sie sicherstellt, dass nur Sie auf Ihr Konto zugreifen können, selbst wenn jemand Ihr Passwort kennt.

## Einrichtung

Die 2FA kann in Ihrem Konto aktiviert werden. Dazu müssen Sie sich zunächst anmelden und die 2FA in den Einstellungen aktivieren. Anschließend müssen Sie einen Sicherheitsschlüssel hinzufügen, der zur Authentifizierung verwendet wird.

Die 2FA kann auf verschiedene Arten aktiviert werden, z.B. durch die Eingabe eines Codes, der per SMS oder E-Mail gesendet wird, oder durch die Verwendung einer Authentifizierungs-App wie Google Authenticator oder Authy.

## Verwendung

Nachdem die 2FA aktiviert wurde, müssen Sie sich bei jedem Anmeldeversuch zusätzlich zur Eingabe Ihres Passworts auch mit einem Sicherheitsschlüssel authentifizieren. Dies kann z.B. durch die Eingabe eines Codes aus der Authentifizierungs-App erfolgen.

Die 2FA bietet eine zusätzliche Sicherheitsebene für Ihr Konto und schützt es vor unbefugtem Zugriff. Wir empfehlen daher, die 2FA zu aktivieren, um Ihr Konto zu schützen.

## Umsetzung in REDAXO

In REDAXO kann die 2FA z.B. mit dem AddOn YCom umgesetzt werden. Dazu müssen Sie zunächst die 2FA in den Einstellungen von YCom aktivieren und einen Sicherheitsschlüssel hinzufügen. Anschließend müssen Sie sich bei jedem Anmeldeversuch zusätzlich zur Eingabe Ihres Passworts auch mit einem Sicherheitsschlüssel authentifizieren.

Die 2FA bietet eine zusätzliche Sicherheitsebene für Ihr REDAXO-Konto und schützt es vor unbefugtem Zugriff. Wir empfehlen daher, die 2FA zu aktivieren, um Ihr Konto zu schützen.

## E-Mail Template

Standardmäßig wird eine vorbereitete E-Mail mit festem Text verschickt.

Es kann aber ein eigenes YForm-Template für die 2FA verwendet. Mit dem Key ```ycom_otp_code_template``` kann ein eigenes Template gesetzt wird. Folgende Werte sind verfügbar: ```name, email, firstname, code```.
589 changes: 317 additions & 272 deletions install/tablesets/yform_user.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lang/de_de.lang
Original file line number Diff line number Diff line change
@@ -74,6 +74,7 @@ docs = Dokumentation
ycom_docs_tricks = Tricks
ycom_docs_user_groups = User und Gruppen auslesen
ycom_docs = Dokumentation
ycom_docs_otp = 2FA (Zwei-Faktor-Authentifizierung)

ycom_log = Log
ycom_user_log = YCom-User-Log
15 changes: 15 additions & 0 deletions lib/ycom_user.php
Original file line number Diff line number Diff line change
@@ -96,4 +96,19 @@ public function increaseLoginTries(): self
$this->setValue('login_tries', $this->getValue('login_tries') + 1);
return $this;
}

public function increaseOTPTries(): self
{
$otp_tries = (int) $this->getValue('otp_tries');
$this->setValue('otp_tries', $otp_tries + 1);
$this->setValue('otp_last_try_time', time());
return $this;
}

public function resetOTPTries(): self
{
$this->setValue('otp_tries', 0);
$this->setValue('otp_last_try_time', time());
return $this;
}
}
150 changes: 150 additions & 0 deletions plugins/auth/assets/clipboard-copy-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.ClipboardCopyElement = factory());
}(this, function () { 'use strict';

function createNode(text) {
const node = document.createElement('pre');
node.style.width = '1px';
node.style.height = '1px';
node.style.position = 'fixed';
node.style.top = '5px';
node.textContent = text;
return node;
}

function copyNode(node) {
if ('clipboard' in navigator) {
// eslint-disable-next-line flowtype/no-flow-fix-me-comments
// $FlowFixMe Clipboard is not defined in Flow yet.
return navigator.clipboard.writeText(node.textContent);
}

const selection = getSelection();

if (selection == null) {
return Promise.reject(new Error());
}

selection.removeAllRanges();
const range = document.createRange();
range.selectNodeContents(node);
selection.addRange(range);
document.execCommand('copy');
selection.removeAllRanges();
return Promise.resolve();
}
function copyText(text) {
if ('clipboard' in navigator) {
// eslint-disable-next-line flowtype/no-flow-fix-me-comments
// $FlowFixMe Clipboard is not defined in Flow yet.
return navigator.clipboard.writeText(text);
}

const body = document.body;

if (!body) {
return Promise.reject(new Error());
}

const node = createNode(text);
body.appendChild(node);
copyNode(node);
body.removeChild(node);
return Promise.resolve();
}

function copy(button) {
const id = button.getAttribute('for');
const text = button.getAttribute('value');

function trigger() {
button.dispatchEvent(new CustomEvent('clipboard-copy', {
bubbles: true
}));
}

if (text) {
copyText(text).then(trigger);
} else if (id) {
const root = 'getRootNode' in Element.prototype ? button.getRootNode() : button.ownerDocument;
if (!(root instanceof Document || 'ShadowRoot' in window && root instanceof ShadowRoot)) return;
const node = root.getElementById(id);
if (node) copyTarget(node).then(trigger);
}
}

function copyTarget(content) {
if (content instanceof HTMLInputElement || content instanceof HTMLTextAreaElement) {
return copyText(content.value);
} else if (content instanceof HTMLAnchorElement && content.hasAttribute('href')) {
return copyText(content.href);
} else {
return copyNode(content);
}
}

function clicked(event) {
const button = event.currentTarget;

if (button instanceof HTMLElement) {
copy(button);
}
}

function keydown(event) {
if (event.key === ' ' || event.key === 'Enter') {
const button = event.currentTarget;

if (button instanceof HTMLElement) {
event.preventDefault();
copy(button);
}
}
}

function focused(event) {
event.currentTarget.addEventListener('keydown', keydown);
}

function blurred(event) {
event.currentTarget.removeEventListener('keydown', keydown);
}

class ClipboardCopyElement extends HTMLElement {
constructor() {
super();
this.addEventListener('click', clicked);
this.addEventListener('focus', focused);
this.addEventListener('blur', blurred);
}

connectedCallback() {
if (!this.hasAttribute('tabindex')) {
this.setAttribute('tabindex', '0');
}

if (!this.hasAttribute('role')) {
this.setAttribute('role', 'button');
}
}

get value() {
return this.getAttribute('value') || '';
}

set value(text) {
this.setAttribute('value', text);
}

}

if (!window.customElements.get('clipboard-copy')) {
window.ClipboardCopyElement = ClipboardCopyElement;
window.customElements.define('clipboard-copy', ClipboardCopyElement);
}

return ClipboardCopyElement;

}));
6 changes: 6 additions & 0 deletions plugins/auth/assets/qrious.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions plugins/auth/install.php
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@
->ensureColumn(new rex_sql_column('starttime', 'datetime'))
->ensureColumn(new rex_sql_column('last_activity', 'datetime'))
->ensureColumn(new rex_sql_column('last_activity', 'datetime'))
->ensureColumn(new rex_sql_column('otp_verified', 'tinyint(1)', false, '0'))
->ensureColumn(new rex_sql_column('cookie_key', 'varchar(255)', true))
->ensureIndex(new rex_sql_index('cookie_key', ['cookie_key'], rex_sql_index::UNIQUE))
->setPrimaryKey('session_id')
20 changes: 20 additions & 0 deletions plugins/auth/lang/de_de.lang
Original file line number Diff line number Diff line change
@@ -78,3 +78,23 @@ ycom_session_added_ready_to_login = Session wurde angelegt. Bitte hier einloggen
ycom_session_could_not_been_added = Session konnte nicht angelegt werden, weil der User inaktiv ist.
ycom_user_session_delete_all_sessions_link = Alle Sessions <a href="{0}">löschen</a>
ycom_sessions_deleted = {0} Session/s wurden gelöscht

otp_auth_config = OneTimePassword (OTP) Einstellungen. E-Mail und Authentikator
otp_article_id = ... zum OTP-Artikel
otp_auth_enforce = Einschränkungen
otp_auth_enforce_all = Erzwungen für alle YCom Benutzer
otp_auth_enforce_disabled = Keine Einschränkungen (Optional für jeden)
otp_auth_options = Optionen
otp_auth_option_all = Alle Möglichkeiten erlauben
otp_auth_option_totp_only = Nur Time-Based One-Time Passwort erlauben
otp_auth_option_email_only = Nur One-Time Passwort über E-Mail erlauben
otp_auth_email_period = Zeitintervall bei E-Mail Authentifizierung
otp_auth_totp_period = Zeitintervall bei Authentikatoren (nicht veränderbar)
otp_auth_totp_period_info = {0} Sekunden
otp_auth_logintries = Anzahl der erlaubten Fehlversuche (nicht veränderbar)
otp_auth_logintries_info = {0} Versuche

otp_tries = OTP Fehlversuche
otp_tries_info = Erlaubte Fehlversuche werden im Plugin <a href="index.php?page=ycom/auth/settings">YCom/Auth</a> definiert
otp_last_try_time = OTP Letzter Versuch
otp_config = OTP Config
10 changes: 10 additions & 0 deletions plugins/auth/lib/injections/abstract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

abstract class rex_ycom_injection_abtract
{
abstract public function getRewrite(): bool|string;

abstract public function getSettingsContent(): string;

abstract public function triggerSaveSettings(): void;
}
164 changes: 164 additions & 0 deletions plugins/auth/lib/injections/otp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

class rex_ycom_injection_otp extends rex_ycom_injection_abtract
{
public function getRewrite(): bool|string
{
$user = rex_ycom_auth::getUser();

if (!$user) {
return false;
}

$otp_article_id = (int) rex_addon::get('ycom')->getConfig('otp_article_id');

// 1. User ist eingeloggt
// 2. OTP Article vorhanden => OTP aktiviert ?
if (0 == $otp_article_id) {
return false;
}

// 1. User ist eingeloggt
// 2. Keine OTP Konfiguration/Article vorhanden
// 3. Session OTP Check bereits durchgeführt?
$SessionInstance = rex_ycom_user_session::getInstance()->getCurrentSession($user);
if (1 == $SessionInstance['otp_verified']) {
return false;
}

// - user hat überprüfung und keine OTP Session -> zwingend auf otp-article
// - OTP ist erzwungen.
$config = rex_ycom_otp_password_config::forCurrentUser();
$otp_auth_enforce = rex_addon::get('ycom')->getConfig('otp_auth_enforce');
$enforcedAll = rex_ycom_otp_password::ENFORCED_ALL == $otp_auth_enforce ? true : false;
if (!($enforcedAll || $config->enabled)) {
return false;
}

if (rex_article::getCurrentId() == $otp_article_id) {
return false;
}

return rex_getUrl($otp_article_id, '', [], '&');
}

public function getSettingsContent(): string
{
$addon = rex_addon::get('ycom');

$selectEnforce = new rex_select();
$selectEnforce->setId('otp_auth_enforce');
$selectEnforce->setName('otp_auth_enforce');
$selectEnforce->setAttribute('class', 'form-control selectpicker');
$selectEnforce->setSelected($addon->getConfig('otp_auth_enforce'));

$selectEnforce->addOption($addon->i18n('otp_auth_enforce_' . rex_ycom_otp_password::ENFORCED_ALL), rex_ycom_otp_password::ENFORCED_ALL);
$selectEnforce->addOption($addon->i18n('otp_auth_enforce_' . rex_ycom_otp_password::ENFORCED_DISABLED), rex_ycom_otp_password::ENFORCED_DISABLED);

$selectOption = new rex_select();
$selectOption->setId('otp_auth_option');
$selectOption->setName('otp_auth_option');
$selectOption->setAttribute('class', 'form-control selectpicker');
$selectOption->setSelected($addon->getConfig('otp_auth_option'));

$selectOption->addOption($addon->i18n('otp_auth_option_' . rex_ycom_otp_password::OPTION_ALL), rex_ycom_otp_password::OPTION_ALL);
$selectOption->addOption($addon->i18n('otp_auth_option_' . rex_ycom_otp_password::OPTION_TOTP), rex_ycom_otp_password::OPTION_TOTP);
$selectOption->addOption($addon->i18n('otp_auth_option_' . rex_ycom_otp_password::OPTION_EMAIL), rex_ycom_otp_password::OPTION_EMAIL);

$selectEmailPeriod = new rex_select();
$selectEmailPeriod->setId('otp_auth_email_period');
$selectEmailPeriod->setName('otp_auth_email_period');
$selectEmailPeriod->setAttribute('class', 'form-control selectpicker');
$selectEmailPeriod->setSelected($addon->getConfig('otp_auth_email_period'));

$selectEmailPeriod->addOption('5 ' . $addon->i18n('minutes'), 300);
$selectEmailPeriod->addOption('10 ' . $addon->i18n('minutes'), 600);
$selectEmailPeriod->addOption('15 ' . $addon->i18n('minutes'), 900);
$selectEmailPeriod->addOption('30 ' . $addon->i18n('minutes'), 1800);

$selectTOTPPeriod = new rex_select();
$selectTOTPPeriod->setAttribute('class', 'form-control selectpicker');
$selectTOTPPeriod->setDisabled(true);
$selectTOTPPeriod->addOption($addon->i18n('otp_auth_totp_period_info', rex_ycom_otp_method_totp::getPeriod()), 30);

$selectLoginTries = new rex_select();
$selectLoginTries->setAttribute('class', 'form-control selectpicker');
$selectLoginTries->setDisabled(true);
$selectLoginTries->addOption($addon->i18n('otp_auth_logintries_info', rex_ycom_otp_method_totp::getloginTries()), 30);

return '
<fieldset>
<legend>' . $addon->i18n('otp_auth_config') . '</legend>
<div class="row abstand">
<div class="col-xs-12 col-sm-6">
<label for="rex-form-otp_article_id">' . $addon->i18n('otp_article_id') . '</label>
</div>
<div class="col-xs-12 col-sm-6">
' . rex_var_link::getWidget(17, 'otp_article_id', (int) $addon->getConfig('otp_article_id')) . '
<small>[otp_article_id]</small>
</div>
</div>
<div class="row abstand">
<div class="col-xs-12 col-sm-6">
<label for="rex_ycom_otp_auth_enforce">' . $addon->i18n('otp_auth_enforce') . '</label>
</div>
<div class="col-xs-12 col-sm-6">
' . $selectEnforce->get() . '
<small>[otp_auth_enforce]</small>
</div>
</div>
<div class="row abstand">
<div class="col-xs-12 col-sm-6">
<label for="2factor_auth_options">' . $addon->i18n('otp_auth_options') . '</label>
</div>
<div class="col-xs-12 col-sm-6">
' . $selectOption->get() . '
<small>[otp_auth_option]</small>
</div>
</div>
<div class="row abstand">
<div class="col-xs-12 col-sm-6">
<label for="2factor_auth_email_period">' . $addon->i18n('otp_auth_email_period') . '</label>
</div>
<div class="col-xs-12 col-sm-6">
' . $selectEmailPeriod->get() . '
<small>[otp_auth_email_period]</small>
</div>
</div>
<div class="row abstand">
<div class="col-xs-12 col-sm-6">
<label for="2factor_auth_email_period">' . $addon->i18n('otp_auth_totp_period') . '</label>
</div>
<div class="col-xs-12 col-sm-6">
' . $selectTOTPPeriod->get() . '
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-6">
<label for="2factor_auth_email_period">' . $addon->i18n('otp_auth_logintries') . '</label>
</div>
<div class="col-xs-12 col-sm-6">
' . $selectLoginTries->get() . '
</div>
</div>
</fieldset>';
}

public function triggerSaveSettings(): void
{
$addon = rex_addon::get('ycom');
$addon->setConfig('otp_article_id', rex_request('otp_article_id', 'int'));
$addon->setConfig('otp_auth_enforce', rex_request('otp_auth_enforce', 'string'));
$addon->setConfig('otp_auth_option', rex_request('otp_auth_option', 'string'));
$addon->setConfig('otp_auth_email_period', rex_request('otp_auth_email_period', 'int', 300));
}
}
43 changes: 43 additions & 0 deletions plugins/auth/lib/injections/passwordchange.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

class rex_ycom_injection_passwordchange extends rex_ycom_injection_abtract
{
public function getRewrite(): bool|string
{
$user = rex_ycom_auth::getUser();
if ($user) {
$article_id_password = (int) rex_plugin::get('ycom', 'auth')->getConfig('article_id_jump_password', 0);
if (0 != $article_id_password && 1 == $user->getValue('new_password_required')) {
if ($article_id_password != rex_article::getCurrentId()) {
return rex_getUrl($article_id_password, '', [], '&');
}
}
}
return false;
}

public function getSettingsContent(): string
{
$addon = rex_plugin::get('ycom', 'auth');
return '
<fieldset>
<legend>' . $addon->i18n('ycom_auth_config_passwordchange') . '</legend>
<div class="row abstand">
<div class="col-xs-12 col-sm-6">
<label for="rex-form-article_password">' . $addon->i18n('ycom_auth_config_id_jump_password') . '</label>
</div>
<div class="col-xs-12 col-sm-6">
' . rex_var_link::getWidget(9, 'article_id_jump_password', (int) $addon->getConfig('article_id_jump_password')) . '
<small>[article_id_jump_password]</small>
</div>
</div>
</fieldset>
';
}

public function triggerSaveSettings(): void
{
$addon = rex_plugin::get('ycom', 'auth');
$addon->setConfig('article_id_jump_password', rex_request('article_id_jump_password', 'int'));
}
}
44 changes: 44 additions & 0 deletions plugins/auth/lib/injections/termsofuse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

class rex_ycom_injection_termsofuse extends rex_ycom_injection_abtract
{
public function getRewrite(): bool|string
{
$user = rex_ycom_auth::getUser();
if ($user) {
$article_id_termsofuse = (int) rex_plugin::get('ycom', 'auth')->getConfig('article_id_jump_termsofuse', 0);
if (0 != $article_id_termsofuse && 1 != $user->getValue('termsofuse_accepted')) {
if ($article_id_termsofuse != rex_article::getCurrentId()) {
return rex_getUrl($article_id_termsofuse, '', [], '&');
}
}
}
return false;
}

public function getSettingsContent(): string
{
$addon = rex_plugin::get('ycom', 'auth');

return '
<fieldset>
<legend>' . $addon->i18n('ycom_auth_config_termsofuse') . '</legend>
<div class="row abstand">
<div class="col-xs-12 col-sm-6">
<label for="rex-form-article_termsofuse">' . $addon->i18n('ycom_auth_config_id_jump_termsofuse') . '</label>
</div>
<div class="col-xs-12 col-sm-6">
' . rex_var_link::getWidget(10, 'article_id_jump_termsofuse', (int) $addon->getConfig('article_id_jump_termsofuse')) . '
<small>[article_id_jump_termsofuse]</small>
</div>
</div>
</fieldset>
';
}

public function triggerSaveSettings(): void
{
$addon = rex_plugin::get('ycom', 'auth');
$addon->setConfig('article_id_jump_termsofuse', rex_request('article_id_jump_termsofuse', 'int', 0));
}
}
3 changes: 3 additions & 0 deletions plugins/auth/lib/otp/exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

class rex_ycom_otp_exception extends rex_exception {}
83 changes: 83 additions & 0 deletions plugins/auth/lib/otp/method_email.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

use OTPHP\Factory;
use OTPHP\TOTP;

final class rex_ycom_otp_method_email implements rex_ycom_otp_method_interface
{
public static string $yform_email_template_key = 'ycom_otp_code_template';

public function challenge(string $provisioningUrl, rex_ycom_user $user): void
{
$otp = Factory::loadFromProvisioningUri($provisioningUrl);
$otpCode = $otp->at(time());

if ($ycom_otp_code_template = rex_yform_email_template::getTemplate(self::$yform_email_template_key)) {
$values = [];
$values['email'] = $user->getValue('email');
$values['name'] = $user->getValue('name');
$values['firstname'] = $user->getValue('firstname');
$values['code'] = $otpCode;

$yform_email_template = rex_yform_email_template::replaceVars($ycom_otp_code_template, $values);
$yform_email_template['mail_to'] = $user->getValue('email');
$yform_email_template['mail_to_name'] = $user->getValue('name');

if (!rex_yform_email_template::sendMail($yform_email_template, self::$yform_email_template_key)) {
throw new Exception('Unable to send email via Template. Make sure to setup the phpmailer AddOn.');
}
} else {
$mail = new rex_mailer();
$mail->addAddress($user->getValue('email'));
$mail->Subject = 'OTP-Code: (' . $_SERVER['HTTP_HOST'] . ')';
$mail->isHTML();
$mail->Body = '<style>body { font-size: 1.2em; text-align: center;}</style><h2>' . rex::getServerName() . ' Login verification</h2><br><h3><strong>' . $otpCode . '</strong></h3><br> is your 2 factor authentication code.';
$mail->AltBody = " Login verification \r\n ------------------ \r\n" . $otpCode . "\r\n ------------------ \r\nis your 2 factor authentication code.";

if (!$mail->send()) {
throw new Exception('Unable to send email. Make sure to setup the phpmailer AddOn.');
}
}
}

public static function getPeriod(): int
{
return (int) rex_addon::get('2factor_auth')->getConfig('email_period', 300);
}

public static function getloginTries(): int
{
return 10;
}

public function verify(string $provisioningUrl, string $otp): bool
{
$TOTP = Factory::loadFromProvisioningUri($provisioningUrl);

// re-create from an existant uri
if ($TOTP->verify($otp)) {
return true;
}

$lastOTPCode = $TOTP->at(time() - self::getPeriod());
if ($lastOTPCode == $otp) {
return Factory::loadFromProvisioningUri($provisioningUrl)->verify($TOTP->at(time()));
}
return false;
}

public function getProvisioningUri(rex_ycom_user $user): string
{
// create a uri with a random secret
$otp = TOTP::create(null, self::getPeriod());

// the label rendered in "Google Authenticator" or similar app
$label = $user->getValue('login') . '@' . rex::getServerName() . ' (' . $_SERVER['HTTP_HOST'] . ')';
$label = str_replace(':', '_', $label); // colon is forbidden
$otp->setLabel($label);
$otp->setParameter('period', self::getPeriod());
$otp->setIssuer(str_replace(':', '_', $user->getValue('login')));

return $otp->getProvisioningUri();
}
}
20 changes: 20 additions & 0 deletions plugins/auth/lib/otp/method_interface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

interface rex_ycom_otp_method_interface
{
/**
* @throws exception
*/
public function challenge(string $provisioningUrl, rex_ycom_user $user): void;

/**
* @throws exception
*/
public function verify(string $provisioningUrl, string $otp): bool;

public function getProvisioningUri(rex_ycom_user $user): string;

public static function getPeriod(): int;

public static function getloginTries();
}
45 changes: 45 additions & 0 deletions plugins/auth/lib/otp/method_totp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

use OTPHP\Factory;
use OTPHP\TOTP;

use function str_replace;

final class rex_ycom_otp_method_totp implements rex_ycom_otp_method_interface
{
public function challenge(string $provisioningUrl, rex_ycom_user $user): void
{
// nothing todo
}

public function verify(string $provisioningUrl, string $otp): bool
{
// re-create from an existant uri
return Factory::loadFromProvisioningUri($provisioningUrl)->verify($otp);
}

public static function getPeriod(): int
{
// default period is 30s and digest is sha1. Google Authenticator is restricted to this settings
return 30;
}

public static function getloginTries(): int
{
return 10;
}

public function getProvisioningUri(rex_ycom_user $user): string
{
// create a uri with a random secret
$otp = TOTP::create(null, self::getPeriod());

// the label rendered in "Google Authenticator" or similar app
$label = $user->getValue('login') . '@' . rex::getServerName() . ' (' . $_SERVER['HTTP_HOST'] . ')';
$label = str_replace(':', '_', $label); // colon is forbidden
$otp->setLabel($label);
$otp->setIssuer(str_replace(':', '_', $user->getValue('login')));

return $otp->getProvisioningUri();
}
}
94 changes: 94 additions & 0 deletions plugins/auth/lib/otp/password.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

use InvalidArgumentException;
use rex;
use rex_config;
use rex_singleton_trait;

final class rex_ycom_otp_password
{
use rex_singleton_trait;

public const ENFORCED_ALL = 'all';
public const ENFORCED_DISABLED = 'disabled';

public const OPTION_ALL = 'all';
public const OPTION_TOTP = 'totp_only';
public const OPTION_EMAIL = 'email_only';

/** @var rex_ycom_otp_method_interface|null */
private $method;

public function challenge(): void
{
$user = rex_ycom_auth::getUser();
$uri = str_replace('&amp;', '&', (string) rex_ycom_otp_password_config::forCurrentUser()->getProvisioningUri());
$this->getMethod()->challenge($uri, $user);
}

public function verify(string $otp): bool
{
$uri = str_replace('&amp;', '&', (string) rex_ycom_otp_password_config::forCurrentUser()->getProvisioningUri());
$verified = $this->getMethod()->verify($uri, $otp);
return $verified;
}

public function isVerified(): bool
{
return rex_session('otp_verified', 'boolean', false);
}

public function isEnabled(): bool
{
return rex_ycom_otp_password_config::forCurrentUser()->enabled;
}

/**
* @param self::ENFORCE* $enforce
*/
public function enforce($enforce): void
{
rex_config::set('ycom', 'otp_auth_enforce', $enforce);
}

/**
* @return self::ENFORCE*
*/
public function isEnforced()
{
return rex_config::get('ycom', 'otp_auth_enforce', self::ENFORCED_DISABLED);
}

/**
* @return self::OPTION*
*/
public function getAuthOption()
{
return rex_config::get('ycom', 'otp_auth_option', self::OPTION_ALL);
}

public function setAuthOption(string $option): void
{
rex_config::set('ycom', 'otp_auth_option', $option);
}

/**
* @return rex_ycom_otp_method_interface
*/
public function getMethod()
{
if (null === $this->method) {
$methodType = rex_ycom_otp_password_config::forCurrentUser()->getMethod();

if ('totp' === $methodType) {
$this->method = new rex_ycom_otp_method_totp();
} elseif ('email' === $methodType) {
$this->method = new rex_ycom_otp_method_email();
} else {
throw new InvalidArgumentException("Unknown method: $methodType");
}
}

return $this->method;
}
}
137 changes: 137 additions & 0 deletions plugins/auth/lib/otp/password_config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

/**
* @internal
*/
final class rex_ycom_otp_password_config
{
/** @var string|null */
public $provisioningUri;
/** @var bool */
public $enabled = false;
/** @var 'totp'|'email'|null */
public $method;
/** @var rex_ycom_user */
public $user;

public function __construct(rex_ycom_user $user)
{
$this->user = $user;
}

public static function forCurrentUser(): self
{
$user = rex_ycom_auth::getUser();
return self::forUser($user);
}

public static function forUser(rex_ycom_user $user): self
{
return self::fromJson($user->getValue('otp_config'), $user);
}

public static function loadFromDb(rex_ycom_otp_method_interface $method, rex_ycom_user $user): self
{
// get non-cached values
$userSql = rex_sql::factory();
$userSql->setTable(rex::getTablePrefix() . 'ycom_user');
$userSql->setWhere(['id' => $user->getId()]);
$userSql->select();

$json = (string) $userSql->getValue('otp_config');
$config = self::fromJson($json, $user);
$config->method = $method instanceof rex_ycom_otp_method_email ? 'email' : 'totp';
if (null === $config->getProvisioningUri()) {
$config->setProvisioningUri($method->getProvisioningUri($user));
}
return $config;
}

private static function fromJson(?string $json, rex_ycom_user $user): self
{
if (is_string($json)) {
$configArr = json_decode($json, true);

if (is_array($configArr)) {
// compat with older versions, which did not yet define a method
if (!array_key_exists('method', $configArr)) {
$configArr['method'] = 'totp';
}

$config = new self($user);
$config->provisioningUri = $configArr['provisioningUri'];
$config->enabled = $configArr['enabled'];
$config->method = $configArr['method'];
return $config;
}
}

$method = new rex_ycom_otp_method_totp();

$default = new self($user);
$default->method = $method instanceof rex_ycom_otp_method_email ? 'email' : 'totp';
$default->provisioningUri = $method->getProvisioningUri($user);

return $default;
}

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

public function enable(): self
{
$this->enabled = true;
return $this;
}

public function disable(): self
{
$this->enabled = false;
$this->provisioningUri = null;
return $this;
}

public function updateMethod(rex_ycom_otp_method_interface $method): self
{
$this->method = $method instanceof rex_ycom_otp_method_email ? 'email' : 'totp';
$this->provisioningUri = $method->getProvisioningUri($this->user);
return $this;
}

public function getProvisioningUri()
{
return $this->provisioningUri;
}

public function setProvisioningUri($provisioningUri): self
{
$this->provisioningUri = $provisioningUri;
return $this;
}

public function getMethod()
{
return $this->method;
}

public function save(): void
{
echo '<pre>';
debug_print_backtrace();
echo '</pre>';

$userSql = rex_sql::factory();
$userSql->setTable(rex::getTablePrefix() . 'ycom_user');
$userSql->setWhere(['id' => $this->user->getId()]);
$userSql->setValue('otp_config', json_encode(
[
'provisioningUri' => $this->provisioningUri,
'method' => $this->method,
'enabled' => $this->enabled,
],
));
$userSql->update();
}
}
40 changes: 28 additions & 12 deletions plugins/auth/lib/ycom_auth.php
Original file line number Diff line number Diff line change
@@ -27,6 +27,26 @@ class rex_ycom_auth
];
public static string $sessionKey = 'ycom_login';

public static array $injections = [];

public static function addInjection(rex_ycom_injection_abtract $injection, int $level = 1): void
{
self::$injections[$level][] = $injection; // $level][
}

public static function getInjections(): array
{
$injections = [];
ksort(self::$injections);

foreach (self::$injections as $level => $injection) {
foreach ($injection as $inj) {
$injections[] = $inj;
}
}
return $injections;
}

public static function getRequestKey(string $requestKey): string
{
return rex_config::get('ycom', $requestKey, self::$DefaultRequestKeys[$requestKey]);
@@ -110,19 +130,15 @@ public static function init(): string
$params['loginStatus'] = $login_status;
$params = rex_extension::registerPoint(new rex_extension_point('YCOM_AUTH_INIT', $params, []));

if (self::getUser()) {
$article_id_password = (int) rex_plugin::get('ycom', 'auth')->getConfig('article_id_jump_password');
$article_id_termsofuse = (int) rex_plugin::get('ycom', 'auth')->getConfig('article_id_jump_termsofuse');
if (rex_plugin::get('ycom', 'auth')->getConfig('article_id_logout') == rex_article::getCurrentId()) {
// ignore rest - because logout is always ok .
} else {
// dd(self::getInjections());

if (rex_plugin::get('ycom', 'auth')->getConfig('article_id_logout') == rex_article::getCurrentId()) {
// ignore rest - because logout is always ok .
} elseif (0 != $article_id_termsofuse && 1 != self::getUser()->getValue('termsofuse_accepted')) {
if ($article_id_termsofuse != rex_article::getCurrentId()) {
$params['redirect'] = rex_getUrl($article_id_termsofuse, '', [], '&');
}
} elseif (0 != $article_id_password && 1 == self::getUser()->getValue('new_password_required')) {
if ($article_id_password != rex_article::getCurrentId()) {
$params['redirect'] = rex_getUrl($article_id_password, '', [], '&');
foreach (self::getInjections() as $injection) {
$rewrite = $injection->getRewrite();
if ($rewrite && '' != $rewrite) {
return $rewrite;
}
}
}
31 changes: 31 additions & 0 deletions plugins/auth/lib/ycom_user_session.php
Original file line number Diff line number Diff line change
@@ -28,6 +28,21 @@ public function storeCurrentSession($user, ?string $cookieKey = null): void
->insertOrUpdate();
}

/**
* @param rex_ycom_user|rex_yform_manager_dataset $user
* @throws rex_exception
* @throws rex_sql_exception
*/
public function getCurrentSession($user): ?array
{
$Sessions = rex_sql::factory()
->setTable(rex::getTable('ycom_user_session'))
->setWhere('session_id = ? and user_id = ?', [session_id(), $user->getId()])
->select()
->getArray();
return (0 == count($Sessions)) ? null : $Sessions[0];
}

public function clearCurrentSession(): self
{
$sessionId = session_id();
@@ -59,6 +74,22 @@ public function updateLastActivity(rex_ycom_user $user): void
->insertOrUpdate();
}

public function setOTPverified(rex_ycom_user $user, $sessionId = null): void
{
if (null === $sessionId) {
$sessionId = session_id();
if (false === $sessionId || '' === $sessionId) {
return;
}
}

rex_sql::factory()
->setTable(rex::getTable('ycom_user_session'))
->setValue('otp_verified', 1)
->setWhere('session_id = :session_id and user_id = :user_id', ['session_id' => $sessionId, 'user_id' => $user->getId()])
->update();
}

public static function clearExpiredSessions(): void
{
rex_sql::factory()
88 changes: 88 additions & 0 deletions plugins/auth/lib/yform/value/ycom_auth_otp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

class rex_yform_value_ycom_auth_otp extends rex_yform_value_abstract
{
public function enterObject(): void
{
// User ist hier, weil er will oder muss
// 1. User ist eingeloggt

$user = rex_ycom_auth::getUser();
if (!$user) {
return;
}

$otp_article_id = (int) rex_addon::get('ycom')->getConfig('otp_article_id');
if (0 == $otp_article_id) {
return;
}

// es gibt hier folgende Seiten

// Setup - Auswahl der Methode
// setup, bestätigung der Methode mit Code
// -> hier wird, wenn erfolgereich, auf die UserStartseite geleitet

// -> wenn user enabled ist dann auf setup und es eventuell zu deaktivieren

// Verify - Eingabe des Codes
// -> hier wird, wenn erfolgereich, auf die UserStartseite geleitet

$page = 'setup';
$SessionInstance = null;
$config = rex_ycom_otp_password_config::forCurrentUser();
if ($config->enabled) {
$SessionInstance = rex_ycom_user_session::getInstance()->getCurrentSession($user);
if (1 != $SessionInstance['otp_verified']) {
$page = 'verify';
}
}

switch ($page) {
case 'verify':
$this->params['form_output'][$this->getId()] = $this->parse(
['value.ycom_auth_otp_verify.tpl.php'],
[
'user' => $user,
'SessionInstance' => $SessionInstance,
'config' => $config,
'otp_article_id' => $otp_article_id,
],
);
break;
default:
// setup
$this->params['form_output'][$this->getId()] = $this->parse(
['value.ycom_auth_otp_setup.tpl.php'],
[
'user' => $user,
'SessionInstance' => $SessionInstance,
'config' => $config,
'otp_article_id' => $otp_article_id,
],
);
}
}

public function getDescription(): string
{
return 'ycom_auth_otp -> Beispiel: ycom_auth_otp';
}

/**
* @return array<string, mixed>
*/
public function getDefinitions(): array
{
return [
'type' => 'value',
'name' => 'ycom_auth_otp',
'values' => [
'name' => ['type' => 'name', 'label' => 'Feld'],
'label' => ['type' => 'text', 'label' => 'Bezeichnung'],
],
'description' => '',
'famous' => false,
];
}
}
39 changes: 12 additions & 27 deletions plugins/auth/pages/settings.php
Original file line number Diff line number Diff line change
@@ -20,8 +20,6 @@
$this->setConfig('article_id_jump_not_ok', rex_request('article_id_jump_not_ok', 'int'));
$this->setConfig('article_id_jump_logout', rex_request('article_id_jump_logout', 'int'));
$this->setConfig('article_id_jump_denied', rex_request('article_id_jump_denied', 'int'));
$this->setConfig('article_id_jump_password', rex_request('article_id_jump_password', 'int'));
$this->setConfig('article_id_jump_termsofuse', rex_request('article_id_jump_termsofuse', 'int'));
$this->setConfig('article_id_login', rex_request('article_id_login', 'int'));
$this->setConfig('article_id_logout', rex_request('article_id_logout', 'int'));
$this->setConfig('article_id_register', rex_request('article_id_register', 'int'));
@@ -32,6 +30,10 @@
$this->setConfig('session_max_overall_duration', rex_request('session_max_overall_duration', 'int'));
$this->setConfig('session_duration', rex_request('session_duration', 'int'));

foreach (rex_ycom_auth::getInjections() as $injection) {
$injection->triggerSaveSettings();
}

echo rex_view::success($this->i18n('ycom_auth_settings_updated'));
}

@@ -74,8 +76,9 @@
$content .= '
<form action="index.php" method="post" id="ycom_auth_settings">
<input type="hidden" name="page" value="ycom/auth/settings" />
<input type="hidden" name="func" value="update" />
<input type="hidden" name="func" value="update" />';

$content .= '
<fieldset>
<legend>' . $this->i18n('ycom_auth_config_forwarder') . '</legend>
@@ -117,27 +120,6 @@
<small>' . $this->i18n('ycom_auth_config_id_jump_denied_notice') . '</small>
</div>
</div>
<div class="row abstand">
<div class="col-xs-12 col-sm-6">
<label for="rex-form-article_password">' . $this->i18n('ycom_auth_config_id_jump_password') . '</label>
</div>
<div class="col-xs-12 col-sm-6">
' . rex_var_link::getWidget(9, 'article_id_jump_password', (int) $this->getConfig('article_id_jump_password')) . '
<small>[article_id_jump_password]</small>
</div>
</div>
<div class="row abstand">
<div class="col-xs-12 col-sm-6">
<label for="rex-form-article_termsofuse">' . $this->i18n('ycom_auth_config_id_jump_termsofuse') . '</label>
</div>
<div class="col-xs-12 col-sm-6">
' . rex_var_link::getWidget(10, 'article_id_jump_termsofuse', (int) $this->getConfig('article_id_jump_termsofuse')) . '
<small>[article_id_jump_termsofuse]</small>
</div>
</div>
</fieldset>
<fieldset>
@@ -251,22 +233,25 @@
<label for="rex-form-article_login_failed">' . $this->i18n('ycom_auth_config_id_jump_not_ok') . '</label>
</div>
<div class="col-xs-12 col-sm-6">
' . rex_var_link::getWidget(6, 'article_id_jump_not_ok', $this->getConfig('article_id_jump_not_ok', '')) . '
' . rex_var_link::getWidget(15, 'article_id_jump_not_ok', $this->getConfig('article_id_jump_not_ok', '')) . '
<small>[article_id_jump_not_ok]</small>
</div>
</div>
</fieldset>
</fieldset>';

foreach (rex_ycom_auth::getInjections() as $injection) {
$content .= $injection->getSettingsContent();
}

$content .= '
<div class="row">
<div class="col-xs-12 col-sm-6 col-sm-push-6">
<button class="btn btn-save right" type="submit" name="config-submit" value="1" title="' . $this->i18n('ycom_auth_config_save') . '">' . $this->i18n('ycom_auth_config_save') . '</button>
</div>
</div>
</form>
';

$fragment = new rex_fragment();
190 changes: 190 additions & 0 deletions plugins/auth/ytemplates/bootstrap/value.ycom_auth_otp_setup.tpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<?php

/** @var rex_yform_value_abstract $this */
/** @var rex_ycom_user $user */
/** @var array|null $SessionInstance */
/** @var rex_ycom_otp_password_config $config */
/** @var int $otp_article_id */

$addon = rex_plugin::get('ycom', 'auth');

$otp = rex_ycom_otp_password::getInstance();
$otp_options = $otp->getAuthOption();

$func = rex_request('otp-func', 'string');
$myOTP = rex_post('otp', 'string', null);

$otpOptions = [];

switch ($otp_options) {
case rex_ycom_otp_password::OPTION_ALL:
$otpOptions[] = 'email';
$otpOptions[] = 'totp';
$defaultOption = 'email';
break;
case rex_ycom_otp_password::OPTION_EMAIL:
$defaultOption = 'email';
$otpOptions[] = $defaultOption;
break;
case rex_ycom_otp_password::OPTION_TOTP:
$defaultOption = 'totp';
$otpOptions[] = $defaultOption;
break;
}

// 1. Setup neu durchlaufen wenn man schon verified ist
// 1.1. Authcode um OTP zu deaktivieren -> Schritt 2

if ($otp->isEnabled() && $config->enabled) {
// TODO: AuthCode zum deaktivieren abfragen

if ('disable' == $func) {
$OTPInstance = rex_ycom_otp_password::getInstance();
$OTPMethod = $OTPInstance->getMethod();
rex_ycom_otp_password_config::loadFromDb($OTPMethod, $user)
->disable()
->save();
$func = '';

$this->params['warning'][$this->getId()] = $this->params['error_class'];
$this->params['warning_messages'][$this->getId()] = '{ ycom_otp_diabled }';
} else {
echo '
<div class="form-check">
<input class="form-check" type="radio" name="otp-func" id="otp-func" value="disable" checked="checked" />
<label class="form-check" for="otp-func-">{ ycom_otp_disable_info }</label>
</div>';
}

return;
}

// 2. Setup starten
// 2.1 jeweile Methode auswählen
// 2.2 -> Codeseite /email oder totp

if (in_array($func, $otpOptions)) {
switch ($func) {
case 'email':
$defaultOption = 'email';
$otpMethod = new rex_ycom_otp_method_email();
break;
case 'totp':
default:
$defaultOption = 'totp';
$otpMethod = new rex_ycom_otp_method_totp();
break;
}

if (null === $myOTP) {
rex_ycom_otp_password_config::loadFromDb($otpMethod, $user)
->updateMethod($otpMethod)
->save();
$user->loadData(); // Refresh OTP with new DB Data
$this->params['warning'][$this->getId()] = $this->params['error_class'];
}

if ('email' === $func && (null === $myOTP || 'resend' == rex_request('otp-func-email', 'string'))) {
$this->params['warning'][$this->getId()] = $this->params['error_class'];
$this->params['warning_messages'][$this->getId()] = '{ ycom_otp_email_check }';
rex_ycom_otp_password::getInstance()->challenge();
}

// initial starten wenn beim user nicht vorhanden oder noch nicht enabled.
if (is_string($myOTP) && '' !== $myOTP) {
if ($otp->verify($myOTP)) {
rex_ycom_otp_password_config::loadFromDb($otpMethod, $user)
->enable()
->save();
$user->loadData();
$user
->resetOTPTries()
->save();
rex_ycom_user_session::getInstance()
->setOTPverified($user);
$article_jump_ok = (int) rex_plugin::get('ycom', 'auth')->getConfig('article_id_jump_ok');
rex_response::sendRedirect(rex_getUrl($article_jump_ok, rex_clang::getCurrentId()));
} else {
$this->params['warning'][$this->getId()] = $this->params['error_class'];
$this->params['warning_messages'][$this->getId()] = '{ ycom_otp_code_error }';
}
}

if ('totp' == $func) {
$uri = rex_ycom_otp_password_config::loadFromDb($otpMethod, $user)->getProvisioningUri();

?>
<div class="row">
<div class="col-lg-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{ ycom_otp_setup_scan }</h3>
</div>
<div class="panel-body">
<div class="form-group">
<canvas id="ycom-auth-otp-qr-code"></canvas>
</div>
<div class="form-group">
<div class="input-group">
<label for="ycom-auth-otp-uri">{ ycom_otp_setup_uri }</label>
<input type="text" class="form-control" value="<?= $uri ?>" id="ycom-auth-otp-uri" readonly />
</div>
</div>
</div>
</div>
</div>
</div>
<script src="<?= $addon->getAssetsUrl('qrious.min.js') ?>" nonce="<?= rex_response::getNonce() ?>"></script>
<script nonce="<?= rex_response::getNonce() ?>">
new QRious({
element: document.getElementById("ycom-auth-otp-qr-code"),
value: document.getElementById("ycom-auth-otp-uri").value,
size: 300
});
</script>
<?php
}

?>
<div class="row">
<div class="col-lg-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{ ycom_otp_setup_title }</h3>
</div>
<div class="panel-body">
<input type="hidden" name="func" value="verify-totp" />
<div class="form-group">
<div class="input-group">
<label for="otp-setup-code">{ ycom_otp_setup_code }</label>
<input type="text" class="form-control" name="otp" id="otp-setup-code" />
</div>
</div>
</div>
</div>
</div>
</div>

<?php

if ('email' == $func) {
?>
<div class="row">
<div class="col-lg-6">
<div class="form-check">
<input class="form-check" type="checkbox" name="otp-func-email" id="otp-func-email-resend" value="resend" />
<label class="form-check" for="otp-func-email-resend">{ ycom_otp_email_resend_info }</label>
</div>
</div>
</div><?php

}
}

foreach ($otpOptions as $option) {
echo '
<div class="form-check">
<input class="form-check" type="radio" name="otp-func" id="otp-func-' . $option . '" value="' . $option . '" ' . ($defaultOption == $option ? 'checked="checked"' : '') . ' />
<label class="form-check" for="otp-func-' . $option . '">{ ycom_otp_' . $option . '_info }</label>
</div>';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

/** @var rex_yform_value_abstract $this */
/** @var rex_ycom_user $user */
/** @var array|null $SessionInstance */
/** @var rex_ycom_otp_password_config $config */
/** @var int $otp_article_id */

$addon = rex_plugin::get('ycom', 'auth');

$OTPInstance = rex_ycom_otp_password::getInstance();
$OTPMethod = $OTPInstance->getMethod();
$blockTime = (int) ($OTPMethod::getPeriod() / 10);
$loginTriesAllowed = $OTPMethod::getloginTries();
$loginTries = (int) $user->getValue('otp_tries');
$loginLastTry = (int) $user->getValue('otp_last_try_time');

$myOTP = rex_post('otp', 'string', null);

if (null !== $myOTP) {
if ($loginTries >= $loginTriesAllowed && ($loginLastTry > time() - $blockTime)) {
$this->params['warning'][$this->getId()] = $this->params['error_class'];
$this->params['warning_messages'][$this->getId()] = '{{ ycom_otp_code_error_blocked }}';
$countdownTime = $loginLastTry - time() + $blockTime;

echo '<script nonce="' . rex_response::getNonce() . '">
let countdown = ' . $countdownTime . ';
let countdownElement = document.getElementById("otp_countdown");
let interval = setInterval(() => {
countdown--;
countdownElement.innerHTML = countdown;
if (countdown <= 0) {
clearInterval(interval);
}
}, 1000);
</script>';
} else {
if ($OTPInstance->verify($myOTP)) {
$user->resetOTPTries()->save();
rex_ycom_user_session::getInstance()->setOTPVerified($user);
$article_jump_ok = (int) rex_plugin::get('ycom', 'auth')->getConfig('article_id_jump_ok');
rex_response::sendRedirect(rex_getUrl($article_jump_ok, rex_clang::getCurrentId()));
} else {
$user->increaseOTPTries()->save();
$this->params['warning'][$this->getId()] = $this->params['error_class'];
$this->params['warning_messages'][$this->getId()] = '{ ycom_otp_code_error }';
}
}
} else {
if ('rex_ycom_otp_method_email' === $OTPMethod::class) {
rex_ycom_otp_password::getInstance()->challenge();
}
}

?>

<div class="row">
<div class="col-lg-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{ ycom_otp_verify_title }</h3>
</div>
<div class="panel-body">
<input type="hidden" name="func" value="verify-totp" />
<div class="form-group">
<div class="input-group">
<label for="otp_verify_code">{ ycom_otp_verify_code }</label>
<input type="text" class="form-control" name="otp" id="otp_verify_code" />
</div>
</div>
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@
$notice[] = rex_i18n::translate($this->getElement('notice'), false);
}
if (isset($this->params['warning_messages'][$this->getId()]) && !$this->params['hide_field_warning_messages']) {
$notice[] = '<span class="text-warning">' . rex_i18n::translate($this->params['warning_messages'][$this->getId()]) . '</span>'; // var_dump();
$notice[] = '<span class="text-warning">' . rex_i18n::translate($this->params['warning_messages'][$this->getId()]) . '</span>';
}
if (count($notice) > 0) {
$notice = '<p class="help-block">' . implode('<br />', $notice) . '</p>';
119 changes: 0 additions & 119 deletions vendor/bin/php-cs-fixer

This file was deleted.

119 changes: 0 additions & 119 deletions vendor/bin/php-parse

This file was deleted.

122 changes: 0 additions & 122 deletions vendor/bin/phpunit

This file was deleted.

637 changes: 0 additions & 637 deletions vendor/composer/autoload_classmap.php

Large diffs are not rendered by default.

11 changes: 1 addition & 10 deletions vendor/composer/autoload_files.php
Original file line number Diff line number Diff line change
@@ -6,17 +6,8 @@
$baseDir = dirname($vendorDir);

return array(
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
'344f11dc3484aaed5cbde58e23513be4' => $vendorDir . '/apereo/phpcas/source/CAS.php',
'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
);
29 changes: 3 additions & 26 deletions vendor/composer/autoload_psr4.php
Original file line number Diff line number Diff line change
@@ -6,38 +6,15 @@
$baseDir = dirname($vendorDir);

return array(
'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
'Symfony\\Contracts\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher-contracts'),
'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'),
'Symfony\\Component\\Stopwatch\\' => array($vendorDir . '/symfony/stopwatch'),
'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'),
'Symfony\\Component\\OptionsResolver\\' => array($vendorDir . '/symfony/options-resolver'),
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
'Symfony\\Component\\Filesystem\\' => array($vendorDir . '/symfony/filesystem'),
'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'),
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'RobRichards\\XMLSecLibs\\' => array($vendorDir . '/robrichards/xmlseclibs/src'),
'Redaxo\\PhpCsFixerConfig\\' => array($vendorDir . '/redaxo/php-cs-fixer-config/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src'),
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PhpCsFixer\\' => array($vendorDir . '/friendsofphp/php-cs-fixer/src'),
'PhpCsFixerCustomFixers\\' => array($vendorDir . '/kubawerlos/php-cs-fixer-custom-fixers/src'),
'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'),
'OneLogin\\' => array($vendorDir . '/onelogin/php-saml/src'),
'OTPHP\\' => array($vendorDir . '/spomky-labs/otphp/src'),
'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src'),
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
'Doctrine\\Instantiator\\' => array($vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator'),
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
'Composer\\XdebugHandler\\' => array($vendorDir . '/composer/xdebug-handler/src'),
'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'),
'Composer\\Pcre\\' => array($vendorDir . '/composer/pcre/src'),
);
794 changes: 12 additions & 782 deletions vendor/composer/autoload_static.php

Large diffs are not rendered by default.

4,031 changes: 318 additions & 3,713 deletions vendor/composer/installed.json

Large diffs are not rendered by default.

532 changes: 41 additions & 491 deletions vendor/composer/installed.php

Large diffs are not rendered by default.

19 changes: 0 additions & 19 deletions vendor/composer/pcre/LICENSE

This file was deleted.

181 changes: 0 additions & 181 deletions vendor/composer/pcre/README.md

This file was deleted.

46 changes: 0 additions & 46 deletions vendor/composer/pcre/composer.json

This file was deleted.

46 changes: 0 additions & 46 deletions vendor/composer/pcre/src/MatchAllResult.php

This file was deleted.

46 changes: 0 additions & 46 deletions vendor/composer/pcre/src/MatchAllStrictGroupsResult.php

This file was deleted.

48 changes: 0 additions & 48 deletions vendor/composer/pcre/src/MatchAllWithOffsetsResult.php

This file was deleted.

39 changes: 0 additions & 39 deletions vendor/composer/pcre/src/MatchResult.php

This file was deleted.

39 changes: 0 additions & 39 deletions vendor/composer/pcre/src/MatchStrictGroupsResult.php

This file was deleted.

41 changes: 0 additions & 41 deletions vendor/composer/pcre/src/MatchWithOffsetsResult.php

This file was deleted.

60 changes: 0 additions & 60 deletions vendor/composer/pcre/src/PcreException.php

This file was deleted.

Loading

0 comments on commit e831c6e

Please sign in to comment.