Skip to content
Draft

2fa #204

Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docker/www/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ ARG XDEBUG_AUTOSTART=1
ARG XDEBUG_REMOTE_LOG

RUN apt-get update -y
RUN apt-get install -y --no-install-recommends libzip-dev zip unzip git libfreetype6-dev libjpeg62-turbo-dev libpng-dev
RUN apt-get install -y --no-install-recommends libzip-dev zip unzip git libfreetype6-dev libjpeg62-turbo-dev libpng-dev zlib1g-dev libicu-dev g++

RUN docker-php-ext-install mysqli calendar mbstring
RUN docker-php-ext-install mysqli calendar mbstring intl
RUN docker-php-ext-configure intl
RUN docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && docker-php-ext-install -j$(nproc) gd
RUN pecl install xdebug && docker-php-ext-enable xdebug && \
echo "xdebug.remote_enable=${XDEBUG_REMOTE_ENABLE}" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini && \
Expand Down
4 changes: 3 additions & 1 deletion forum/composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"name": "coderscommunity/forum",
"require": {
"sentry/sentry": "^1.0"
"sentry/sentry": "^1.0",
"ext-mbstring": "*",
"ext-intl": "*"
}
}
20 changes: 20 additions & 0 deletions forum/qa-plugin/q2a-googleauthenticator-login/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "event15/q2a-googleauthenticator-login",
"type": "library",
"license": "GPLv2",
"authors": [
Comment thread
event15 marked this conversation as resolved.
{
"name": "Marek Woś",
"email": "mwos@getresponse.com"
}
],
"minimum-stability": "dev",
"autoload": {
"psr-4": {
"CodersCommunity\\": "src/"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

myślę, że namespace powinien być bardziej konkretny, tzn. zawierać jeszcze chociażby nazwę plugina, ale tu jest cała kwestia uzywania composera, opisałem w komentarzu

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

miałem na caly ten kod zupełnie inny pomysł z początku, ale zostałem uprzedzony i kod z obiektowego stał się q2a'owy. Przy takim stanie rzeczy przyjąłem to co już było i nie starałem się diametralnie wszystkiego zmieniać. Jeśli się uda dodać do composera głównego zależność a jej tu nie budować, to być może to zniknie, z resztą o ile mam dobrą pamięć, to ten namespace jest nieużyty, a na pewno nie tak, jak powinien.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nawet jeśli ten byłby nie użyty to CodersCommunity kojarzy się z nazwą całej organizacji forumowej, więc czemu ma należeć do jednego plugina. Tym bardziej już gdyby coś było w globalnym lepiej byłoby dać chociażby CodersCommunity\GoogleAuthenticatorLogin czy cokolwiek innego, co jednoznacznie wskazuje, że chodzi o dany plugin, nie całą grupę.

}
},
"require": {
"robthree/twofactorauth": "dev-master"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nie lepiej dla bezpieczeństwa wskazać konkretną wersję o jaką nam chodzi?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK

}
}
72 changes: 72 additions & 0 deletions forum/qa-plugin/q2a-googleauthenticator-login/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions forum/qa-plugin/q2a-googleauthenticator-login/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "Google Authenticator 2-factor authentication for Q2A",
"description": "This plugin provides a Google 2FA security for users on forum",
"version": "1.0",
"date": "2018-05-30",
"author": "Marek Woś",
Comment thread
event15 marked this conversation as resolved.
"author_uri": "http://github.com/event15",
"license": "GPLv2",
"update_uri": "Web address for Q2A to check for updates",
"min_q2a": "1.7",
"min_php": "5.4",
"load_order": "Bootstrap moment in which the plugin will be loaded"
}
Comment thread
event15 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace CodersCommunity;

require_once __DIR__ . '/../../vendor/autoload.php';

class q2a_googleauthenticator_admin
{
public function init_queries()
{
$isActive = qa_opt('googleauthenticator_login');
$result = null;

if (1 === $isActive) {
return null;
}

$queries = [];

$columns = qa_db_read_all_values(qa_db_query_sub('describe ^users'));
if (!in_array('2fa_enabled', $columns, true)) {
$queries[] = 'ALTER TABLE ^users ADD `2fa_enabled` SMALLINT (1) DEFAULT 0';
}

if (!in_array('2fa_change_date', $columns, true)) {
$queries[] = 'ALTER TABLE ^users ADD `2fa_change_date` VARCHAR (80) DEFAULT 0';
}

if (!in_array('2fa_secret', $columns, true)) {
$queries[] =
'ALTER TABLE ^users ADD `2fa_secret` VARCHAR ( 80 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL';
}

if (!in_array('2fa_recovery_code', $columns, true)) {
$queries[] = 'ALTER TABLE ^users ADD `2fa_recovery_code` VARCHAR (11) DEFAULT 0';
}

if(count($queries)) {
$result = $queries;
}

// we're already set up
qa_opt('googleauthenticator_login', 1);

return $result;
}

public function admin_form()
{
$saved = false;

if (qa_clicked('2fa_save_button')) {
$enabled = qa_post_text('googleauthenticator_enable_plugin');
qa_opt('googleauthenticator_login', empty($enabled) ? 0 : 1);

$saved = true;
}

return [
'ok' => $saved ? qa_lang('plugin_2fa/saved_plugin_settings') : null,
'fields' => [[
'type' => 'checkbox',
'label' => qa_lang('plugin_2fa/disable_plugin'),
'value' => qa_opt('googleauthenticator_login') ? true : false,
'tags' => 'NAME="googleauthenticator_enable_plugin"'
]
],
'buttons' => [[
'label' => qa_lang('plugin_2fa/save_settings'),
'tags' => 'NAME="2fa_save_button"'
]
]
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php

require_once GOOGLEAUTHENTICATOR_BASIC_PATH . '/src/GoogleAuthenticator.php';

class qa_html_theme_layer extends qa_html_theme_base
{
public function doctype()
{
parent::doctype();

if ('account' === $this->request && true === (bool) qa_opt('googleauthenticator_login')) {
$content = [
'tags' => 'method="post" action="' . qa_self_html() . '"',
'style' => 'wide',
'title' => qa_lang('plugin_2fa/title')
];

$content += $this->enablePlugin();

$this->content['form_2fa'] = $content;
qa_html_theme_base::doctype();
}
}

private function getUserQuery($userId): ?array
{
$users = qa_db_read_all_assoc(
qa_db_query_sub(
'SELECT us.userid, us.2fa_enabled, us.email, us.handle, us.2fa_change_date, up.points FROM ^users us LEFT JOIN ^userpoints up ON us.userid = up.userid WHERE us.userid=$',
$userId['userid'] ?? $userId
)
);

return empty($users) ? null : $users[0];
}

private function updateUserEnable2FA($userId, $isEnabled, $secret = null, $recoveryCode = null): ?bool
{
$time = $this->setTime();
$result = qa_db_query_sub(
'UPDATE ^users SET 2fa_enabled=#, 2fa_change_date=$, 2fa_secret=$, 2fa_recovery_code=$ WHERE userid=#',
$isEnabled,
$time,
$secret,
$recoveryCode,
$userId['userid']
);

if (true === $result) {
return $isEnabled;
}

user_error(qa_lang('plugin_2fa/2fa_setup_error'));
}

private function enablePlugin(): array
{
$userAccount = $this->getUser();
[$userActive2fa, $recoveryCode, $secret] = $this->prepareGoogleAuthenticator($userAccount);

if (!(bool) $userActive2fa) {
return $this->render2FADisabledForm();
}

$result = $this->render2FAEnabledForm($userAccount['2fa_change_date']);

if (isset($this->init)) {
$note = qa_lang_html('plugin_2fa/2fa_data_info');
$note = str_replace(
['{{ QR_CODE }}', '{{ SECRET }}', '{{ RECOVERY_CODE }}', '{{ ERROR_START }}', '{{ ERROR_END }}'],
[
'<br><div style="text-align: center;"><img src="' . $this->init->getQRCode() . '"></div><br>',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Czy te <br>y są potrzebne? Odstęp między elementami można zrobić przy użyciu margin w CSS. A skoro o stylowaniu mowa, to przy okazji wypadałoby przenieść ten text-align: center; do CSSa.

Brakuje atrybutu [alt] dla obrazka - mógłby mieć wartość np. "QRCode".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Jeśli chcesz, możesz wprowadzić zmiaany - nie mam zbyt wiele kompetencji do edycji html/css.

'<code>' . $secret . '</code>',
'<code>' . $recoveryCode . '</code>',
'<br><div class="qa-error">',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

J.w. czy ten <br> jest potrzebny?

'</div><br>'
],
$note
);

$result['fields'][] = [
'style' => 'tall',
'type' => 'static',
'note' => $note
];
}

return $result;
}

private function render2FAEnabledForm($date): array
{
return [
'fields' => [
'old' => [
'label' => qa_lang('plugin_2fa/plugin_is_enabled'),
'tags' => 'name="oldpassword" disabled',
'value' => $date,
'type' => 'input'
],
],
'buttons' => [
'enable' => [
'label' => qa_lang('plugin_2fa/disable_plugin')
]
],
'hidden' => [
'dodisable2fa' => '1',
'code' => qa_get_form_security_code('2faform')
]
];
}

private function render2FADisabledForm(): array
{
return [
'fields' => [
'old' => [
'label' => qa_lang('plugin_2fa/plugin_is_disabled'),
'value' => '',
'type' => 'static'
]
],
'buttons' => [
'enable_2fa' => [
'label' => qa_lang('plugin_2fa/enable_plugin')
]
],
'hidden' => [
'doenable2fa' => '1',
'code' => qa_get_form_security_code('2faform')
]
];
}

private function getUser(): ?array
{
$userId = qa_get_logged_in_userid();

if (!isset($userId)) {
qa_redirect('login');
}

return $this->getUserQuery($userId);
}

private function setTime()
{
$time = new DateTime('now');
$formatter = new IntlDateFormatter('pl_PL', IntlDateFormatter::SHORT, IntlDateFormatter::SHORT);
$formatter->setPattern('EEEE, dd MMMM yyyy, HH:mm:ss');

return $formatter->format($time);
}

private function prepareGoogleAuthenticator(?array $userAccount): array
{
$userActive2fa = $userAccount['2fa_enabled'];
if (qa_clicked('doenable2fa')) {
$this->init = new GoogleAuthenticator;
$this->init->createSecret();
$recoveryCode = $this->init->getRandomRecoveryCode();
$secret = $this->init->getSecret();
$userActive2fa = $this->updateUserEnable2FA($userAccount, true, $secret, $recoveryCode);
} elseif (qa_clicked('dodisable2fa')) {
$userActive2fa = $this->updateUserEnable2FA($userAccount, false);
}

return [$userActive2fa ?? false, $recoveryCode ?? false, $secret ?? false];
}
}
Loading