diff --git a/bin/module.php b/bin/module.php
new file mode 100755
index 0000000000000000000000000000000000000000..f69cafbdf80cb24dd58a8d714de16d6b14c9896a
--- /dev/null
+++ b/bin/module.php
@@ -0,0 +1,77 @@
+#!/usr/bin/env php
+<?php
+
+/* This is the base directory of the simpleSAMLphp installation. */
+$baseDir = dirname(dirname(__FILE__));
+
+/* Add library autoloader. */
+require_once($baseDir . '/lib/_autoload.php');
+
+if (count($argv) < 1) {
+	echo "Wrong number of parameters. Run:   " . $argv[0] . " [install,show] url [branch]\n"; exit;
+}
+
+// Needed in order to make session_start to be called before output is printed.
+$session = SimpleSAML_Session::getInstance();
+$config = SimpleSAML_Configuration::getConfig('config.php');
+
+
+$action = $argv[1];
+
+
+function getModinfo() {
+	global $argv;
+	if (count($argv) < 2)
+		throw new Exception('Missing second parameter: URL/ID');
+	return sspmod_core_ModuleDefinition::load($argv[2]);
+}
+
+function getBranch() {
+	global $argv;
+	if (isset($argv[3])) return $argv[3];
+	return NULL;
+}
+
+switch($action) {
+	case 'install':
+	 	$mod = getModinfo();
+		$installer = new sspmod_core_ModuleInstaller($mod);
+		$installer->install(getBranch());
+		break;
+	
+	case 'remove': 
+	 	$mod = getModinfo();
+		$installer = new sspmod_core_ModuleInstaller($mod);
+		$installer->remove(getBranch());
+		break;
+		
+	case 'upgrade': 
+	 	$mod = getModinfo();
+		$installer = new sspmod_core_ModuleInstaller($mod);
+		$installer->upgrade(getBranch());
+		break;
+	
+	case 'upgrade-all' :
+		$mdir = scandir($config->getBaseDir() . 'modules/');
+		foreach($mdir AS $md) {
+			if (!sspmod_core_ModuleDefinition::validId($md)) continue;
+			if (!sspmod_core_ModuleDefinition::isDefined($md)) continue;
+			$moduledef = sspmod_core_ModuleDefinition::load($md, 'remote');
+			$installer = new sspmod_core_ModuleInstaller($moduledef);
+			
+			if ($moduledef->updateExists() || $moduledef->alwaysUpdate()) {
+				echo "Upgrading [" . $md . "]\n";
+				$installer->upgrade();				
+			} else {
+				echo "No updates available for [" . $md . "]\n";
+			}
+		}
+		break;
+			
+	default: 
+		throw new Exception('Unknown action [' . $action . ']');
+}
+
+
+
+
diff --git a/modules/core/lib/ModuleDefinition.php b/modules/core/lib/ModuleDefinition.php
new file mode 100644
index 0000000000000000000000000000000000000000..9499ed408f2a59cb2e20fe51605d2dac1eb1f1c5
--- /dev/null
+++ b/modules/core/lib/ModuleDefinition.php
@@ -0,0 +1,122 @@
+<?php
+
+class sspmod_core_ModuleDefinition {
+	
+	public $def;
+	private static $cache;
+	
+	private function __construct($def) {
+		$this->def = $def;
+		$this->requireValidIdentifier();
+	}
+	
+	public static function validId($id) {
+		return preg_match('|^[a-zA-Z_]+$|', $id);
+	}
+	
+	public static function isDefined($id) {
+		$config = SimpleSAML_Configuration::getConfig('config.php');
+		$basedir = $config->getBaseDir();
+		$filename = $basedir . 'modules/' . $id . '/definition.json';
+		return (file_exists($filename));
+	}
+	
+	public static function load($id, $force = NULL) {
+		
+		if (isset($cache[$id])) return $cache[$id];
+		
+		if (self::validId($id)) {
+			$config = SimpleSAML_Configuration::getConfig('config.php');
+			$basedir = $config->getBaseDir();
+			$filename = $basedir . 'modules/' . $id . '/definition.json';
+			if (!file_exists($filename))
+				throw new Exception('Could not read definition file for module [' . $id . '] : ' . $filename);
+			$defraw = file_get_contents($filename);
+			$def = json_decode($defraw, TRUE);
+			
+		} elseif(preg_match('|^http(s)?://.*$|', $id)) {
+			$defraw = file_get_contents($id);
+			$def = json_decode($defraw, TRUE);
+		} else {
+			throw new Exception('Could not resolve [' . $id . '] as URL nor module identifier.');
+		}
+		$cache[$id] = new sspmod_core_ModuleDefinition($def);
+		
+		
+		
+		if (isset($force)) {
+			if ($force === 'local') {
+				if(preg_match('|^http(s)?://.*$|', $id)) {
+					return self::load($def['id']);
+				}
+			} elseif($force === 'remote') {
+				if (self::validId($id)) {
+					if (!isset($def['definition']))
+						throw new Exception('Could not load remote definition file for module [' . $id . ']');
+					return self::load($def['definition']);
+				}
+			}
+		}
+		
+		return $cache[$id];
+	}
+	
+	private function requireValidIdentifier() {
+		if (!isset($this->def['id']))
+			throw new Exception('Missing [id] value in module definition');
+		if (!preg_match('|^[a-zA-Z_]+$|', $this->def['id'])) 
+			throw new Exception('Illegal characters in [id] in module definition');
+	}
+	
+	public function getVersion($branch = NULL) {
+		if (!isset($this->def['access']))	throw new Exception('Missing [access] statement in module definition');
+		if (!isset($this->def['branch']))	throw new Exception('Missing [branch] statement in module definition');
+		
+		if (is_null($branch)) $branch = $this->def['branch'];
+		
+		if (!isset($this->def['access'][$branch])) throw new Exception('Missing [access] information for branch [' . var_export($branch, TRUE) . ']');
+		if (!isset($this->def['access'][$branch]['version'])) throw new Exception('Missing version information in [access] in branch [' . var_export($branch, TRUE) . ']');
+		
+		return $this->def['access'][$branch]['version'];
+	}
+	
+	public function alwaysUpdate($branch = NULL) {
+		$access = $this->getAccess($branch);
+		if ($access['type'] === 'svn') return TRUE;
+		return FALSE;
+	}
+	
+	public function getBranch($branch = NULL) {
+		if (!isset($this->def['branch']))	throw new Exception('Missing [branch] statement in module definition');
+		if (is_null($branch)) $branch = $this->def['branch'];
+		return $branch;
+	}
+	
+	public function updateExists($branch = NULL) {
+		$branch = $this->getBranch($branch);
+		
+		$localDef = self::load($this->def['id'], 'local');
+		$thisVersion = $localDef->getVersion($branch);
+
+		$remoteDef = self::load($this->def['definition'], 'remote');
+		$remoteVersion = $remoteDef->getVersion($branch);
+		
+		#echo ' Comparing versions localĂ‚ [' . $thisVersion . '] and remote [' . $remoteVersion . ']' . "\n";
+		
+		return version_compare($remoteVersion, $thisVersion, '>');
+	}
+	
+	
+	public function getAccess($branch = NULL) {
+		if (!isset($this->def['access']))	throw new Exception('Missing [access] statement in module definition');
+		if (!isset($this->def['branch']))	throw new Exception('Missing [branch] statement in module definition');
+		
+		if (is_null($branch)) $branch = $this->def['branch'];
+		
+		if (!isset($this->def['access'][$branch])) throw new Exception('Missing [access] information for branch [' . var_export($branch, TRUE) . ']');
+
+		return $this->def['access'][$branch];
+	}
+	
+	
+}
\ No newline at end of file
diff --git a/modules/core/lib/ModuleInstaller.php b/modules/core/lib/ModuleInstaller.php
new file mode 100644
index 0000000000000000000000000000000000000000..d18f59b88456a815c333ec8899495a6c1b9d292a
--- /dev/null
+++ b/modules/core/lib/ModuleInstaller.php
@@ -0,0 +1,188 @@
+<?php
+
+class sspmod_core_ModuleInstaller {
+	
+	public $module;
+	
+	public function __construct(sspmod_core_ModuleDefinition $module) {
+		$this->module = $module;
+		
+	}
+	
+	public function remove($branch = NULL) { 
+		$access = $this->module->getAccess($branch);
+		
+		switch($access['type']) {
+			// case 'svn' :
+			// 	$this->requireInstalled();
+			// 	$this->remove($access);
+			// 	break;
+			
+			default:
+				$this->requireInstalled();
+				$this->removeModuleDir($access);
+				break;
+				
+		}
+	}
+	
+	public function install($branch = NULL) {
+		
+		$access = $this->module->getAccess($branch);
+		
+		switch($access['type']) {
+			case 'svn' :
+				$this->requireNotInstalled();
+				$this->svnCheckout($access);
+				$this->enable();
+				$this->prepareConfig();
+				break;
+			
+			case 'zip' :
+				$this->requireNotInstalled();
+				$this->zipLoad($access);
+				$this->enable();
+				$this->prepareConfig();
+				break;
+
+			
+			default:
+				throw new Exception('Unknown access method type. Not one of [zip,tgz,svn]');
+				
+		}
+		
+	}
+	
+	public static function exec($cmd) {
+		echo ' $ ' . $cmd . "\n";
+		$output = shell_exec(escapeshellcmd($cmd));	
+		
+		if (empty($output)) return;
+		
+		$oa = explode("\n", $output);
+		
+		foreach($oa AS $ol) {
+			echo ' > ' . $ol . "\n";			
+		}
+
+	}
+	
+	public function upgrade($branch = NULL) {
+		
+		$access = $this->module->getAccess($branch);
+		
+		switch($access['type']) {
+			case 'svn' :
+				$this->requireInstalled();
+				$this->svnUpdate($access);
+				$this->enable();
+				$this->prepareConfig();
+				break;
+				
+			case 'zip' :
+				$this->requireInstalled();
+				$this->zipLoad($access);
+				$this->enable();
+				$this->prepareConfig();
+				break;
+			
+			default:
+				throw new Exception('Unknown access method type. Not one of [zip,tgz,svn]');
+				
+		}
+		
+	}
+	
+	public function dirExists() {
+		$config = SimpleSAML_Configuration::getConfig('config.php');
+		$basedir = $config->getBaseDir();
+		
+		$dir = $basedir . 'modules/' . $this->module->def['id'];
+		
+		return (file_exists($dir) && is_dir($dir));
+	}
+	
+	public function requireValidURL($url) {
+		if (!preg_match('|http(s)?://[a-zA-Z0-9_-/.]|', $url)) 
+			throw new Exception('Invalid URL [' . $url . ']');
+	}
+	
+	public function requireNotInstalled() {
+		if ($this->dirExists())
+			throw new Exception('The module [' . $this->module->def['id'] . '] is already installed.');
+	}
+	
+	public function requireInstalled() {
+		if (!$this->dirExists())
+			throw new Exception('The module [' . $this->module->def['id'] . '] is not installed.');
+	}
+	
+	public function svnCheckout($access) {
+		$config = SimpleSAML_Configuration::getConfig('config.php');
+		$basedir = $config->getBaseDir();
+		$cmd = "svn co " . escapeshellarg($access['url']) . " " . $basedir . "modules/" . $this->module->def['id'];
+		self::exec($cmd);
+	}
+	
+	public function svnUpdate($access) {
+		$config = SimpleSAML_Configuration::getConfig('config.php');
+		$basedir = $config->getBaseDir();
+		$cmd = "svn up " . $basedir . "modules/" . $this->module->def['id'];
+		self::exec($cmd);
+	}
+	
+	public function removeModuleDir($access) {
+		$config = SimpleSAML_Configuration::getConfig('config.php');
+		$basedir = $config->getBaseDir();
+		$cmd = "rm -rf " . $basedir . "modules/" . $this->module->def['id'];
+		self::exec($cmd);
+	}
+	
+	public function enable() {
+		$config = SimpleSAML_Configuration::getConfig('config.php');
+		$basedir = $config->getBaseDir();
+		
+		$this->requireInstalled();
+		
+		$cmd = "touch " . $basedir . "modules/" . $this->module->def['id'] . '/enable';
+		self::exec($cmd);
+	}
+	
+	public function prepareConfig() {
+		$config = SimpleSAML_Configuration::getConfig('config.php');
+		$basedir = $config->getBaseDir();
+		
+		$this->requireInstalled();
+		
+		$dir = $basedir . "modules/" . $this->module->def['id'] . '/config-templates';
+		if (!file_exists($dir)) return;
+		
+		$files = scandir($dir);
+		foreach($files AS $file) {
+			if(!preg_match('|^.*\.php|', $file)) continue;
+			
+			if (file_exists($basedir . 'config/' . $file)) {
+				echo "Configuration file [" . $file . "] already exists. Will not overwrite existing file.\n";
+				continue;
+			}
+			
+			$cmd = 'cp ' . $dir . '/' . $file . ' ' . $basedir . 'config/';
+			self::exec($cmd);	
+		}
+	}
+	
+	public function zipLoad($access) {
+		$config = SimpleSAML_Configuration::getConfig('config.php');
+		$basedir = $config->getBaseDir();
+		
+		$zipfile = $access['url'];
+		$localfile = tempnam(sys_get_temp_dir(), 'ssp-module-');
+		$filecontents = file_get_contents($zipfile);
+		file_put_contents($localfile, $filecontents);
+		
+		$cmd = "unzip -qo " . escapeshellarg($localfile) . " -d " . $basedir . "modules/";
+		self::exec($cmd);
+
+	}
+	
+}
\ No newline at end of file
diff --git a/modules/modinfo/dictionaries/modinfo.definition.json b/modules/modinfo/dictionaries/modinfo.definition.json
index 2391a52a4af3c9a1ba20a016680e07d81beb681b..1c20ab7ae9b210649c9b9fd8e3cb4685e00a730b 100644
--- a/modules/modinfo/dictionaries/modinfo.definition.json
+++ b/modules/modinfo/dictionaries/modinfo.definition.json
@@ -13,5 +13,15 @@
 	},
 	"modlist_disabled": {
 		"en": "Disabled"
+	},
+	"latest_version" : {
+		"en" : "Updated"
+	},
+	"update_exists" : {
+		"en" : "Update available"
+	},
+	"version" : {
+		"en" : "Version"
 	}
+
 }
\ No newline at end of file
diff --git a/modules/modinfo/templates/modlist.php b/modules/modinfo/templates/modlist.php
index d20df10891589d4c15545e3a3ae67295dcb0c03e..253adc7fe5da93aa895476dee1ecb844b1cb76df 100644
--- a/modules/modinfo/templates/modlist.php
+++ b/modules/modinfo/templates/modlist.php
@@ -10,15 +10,29 @@ $this->includeAtTemplateBase('includes/header.php');
 
 <h2><?php echo($this->data['header']); ?></h2>
 
-<table>
+<table class="modules" style="width: 100%">
 <tr>
-<th><?php echo($this->t('{modinfo:modinfo:modlist_name}')); ?></th>
-<th><?php echo($this->t('{modinfo:modinfo:modlist_status}')); ?></th>
+<th colspan="2"><?php echo($this->t('{modinfo:modinfo:modlist_name}')); ?></th>
+<th ><?php echo($this->t('{modinfo:modinfo:modlist_status}')); ?></th>
+<th colspan="2"><?php echo($this->t('{modinfo:modinfo:version}')); ?></th>
 </tr>
 <?php
+
+$i = 0;
 foreach($this->data['modules'] as $id => $info) {
-	echo('<tr>');
-	echo('<td>' . htmlspecialchars($id) . '</td>');
+	echo('<tr class="' . ($i++ % 2 == 0 ? 'odd' : 'even') . '">');
+	
+	
+	if (isset($info['def'])) {
+		echo('<td><a href="http://simplesamlphp.org/modules/' . htmlspecialchars($id) . '">' . htmlspecialchars($info['def']->def['name']) . '</a></td>');		
+	} else {
+		echo('<td> </td>');
+	}
+	
+	
+	echo('<td><tt>' . htmlspecialchars($id) . '</tt></td>');
+	
+	
 	if($info['enabled']) {
 		echo('<td><img src="/' . $this->data['baseurlpath'] . 'resources/icons/silk/accept.png" alt="' .
 			htmlspecialchars($this->t('{modinfo:modinfo:modlist_enabled}')) . '" /></td>');
@@ -26,6 +40,22 @@ foreach($this->data['modules'] as $id => $info) {
 		echo('<td><img src="/' . $this->data['baseurlpath'] . 'resources/icons/silk/delete.png" alt="' .
 			htmlspecialchars($this->t('{modinfo:modinfo:modlist_disabled}')) . '" /></td>');
 	}
+	
+	if (isset($info['def'])) {
+		echo('<td>' . htmlspecialchars($info['def']->getVersion()) . ' (' .htmlspecialchars($info['def']->getBranch()) . ')</td>');	
+		if ($info['def']->updateExists()) {
+			echo('<td><img style="display: inline" src="/' . $this->data['baseurlpath'] . 'resources/icons/silk/delete.png" alt="' .
+				htmlspecialchars($this->t('{modinfo:modinfo:update_exists}')) . '" /> ' . 
+				htmlspecialchars($this->t('{modinfo:modinfo:update_exists}')) . '</td>');		
+		} else {
+			echo('<td><img style="display: inline" src="/' . $this->data['baseurlpath'] . 'resources/icons/silk/accept.png" alt="' .
+				htmlspecialchars($this->t('{modinfo:modinfo:latest_version}')) . '" /> ' . 
+				htmlspecialchars($this->t('{modinfo:modinfo:latest_version}')) . '</td>');
+		}
+	} else {
+		echo('<td colspan="2"> </td>');
+	}
+	
 	echo('</tr>');
 }
 ?>
diff --git a/modules/modinfo/www/index.php b/modules/modinfo/www/index.php
index 74897f025f654b710bb8558bd987ad13a8e1c942..d8b4547fcef0ea2e203aa21b8dd09e31bdd8cc4c 100644
--- a/modules/modinfo/www/index.php
+++ b/modules/modinfo/www/index.php
@@ -9,7 +9,19 @@ foreach($modules as $m) {
 	$modinfo[$m] = array(
 		'enabled' => SimpleSAML_Module::isModuleEnabled($m),
 	);
+	if (sspmod_core_ModuleDefinition::isDefined($m)) {
+		$modinfo[$m]['def'] = sspmod_core_ModuleDefinition::load($m);
+	}
+
+}
+
+function cmpa($a, $b) {
+	
+    if (isset($a['def']) && !isset($b['def'])) return -1;
+    if (isset($b['def']) && !isset($a['def'])) return 1;
+	return 0;
 }
+uasort($modinfo, 'cmpa');
 
 $config = SimpleSAML_Configuration::getInstance();
 $t = new SimpleSAML_XHTML_Template($config, 'modinfo:modlist.php');
diff --git a/www/resources/default.css b/www/resources/default.css
index b3e66f69d459ba7f9bc6f65076faaa76ff6689f0..984304fed3ddf28a0f800ea40822cd20440a1b65 100644
--- a/www/resources/default.css
+++ b/www/resources/default.css
@@ -276,6 +276,15 @@ div.preferredidp {
 	padding: 2px 2em 2px 2em;	
 }
 
+table.modules {
+	border-collapse: collapse;
+}
+table.modules tr td {
+	border-bottom: 1px solid #ddd;
+}
+table.modules tr.even td {
+	background: #f0f0f0;
+}
 
 /* Attribute presentation in example page */
 table.attributes {