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

Commit

Permalink
Implement RestAPI token-based authentication. Last todo is to check t…
Browse files Browse the repository at this point in the history
…he nonce is never reused.

Add ORDER parameter in auth frontends plugins to make sure they are called in the correct order.
Introduce a specific /pydio/ path to query API for non-workspaces actions. To be documented and checked when generating workspace slugs.
  • Loading branch information
cdujeu committed May 10, 2014
1 parent fba982b commit 2935a71
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 12 deletions.
2 changes: 1 addition & 1 deletion core/src/core/classes/class.AJXP_PluginsService.php
Expand Up @@ -507,7 +507,7 @@ public function getActivePlugins()
* Retrieve an array of active plugins for type
* @param string $type
* @param bool $unique
* @return array|bool
* @return AJXP_Plugin[]
*/
public function getActivePluginsForType($type, $unique = false)
{
Expand Down
93 changes: 93 additions & 0 deletions core/src/core/classes/class.AJXP_XMLWriter.php
Expand Up @@ -670,4 +670,97 @@ public static function loggingResult($result, $rememberLogin="", $rememberPass =
print("<logging_result value=\"$result\"$remString/>");
}

/**
* Create plain PHP associative array from XML.
*
* Example usage:
* $xmlNode = simplexml_load_file('example.xml');
* $arrayData = xmlToArray($xmlNode);
* echo json_encode($arrayData);
*
* @param \DOMNode $domXml The dom node to load
* @param array $options Associative array of options
* @return array
* @link http://outlandishideas.co.uk/blog/2012/08/xml-to-json/ More info
* @author Tamlyn Rhodes <http://tamlyn.org>
* @license http://creativecommons.org/publicdomain/mark/1.0/ Public Domain
*/
public static function xmlToArray($domXml, $options = array()) {
$xml = simplexml_import_dom($domXml);
$defaults = array(
'namespaceSeparator' => ':',//you may want this to be something other than a colon
'attributePrefix' => '@', //to distinguish between attributes and nodes with the same name
'alwaysArray' => array(), //array of xml tag names which should always become arrays
'autoArray' => true, //only create arrays for tags which appear more than once
'textContent' => '$', //key used for the text content of elements
'autoText' => true, //skip textContent key if node has no attributes or child nodes
'keySearch' => false, //optional search and replace on tag and attribute names
'keyReplace' => false //replace values for above search values (as passed to str_replace())
);
$options = array_merge($defaults, $options);
$namespaces = $xml->getDocNamespaces();
$namespaces[''] = null; //add base (empty) namespace

//get attributes from all namespaces
$attributesArray = array();
foreach ($namespaces as $prefix => $namespace) {
foreach ($xml->attributes($namespace) as $attributeName => $attribute) {
//replace characters in attribute name
if ($options['keySearch']) $attributeName =
str_replace($options['keySearch'], $options['keyReplace'], $attributeName);
$attributeKey = $options['attributePrefix']
. ($prefix ? $prefix . $options['namespaceSeparator'] : '')
. $attributeName;
$attributesArray[$attributeKey] = (string)$attribute;
}
}

//get child nodes from all namespaces
$tagsArray = array();
foreach ($namespaces as $prefix => $namespace) {
foreach ($xml->children($namespace) as $childXml) {
//recurse into child nodes
$childArray = self::xmlToArray($childXml, $options);
list($childTagName, $childProperties) = each($childArray);

//replace characters in tag name
if ($options['keySearch']) $childTagName =
str_replace($options['keySearch'], $options['keyReplace'], $childTagName);
//add namespace prefix, if any
if ($prefix) $childTagName = $prefix . $options['namespaceSeparator'] . $childTagName;

if (!isset($tagsArray[$childTagName])) {
//only entry with this key
//test if tags of this type should always be arrays, no matter the element count
$tagsArray[$childTagName] =
in_array($childTagName, $options['alwaysArray']) || !$options['autoArray']
? array($childProperties) : $childProperties;
} elseif (
is_array($tagsArray[$childTagName]) && array_keys($tagsArray[$childTagName])
=== range(0, count($tagsArray[$childTagName]) - 1)
) {
//key already exists and is integer indexed array
$tagsArray[$childTagName][] = $childProperties;
} else {
//key exists so convert to integer indexed array with previous value in position 0
$tagsArray[$childTagName] = array($tagsArray[$childTagName], $childProperties);
}
}
}

//get text content of node
$textContentArray = array();
$plainText = trim((string)$xml);
if ($plainText !== '') $textContentArray[$options['textContent']] = $plainText;

//stick it all together
$propertiesArray = !$options['autoText'] || $attributesArray || $tagsArray || ($plainText === '')
? array_merge($attributesArray, $tagsArray, $textContentArray) : $plainText;

//return node as array
return array(
$xml->getName() => $propertiesArray
);
}

}
2 changes: 1 addition & 1 deletion core/src/core/classes/class.AuthService.php
Expand Up @@ -119,7 +119,7 @@ public static function preLogUser($remoteSessionId = "")
{
if(self::getLoggedUser() != null) return ;

$frontends = AJXP_PluginsService::getInstance()->getPluginsByType("authfront");
$frontends = AJXP_PluginsService::getInstance()->getActivePluginsForType("authfront");
$index = 0;
foreach($frontends as $frontendPlugin){
if(!$frontendPlugin->isEnabled()) continue;
Expand Down
3 changes: 3 additions & 0 deletions core/src/plugins/authfront.http_basic/manifest.xml
@@ -1,4 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ajxpcore id="authfront.http_basic" enabled="false" label="CONF_MESSAGE[Basic Http FrontEnd]" description="CONF_MESSAGE[Send a basic http request to the user]" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="file:../core.ajaxplorer/ajxp_registry.xsd">
<class_definition filename="plugins/authfront.http_basic/class.BasicHttpAuthFrontend.php" classname="BasicHttpAuthFrontend"/>
<server_settings>
<global_param name="ORDER" type="integer" label="Order" description="Order this plugin with other auth frontends" default="2"/>
</server_settings>
</ajxpcore>
1 change: 1 addition & 0 deletions core/src/plugins/authfront.http_server/manifest.xml
Expand Up @@ -7,5 +7,6 @@
description="Automatically create user if it does not already exists" default="true"/>
<global_param name="AJXP_ADMIN" type="string" label="Admin login"
description="Automatically set this login as pydio administrator"/>
<global_param name="ORDER" type="integer" label="Order" description="Order this plugin with other auth frontends" default="1"/>
</server_settings>
</ajxpcore>
75 changes: 71 additions & 4 deletions core/src/plugins/authfront.keystore/class.KeystoreAuthFrontend.php
Expand Up @@ -36,31 +36,98 @@ function detectVar($varName){
if(isSet($_GET[$varName])) return $_GET[$varName];
if(isSet($_POST[$varName])) return $_POST[$varName];
if(isSet($_SERVER["HTTP_PYDIO_".strtoupper($varName)])) return $_SERVER["HTTP_".strtoupper($varName)];
return "";
}

function tryToLogUser($isLast = false){

$token = $this->detectVar("auth_token");
if(empty($token)){
$this->logDebug(__FUNCTION__, "Empty token", $_POST);
return false;
}
$secret = $this->detectVar("auth_hash");
$this->storage = ConfService::getConfStorageImpl();
if(!is_a($this->storage, "sqlConfDriver")) return false;

$data = null;
$this->storage->simpleStoreGet("keystore", $token, "serial", $data);
if(empty($data)){
$this->logDebug(__FUNCTION__, "Cannot find token in keystore");
return false;
}
$this->logDebug(__FUNCTION__, "Found token in keystore");
$userId = $data["USER_ID"];
$private = $data["PRIVATE"];
if(md5($userId.$private) == $secret){
AuthService::logUser($userId, "", true);
return true;
$server_uri = rtrim(array_shift(explode("?", $_SERVER["REQUEST_URI"])), "/");
list($nonce, $hash) = explode(":", $this->detectVar("auth_hash"));
$replay = hash_hmac("sha256", $server_uri.":".$nonce.":".$private, $token);
$this->logDebug(__FUNCTION__, "Replay is ".$replay);

if($replay == $hash){
$res = AuthService::logUser($userId, "", true);
if($res > 0) return true;
}
return false;

}

/**
* @param String $action
* @param Array $httpVars
* @param Array $fileVars
*/
function authTokenActions($action, $httpVars, $fileVars){

if(AuthService::getLoggedUser() == null) return;
$this->storage = ConfService::getConfStorageImpl();
if(!is_a($this->storage, "sqlConfDriver")) return false;

$user = AuthService::getLoggedUser()->getId();
if(AuthService::getLoggedUser()->isAdmin() && isSet($httpVars["user_id"])){
$user = $httpVars["user_id"];
}
switch($action){
case "keystore_generate_auth_token":

$token = AJXP_Utils::generateRandomString();
$private = AJXP_Utils::generateRandomString();
$data = array("USER_ID" => $user, "PRIVATE" => $private);
if(!empty($httpVars["device"])){
// Revoke previous tokens for this device
$device = $httpVars["device"];
$keys = $this->storage->simpleStoreList("keystore", null, "", "serial", '%"DEVICE_ID";s:'.strlen($device).':"'.$device.'"%');
foreach($keys as $keyId => $keyData){
if($keyData["USER_ID"] != $user) continue;
$this->storage->simpleStoreClear("keystore", $keyId);
}
$data["DEVICE_ID"] = $device;
}
$this->storage->simpleStoreSet("keystore", $token, $data, "serial");
header("Content-type: application/json;");
echo(json_encode(array(
"t" => $token,
"p" => $private)
));

break;

case "keystore_revoke_tokens":

// Invalidate previous tokens
$keys = $this->storage->simpleStoreList("keystore", null, "", "serial", '%"USER_ID";s:'.strlen($user).':"'.$user.'"%');
foreach($keys as $keyId => $keyData){
$this->storage->simpleStoreClear("keystore", $keyId);
}
break;

default:
break;
}





}

}
40 changes: 39 additions & 1 deletion core/src/plugins/authfront.keystore/manifest.xml
@@ -1,5 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<ajxpcore id="authfront.keystore" label="CONF_MESSAGE[Basic Http by Server]" enabled="true" description="CONF_MESSAGE[Basic Http Auth performed by server (e.g. apache htaccess)]"
<ajxpcore id="authfront.keystore" label="CONF_MESSAGE[API Keystore]" enabled="true" description="CONF_MESSAGE[Store API keys/token to simplify REST connection]"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="file:../core.ajaxplorer/ajxp_registry.xsd">
<class_definition filename="plugins/authfront.keystore/class.KeystoreAuthFrontend.php" classname="KeystoreAuthFrontend"/>
<client_settings>
<resources>
<js className="ApikeysPane" file="plugins/authfront.keystore/class.ApikeysPane.js" autoload="true"/>
</resources>
</client_settings>
<server_settings>
<global_param name="ORDER" type="integer" label="Order"
description="Order this plugin with other auth frontends" default="3"/>
</server_settings>
<registry_contributions>
<client_configs>
<component_config className="AjxpTabulator::userdashboard_parameters_tab">
<additional_tab id="apikeys_pane"
tabInfo='{"id":"my-api-data","iconClass":"icon-cog","element":"apikeys_pane","closeable":false,"label":"403","title":"403","dontFocus":true,"position":2}'
paneInfo='{"type":"widget"}'><![CDATA[
<div id="apikeys_pane" ajxpClass="ApikeysPane" class="tabbed_editor">
<div id="generate_token_button">Generate new tokens!</div>
<div id="token_results"></div>
<div id="revoke_tokens_button">Revoke existing token!</div>
</div>
]]></additional_tab>
</component_config>
</client_configs>
<actions>
<action name="keystore_generate_auth_token">
<rightsContext adminOnly="false" noUser="false" read="false" userLogged="true" write="false"/>
<processing>
<serverCallback methodName="authTokenActions" restParams="/device" sdkMethodName="generateAuthToken"/>
</processing>
</action>
<action name="keystore_revoke_tokens">
<rightsContext adminOnly="false" noUser="false" read="false" userLogged="true" write="false"/>
<processing>
<serverCallback methodName="authTokenActions" restParams="/" sdkMethodName="generateAuthToken"/>
</processing>
</action>
</actions>
</registry_contributions>
</ajxpcore>
34 changes: 34 additions & 0 deletions core/src/plugins/core.authfront/class.AbstractAuthFrontend.php
@@ -0,0 +1,34 @@
<?php
/*
* Copyright 2007-2013 Charles du Jeu - Abstrium SAS <team (at) pyd.io>
* This file is part of Pydio.
*
* Pydio is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pydio is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Pydio. If not, see <http://www.gnu.org/licenses/>.
*
* The latest code can be found at <http://pyd.io/>.
*/
defined('AJXP_EXEC') or die( 'Access not allowed');

abstract class AbstractAuthFrontend extends AJXP_Plugin {

/**
* Try to authenticate the user based on various external parameters
* Return true if user is now logged.
*
* @param bool $isLast Whether this is is the last plugin called.
* @return bool
*/
abstract function tryToLogUser($isLast = false);

}
54 changes: 54 additions & 0 deletions core/src/plugins/core.authfront/class.FrontendsLoader.php
@@ -0,0 +1,54 @@
<?php
/*
* Copyright 2007-2013 Charles du Jeu - Abstrium SAS <team (at) pyd.io>
* This file is part of Pydio.
*
* Pydio is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Pydio is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Pydio. If not, see <http://www.gnu.org/licenses/>.
*
* The latest code can be found at <http://pyd.io/>.
*/
defined('AJXP_EXEC') or die( 'Access not allowed');

class FrontendsLoader extends AJXP_Plugin {

public function init($options){

parent::init($options);

// Load all enabled frontend plugins
$fronts = AJXP_PluginsService::getInstance()->getPluginsByType("authfront");
usort($fronts, array($this, "frontendsSort"));
foreach($fronts as $front){
if($front->isEnabled()){
AJXP_PluginsService::setPluginActive($front->getType(), $front->getName(), true);
}
}

}

/**
* @param AJXP_Plugin $a
* @param AJXP_Plugin $b
* @return int
*/
public function frontendsSort($a, $b){
$aConf = $a->getConfigs();
$bConf = $b->getConfigs();
$orderA = intval($aConf["ORDER"]);
$orderB = intval($bConf["ORDER"]);
if($orderA == $orderB) return 0;
return $orderA > $orderB ? 1 : -1;
}

}
7 changes: 7 additions & 0 deletions core/src/plugins/core.authfront/manifest.xml
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ajxpcore id="core.authfront" label="CONF_MESSAGE[Authentification FrontEnd]"
description="CONF_MESSAGE[Actual way to authenticate users (via credentials, certificates, http, etc]"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="file:../core.ajaxplorer/ajxp_registry.xsd">
<class_definition classname="FrontendsLoader" filename="plugins/core.authfront/class.FrontendsLoader.php"/>
</ajxpcore>

0 comments on commit 2935a71

Please sign in to comment.