Permalink
Browse files

Rework OTP using locks and new parameters to enable from admin or fro…

…m My Account, generate API key automatically and display QRCode for easy configuration.
1 parent b60e186 commit 3d75e969a76dee108a6c294c31837c5bac8bfdc2 @cdujeu cdujeu committed Sep 7, 2016
@@ -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;
@@ -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);
}
@@ -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;
}
@@ -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');
@@ -40,12 +50,41 @@ 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.
*
@@ -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;
}
@@ -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, '');
}
@@ -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;
+}
@@ -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;
+ }
+ }
+}
@@ -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.",
+];
@@ -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.",
+];
@@ -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>
@@ -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>
@@ -58,4 +119,4 @@
]]></template>
</client_configs>
</registry_contributions>
-</authdriver>
+</authdriver>

0 comments on commit 3d75e96

Please sign in to comment.