Skip to content
This repository has been archived by the owner on Nov 25, 2020. It is now read-only.

Commit

Permalink
Rework OTP using locks and new parameters to enable from admin or fro…
Browse files Browse the repository at this point in the history
…m My Account, generate API key automatically and display QRCode for easy configuration.
  • Loading branch information
cdujeu committed Sep 7, 2016
1 parent b60e186 commit 3d75e96
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 25 deletions.
32 changes: 26 additions & 6 deletions core/src/core/src/pydio/Core/Exception/AuthRequiredException.php
Expand Up @@ -20,6 +20,7 @@
*/
namespace Pydio\Core\Exception;

use Pydio\Core\Http\Message\LoggingResult;
use Pydio\Core\Http\Message\UserMessage;
use Pydio\Core\Http\Response\JSONSerializableResponseChunk;
use Pydio\Core\Http\Response\XMLSerializableResponseChunk;
Expand All @@ -28,15 +29,29 @@

defined('AJXP_EXEC') or die('Access not allowed');


/**
* Class AuthRequiredException
* @package Pydio\Core\Exception
*/
class AuthRequiredException extends PydioException implements XMLSerializableResponseChunk, JSONSerializableResponseChunk
{
public function __construct($messageId = "", $messageString = "")
private $loginResult;

/**
* AuthRequiredException constructor.
* @param string $messageId
* @param string $messageString
* @param null $loginResult
*/
public function __construct($messageId = "", $messageString = "", $loginResult = null)
{
if(!empty($messageId)){
$mess = LocaleService::getMessages();
if(isSet($mess[$messageId])) $messageString = $mess[$messageId];
}
if(!empty($loginResult)){
$this->loginResult = $loginResult;
}
parent::__construct($messageString, $messageId);
}

Expand All @@ -61,10 +76,15 @@ public function jsonSerializableKey()
*/
public function toXML()
{
$xml = "<require_auth/>";
if($this->getMessage()){
$error = new UserMessage($this->getMessage(), LOG_LEVEL_ERROR);
$xml.= $error->toXML();
if(!empty($this->loginResult)){
$res = new LoggingResult($this->loginResult, "", "", "");
$xml = $res->toXML();
}else{
$xml = "<require_auth/>";
if($this->getMessage()){
$error = new UserMessage($this->getMessage(), LOG_LEVEL_ERROR);
$xml.= $error->toXML();
}
}
return $xml;
}
Expand Down
134 changes: 118 additions & 16 deletions core/src/plugins/authfront.otp/OtpAuthFrontend.php
Expand Up @@ -26,8 +26,18 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Pydio\Auth\Frontend\Core\AbstractAuthFrontend;
use Pydio\Core\Exception\AuthRequiredException;
use Pydio\Core\Exception\PydioException;
use Pydio\Core\Model\ContextInterface;
use Pydio\Core\Model\UserInterface;
use Pydio\Core\Services\ConfService;
use Pydio\Core\Services\LocaleService;
use Pydio\Core\Services\RolesService;
use Pydio\Core\Services\UsersService;
use Pydio\Core\Utils\Vars\InputFilter;
use Pydio\Core\Utils\Vars\StringHelper;
use Sabre\DAV\StringUtil;
use Zend\Diactoros\Response\JsonResponse;

defined('AJXP_EXEC') or die('Access not allowed');

Expand All @@ -40,11 +50,40 @@ class OtpAuthFrontend extends AbstractAuthFrontend

private $yubicoSecretKey;
private $yubicoClientId;

private $googleEnabled;
private $google;
private $googleLast;

private $yubikey1;
private $yubikey2;

/**
* @param array $configData
*/
function loadConfigs($configData){
parent::loadConfigs($configData);
if(isSet($this->pluginConf["google_enabled_admin"]) && $this->pluginConf["google_enabled_admin"] === true){
$this->pluginConf["google_enabled"] = true;
}
}

/**
* @param ContextInterface $ctx
* @param bool $extendedVersion
* @return \DOMElement[]
*/
function getRegistryContributions(ContextInterface $ctx, $extendedVersion = true){
if($ctx->hasUser() && $ctx->getUser()->getPersonalRole()->filterParameterValue("authfront.otp", "google_enabled_admin", AJXP_REPO_SCOPE_ALL, false)){
if(!$this->manifestLoaded) $this->unserializeManifest();
/** @var \DOMElement $param */
$param = $this->getXPath()->query('server_settings/param[@name="google_enabled"]')->item(0);
$param->setAttribute("expose", "false");
$this->reloadXPath();
}
return parent::getRegistryContributions($ctx, $extendedVersion);
}

/**
* Try to authenticate the user based on various external parameters
* Return true if user is now logged.
Expand All @@ -62,14 +101,15 @@ function tryToLogUser(ServerRequestInterface &$request, ResponseInterface &$resp
if (empty($httpVars) || empty($httpVars["userid"])) {
return false;
} else {
$userid = $httpVars["userid"];
$this->loadConfig($userid);
$userid = InputFilter::sanitize($httpVars["userid"], InputFilter::SANITIZE_EMAILCHARS);
$this->loadConfig(UsersService::getUserById($userid));
// if there is no configuration for OTP, this means that this user don't have OTP
if ((empty($this->google) &&
empty($this->googleLast) &&
empty($this->yubikey1) &&
empty($this->yubikey2))
) {
if ((empty($this->googleEnabled) && empty($this->google) && empty($this->googleLast) && empty($this->yubikey1) && empty($this->yubikey2))) {
return false;
}

if(!empty($this->googleEnabled) && empty($this->google)){
$this->showSetupScreen($userid);
return false;
}

Expand Down Expand Up @@ -136,32 +176,94 @@ function tryToLogUser(ServerRequestInterface &$request, ResponseInterface &$resp
}
}
}
return false;
}

/**
* @param $exceptionMsg
* @throws \Pydio\Core\Exception\AuthRequiredException
*/
protected function breakAndSendError($exceptionMsg)
{
protected function breakAndSendError($exceptionMsg) {
throw new AuthRequiredException($exceptionMsg, "", -1);
}

throw new \Pydio\Core\Exception\AuthRequiredException($exceptionMsg);


/**
* @param $userId
* @throws \Pydio\Core\Exception\UserNotFoundException
*/
private function showSetupScreen($userId){
$userObject = UsersService::getUserById($userId);
$userObject->setLock("otp_show_setup_screen");
$userObject->save("superuser");
}

/**
* @param ServerRequestInterface $requestInterface
* @param ResponseInterface $responseInterface
* @throws AuthRequiredException
* @throws PydioException
*/
public function getConfigurationCode(ServerRequestInterface $requestInterface, ResponseInterface &$responseInterface){

/** @var ContextInterface $ctx */
$ctx = $requestInterface->getAttribute("ctx");
if(!$ctx->hasUser()){
throw new AuthRequiredException();
}
$mess = LocaleService::getMessages();
$uObject = $ctx->getUser();
$params = $requestInterface->getParsedBody();
if(isSet($params["step"]) && $params["step"] === "verify"){

$this->loadConfig($uObject);
if(empty($this->google)){
throw new PydioException($mess["authfront.otp.8"]);
}
$otp = $requestInterface->getParsedBody()["otp"];
if($this->checkGooglePass($uObject->getId(), $otp, $this->google, $this->googleLast)){
$responseInterface = new JsonResponse(["RESULT" => "OK"]);
$uObject->removeLock();
$uObject->save("superuser");
}else{
throw new PydioException($mess["authfront.otp.7"]);
}

}else{
$googleKey = $uObject->getMergedRole()->filterParameterValue("authfront.otp", "google", AJXP_REPO_SCOPE_ALL, '');
if(!empty($googleKey)){
$newKey = $googleKey;
}else{
$newKey = StringHelper::generateRandomString(16);
$newKey = str_replace([0,1,2,3,4,5,6,7,8,9], ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'], $newKey);
$newKey = strtoupper($newKey);
}

$uObject->getPersonalRole()->setParameterValue("authfront.otp", "google", $newKey);
ConfService::getConfStorageImpl()->updateRole($uObject->getPersonalRole(), $uObject);

$responseInterface = new JsonResponse([
"key" => $newKey,
"qrcode" => "otpauth://totp/User:%20".$uObject->getId()."?secret=".$newKey."&issuer=".urlencode(ConfService::getGlobalConf("APPLICATION_TITLE"))
]);
}

}

/**
* @param $userid
* @param UserInterface $userObject
* @throws \Pydio\Core\Exception\UserNotFoundException
*/
private function loadConfig($userid)
private function loadConfig($userObject)
{

$userObject = UsersService::getUserById($userid);

if ($userObject != null) {
$this->google = $userObject->getMergedRole()->filterParameterValue("authfront.otp", "google", AJXP_REPO_SCOPE_ALL, '');
$this->googleLast = $userObject->getMergedRole()->filterParameterValue("authfront.otp", "google_last", AJXP_REPO_SCOPE_ALL, '');

$this->googleEnabled = $userObject->getMergedRole()->filterParameterValue("authfront.otp", "google_enabled", AJXP_REPO_SCOPE_ALL, false);
$this->google = $userObject->getMergedRole()->filterParameterValue("authfront.otp", "google", AJXP_REPO_SCOPE_ALL, '');
$this->googleLast = $userObject->getMergedRole()->filterParameterValue("authfront.otp", "google_last", AJXP_REPO_SCOPE_ALL, '');

$this->yubikey1 = $userObject->getMergedRole()->filterParameterValue("authfront.otp", "yubikey1", AJXP_REPO_SCOPE_ALL, '');
$this->yubikey2 = $userObject->getMergedRole()->filterParameterValue("authfront.otp", "yubikey2", AJXP_REPO_SCOPE_ALL, '');
}
Expand Down
16 changes: 16 additions & 0 deletions core/src/plugins/authfront.otp/configurator.css
@@ -0,0 +1,16 @@
#otp_setup_screen {
font-size: 12px;
}
#otp_setup_screen span.step {
font-size: 14px;
font-weight: 500;
}
#otp_setup_screen .codes {
width: 200px;
text-align: center;
margin: 10px auto;
}
#otp_setup_screen .codes input {
text-align: center;
width: 200px;
}
16 changes: 16 additions & 0 deletions core/src/plugins/authfront.otp/configurator.less
@@ -0,0 +1,16 @@
#otp_setup_screen{
font-size: 12px;
span.step{
font-size: 14px;
font-weight: 500;
}
.codes{
width: 200px;
text-align: center;
margin: 10px auto;
input{
text-align: center;
width: 200px;
}
}
}
11 changes: 11 additions & 0 deletions core/src/plugins/authfront.otp/i18n/en.php
@@ -0,0 +1,11 @@
<?php
$mess = [
"1" => "Google Authenticator Configuration",
"2" => "Your account is configured to use 2-factors authentication using Google Authenticator. ",
"3" => "<span class='step'>Step 1</span> - If not already done, please install the Google Authenticator application on your mobile device. Application is available in the stores for iOS and Android.",
"4" => "<span class='step'>Step 2</span> - Once the application is installed, you can directly scan the QRCode below or manually create an account and enter the API key.",
"5" => "<span class='step'>Step 3</span> - Finally to verify that your GA account is properly configured, please enter the one-time-password provided.",
"6" => "Your google key is already configured. Please contact your administrator to reset it.",
"7" => "Cannot validate code, did you properly set up your Google Authenticator application?",
"8" => "Something went wrong, your key should not be empty.",
];
11 changes: 11 additions & 0 deletions core/src/plugins/authfront.otp/i18n/fr.php
@@ -0,0 +1,11 @@
<?php
$mess = [
"1" => "Configuration de Google Authenticator",
"2" => "Votre compte est configuré pour utiliser l'authentification à deux niveaux avec Google Authenticator.",
"3" => "<span class='step'>Etape 1</span> - Installez l'application Google Authenticator sur votre mobile à partir des store iOS ou Android.",
"4" => "<span class='step'>Etape 2</span> - Depuis l'application, scannez directement le code barre, ou créez une entrée manuellement avec la clé qui est affichée.",
"5" => "<span class='step'>Etape 3</span> - Verifiez la configuration en utilisant le code à utilisation unique qui s'affiche sur l'application.",
"6" => "Votre clé est déjà configurée, veuillez contacter l'administrateur pour la remettre à zéro.",
"7" => "La vérification a échoué, avez vous correctement configuré l'application?",
"8" => "Erreur interne, la clé ne devrait pas être vide.",
];
67 changes: 64 additions & 3 deletions core/src/plugins/authfront.otp/manifest.xml
Expand Up @@ -10,6 +10,7 @@
<resources>
<i18n namespace="authfront.otp" path="plugins/authfront.otp/i18n"/>
<js className="OTP_LoginForm" file="plugins/authfront.otp/class.OTP_LoginForm.js" autoload="true"/>
<css file="plugins/authfront.otp/configurator.css" autoload="true"/>
</resources>
</client_settings>
<server_settings>
Expand All @@ -23,12 +24,72 @@
<global_param name="YUBICO_CLIENT_ID" type="string" label="CONF_MESSAGE[Yubico Client ID]" description="CONF_MESSAGE[Yubico client id attached to your account]" mandatory="false"/>
<param name="yubikey1" type="string" label="CONF_MESSAGE[YubiKey 1]" description="CONF_MESSAGE[YubiKey 1]" mandatory="false"/>
<param name="yubikey2" type="string" label="CONF_MESSAGE[YubiKey 2]" description="CONF_MESSAGE[YubiKey 2]" mandatory="false"/>
<param name="google" type="string" label="CONF_MESSAGE[Google Authenticator]" description="CONF_MESSAGE[Google Authenticator Secret]" mandatory="false"/>
<param name="google_last" type="integer" label="CONF_MESSAGE[Google Authenticator Last]" description="CONF_MESSAGE[Google Authenticator replay protection, do not edit]" mandatory="false" editable="false"/>
<param name="google_enabled_admin" group="CONF_MESSAGE[Google Authenticator]" type="boolean" label="CONF_MESSAGE[Force Google Authenticator]" description="CONF_MESSAGE[Force Google Auth usage without letting the choice to the user.]" mandatory="false" default="false" scope="user,group"/>
<param name="google_enabled" group="CONF_MESSAGE[Google Authenticator]" type="boolean" label="CONF_MESSAGE[Enable Google Authenticator]" description="CONF_MESSAGE[If you enable it for the first time, you will be able to configure Google Authenticator application next time you log in.]" mandatory="false" default="false" scope="user,group" expose="true"/>
<param name="google" group="CONF_MESSAGE[Google Authenticator]" type="string" label="CONF_MESSAGE[Google Authenticator Secret]" description="CONF_MESSAGE[Google Authenticator Secret Key.]" mandatory="false" scope="user"/>
<param name="google_last" group="CONF_MESSAGE[Google Authenticator]" type="integer" label="CONF_MESSAGE[Google Authenticator Last]" description="CONF_MESSAGE[Google Authenticator replay protection, do not edit]" mandatory="false" editable="false"/>
</server_settings>
<class_definition filename="plugins/authfront.otp/OtpAuthFrontend.php" classname="Pydio\Auth\Frontend\OtpAuthFrontend"/>
<registry_contributions>
<external_file filename="plugins/core.auth/standard_auth_actions.xml" include="actions/*" exclude=""/>
<actions>
<action name="otp_show_setup_screen">
<gui src="icon-key" iconClass="icon-key" text="authfront.otp.1" title="authfront.otp.1">
<context dir="true" recycle="false" selection="false"/>
</gui>
<processing>
<clientCallback prepareModal="true" dialogOpenForm="otp_setup_screen" dialogOkButtonOnly="true" dialogSkipButtons="false">
<dialogOnOpen><![CDATA[
PydioApi.getClient().request({get_action:"otp_show_setup_screen"}, function(t){
if(t.responseJSON){
modal.getForm().down("#google_otp").setValue(t.responseJSON.key);
React.render(
React.createElement(ReactQRCode, {
size:200,
value:t.responseJSON.qrcode,
level:'L'
}),
modal.getForm().down("#qrcode")
);
}
});
modal.refreshDialogPosition();
]]></dialogOnOpen>
<dialogOnComplete><![CDATA[
if(!modal.getForm().down("#google_otp_verification").getValue()){
pydio.displayMessage('ERROR', 'Please set up verification code');
return false;
}
PydioApi.getClient().request({
get_action:"otp_show_setup_screen",
step:"verify",
otp:modal.getForm().down("#google_otp_verification").getValue()
}, function(t){
if(t.responseJSON && t.responseJSON.RESULT === "OK"){
location.reload();
}
});
]]></dialogOnComplete>
</clientCallback>
<clientForm id="otp_setup_screen"><![CDATA[
<div id="otp_setup_screen" box_width="500">
<div data:ajxp_message_id="authfront.otp.2">AJXP_MESSAGE[authfront.otp.2]</div>
<div data:ajxp_message_id="authfront.otp.3">AJXP_MESSAGE[authfront.otp.3]</div>
<div data:ajxp_message_id="authfront.otp.4">AJXP_MESSAGE[authfront.otp.4]</div>
<div class="codes">
<div id="qrcode"></div>
<input id="google_otp" type="text"/>
</div>
<div class="verif">
<div data:ajxp_message_id="authfront.otp.5">AJXP_MESSAGE[authfront.otp.5]</div>
<input id="google_otp_verification" type="text"/>
</div>
</div>
]]></clientForm>
<serverCallback methodName="getConfigurationCode"/>
</processing>
</action>
</actions>
<client_configs>
<template element="ajxp_desktop" name="otp_script" position="bottom"><![CDATA[
<script>
Expand Down Expand Up @@ -58,4 +119,4 @@
]]></template>
</client_configs>
</registry_contributions>
</authdriver>
</authdriver>

0 comments on commit 3d75e96

Please sign in to comment.