Commit d04e0bc7 authored by Thomas Müller's avatar Thomas Müller

Ready for owncloud 10.0+

parent fac1c374
<?xml version="1.0"?>
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance">
<id>sociallogin</id>
<name>Social Login</name>
<summary>Social login via OAuth or OpenID</summary>
......@@ -11,20 +10,15 @@
<namespace>SocialLogin</namespace>
<category>integration</category>
<category>social</category>
<website>https://github.com/zorn-v/nextcloud-social-login</website>
<bugs>https://github.com/zorn-v/nextcloud-social-login/issues</bugs>
<repository>https://github.com/zorn-v/nextcloud-social-login</repository>
<screenshot>https://raw.githubusercontent.com/zorn-v/nextcloud-social-login/master/appinfo/screenshot.png</screenshot>
<website>https://github.com/owncloud/sociallogin</website>
<bugs>https://github.com/owncloud/sociallogin/issues</bugs>
<repository>https://github.com/owncloud/sociallogin</repository>
<screenshot>https://raw.githubusercontent.com/owncloud/sociallogin/master/appinfo/screenshot.png</screenshot>
<dependencies>
<nextcloud min-version="12" max-version="14" />
<owncloud min-version="10.0" max-version="11.0.0.0" />
</dependencies>
<settings>
<admin>OCA\SocialLogin\Settings\AdminSettings</admin>
<admin-section>OCA\SocialLogin\Settings\AdminSection</admin-section>
<admin>OCA\SocialLogin\Settings\AdminSettings</admin>
<personal>OCA\SocialLogin\Settings\PersonalSettings</personal>
</settings>
<repair-steps>
<post-migration>
<step>OCA\SocialLogin\Migration\SeparateProvidersNameAndTitle</step>
</post-migration>
</repair-steps>
</info>
<?php
use OCA\SocialLogin\Controller\SettingsController;
$controller = \OC::$server->query(SettingsController::class);
return $controller->renderPersonal();
......@@ -37,8 +37,6 @@ class Application extends App
$this->config = $this->query(IConfig::class);
\OCP\App::registerPersonal($this->appName, 'appinfo/personal');
$this->query(IUserManager::class)->listen('\OC\User', 'preDelete', [$this, 'preDeleteUser']);
$userSession = $this->query(IUserSession::class);
......
......@@ -2,11 +2,14 @@
namespace OCA\SocialLogin\Controller;
use OC\Authentication\Token\IToken;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IRequest;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserSession;
use OCP\IUserManager;
use OCP\IURLGenerator;
......@@ -45,9 +48,10 @@ class LoginController extends Controller
private $l;
/** @var SocialConnectDAO */
private $socialConnect;
/** @var ILogger */
private $logger;
public function __construct(
public function __construct(
$appName,
IRequest $request,
IConfig $config,
......@@ -59,7 +63,8 @@ class LoginController extends Controller
IGroupManager $groupManager,
ISession $session,
IL10N $l,
SocialConnectDAO $socialConnect
SocialConnectDAO $socialConnect,
ILogger $logger
) {
parent::__construct($appName, $request);
$this->config = $config;
......@@ -72,7 +77,8 @@ class LoginController extends Controller
$this->session = $session;
$this->l = $l;
$this->socialConnect = $socialConnect;
}
$this->logger = $logger;
}
/**
* @PublicPage
......@@ -164,7 +170,8 @@ class LoginController extends Controller
'user_info_url' => $prov['userInfoUrl'],
'api_base_url' => '',
]),
];
'id.scope' => $prov['idScope'] ?? null
];
break;
}
}
......@@ -199,7 +206,7 @@ class LoginController extends Controller
'profile_url' => $prov['profileUrl'],
]),
'profile_fields' => $prov['profileFields'],
];
];
break;
}
}
......@@ -218,11 +225,12 @@ class LoginController extends Controller
try {
$adapter = new $class($config, null, $this->storage);
$adapter->authenticate();
$profile = $adapter->getUserProfile();
/** @var Profile $profile */
$profile = $adapter->getUserProfile($config['id.scope'] ?? null);
} catch (\Exception $e) {
throw new LoginException($e->getMessage());
}
$profileId = preg_replace('#.*/#', '', rtrim($profile->identifier, '/'));
$profileId = preg_replace('#.*/#', '', rtrim($profile->identifier, '/'));
if (empty($profileId)) {
throw new LoginException($this->l->t('Can not get identifier from provider'));
}
......@@ -230,12 +238,53 @@ class LoginController extends Controller
if (strlen($uid) > 64) {
$uid = $provider.'-'.md5($profileId);
}
return $this->login($uid, $profile);
return $this->login($uid, $profile, $config['id.scope']);
}
private function login($uid, Profile $profile)
/**
* @param $samlNameId
* @return array [string uid, UserInterface backend]
*/
private function determineBackendFor($samlNameId) {
foreach ($this->userManager->getBackends() as $backend) {
$class = get_class($backend);
$this->logger->debug(
"Searching Backend $class for $samlNameId", ['app' => __CLASS__]
);
$userIds = $backend->getUsers($samlNameId, 2);
switch (count($userIds)) {
case 0:
$this->logger->debug(
"Backend $class returned no matching user for $samlNameId",
['app' => __CLASS__]
);
break;
case 1:
$uid = array_pop($userIds);
$this->logger->debug(
"Backend $class returned $uid for $samlNameId",
['app' => __CLASS__]
);
// Found the user in a different backend
return [$uid, $backend];
default:
throw new \InvalidArgumentException("Backend $class returned more than one user for $samlNameId: " . implode(', ', $userIds));
}
}
return [];
}
private function login($uid, Profile $profile, $idScope = null)
{
$user = $this->userManager->get($uid);
if ($idScope !== null && isset($profile->data[$idScope])) {
$user = $this->determineBackendFor($profile->data[$idScope]);
if ($user === null) {
throw new \Exception("No user known for id scope {$profile->data[$idScope]}");
}
$uid = $user[0];
}
$user = $this->userManager->get($uid);
if (null === $user) {
$connectedUid = $this->socialConnect->findUID($uid);
$user = $this->userManager->get($connectedUid);
......@@ -285,7 +334,7 @@ class LoginController extends Controller
$this->config->setUserValue($uid, $this->appName, 'disable_password_confirmation', 1);
}
$this->userSession->completeLogin($user, ['loginName' => $user->getUID(), 'password' => null]);
$this->completeLogin($user, ['loginName' => $user->getUID(), 'password' => null]);
$this->userSession->createSessionToken($this->request, $user->getUID(), $user->getUID());
if ($redirectUrl = $this->session->get('login_redirect_url')) {
......@@ -301,4 +350,39 @@ class LoginController extends Controller
$userAgent = $this->request->getHeader('USER_AGENT');
return $userAgent !== null ? $userAgent : 'unknown';
}
/**
* @param IUser $user
* @param array $loginDetails
* @param bool $regenerateSessionId
* @return true returns true if login successful or an exception otherwise
* @throws LoginException
*/
public function completeLogin(IUser $user, array $loginDetails, $regenerateSessionId = true) {
if (!$user->isEnabled()) {
// disabled users can not log in
// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
$message = \OC::$server->getL10N('lib')->t('User disabled');
throw new LoginException($message);
}
if($regenerateSessionId) {
$this->session->regenerateId();
}
$this->userSession->setUser($user);
// $this->userSession->setLoginName($loginDetails['loginName']);
if(isset($loginDetails['token']) && $loginDetails['token'] instanceof IToken) {
// $this->userSession->setToken($loginDetails['token']->getId());
$firstTimeLogin = false;
} else {
// $this->userSession->setToken(null);
$firstTimeLogin = $user->updateLastLoginTimestamp();
}
$this->userManager->emit('\OC\User', 'postLogin', [$user, $loginDetails['password']]);
if($this->userSession->isLoggedIn()) {
$this->userSession->prepareUserLogin($firstTimeLogin, $regenerateSessionId);
return true;
}
$message = \OC::$server->getL10N('lib')->t('Login canceled by app');
throw new LoginException($message);
}
}
......@@ -104,55 +104,6 @@ class SettingsController extends Controller
}
}
public function renderPersonal()
{
Util::addScript($this->appName, 'personal');
$uid = $this->userSession->getUser()->getUID();
$params = [
'providers' => [],
'connected_logins' => [],
'action_url' => $this->urlGenerator->linkToRoute($this->appName.'.settings.savePersonal'),
'allow_login_connect' => $this->config->getAppValue($this->appName, 'allow_login_connect', false),
'disable_password_confirmation' => $this->config->getUserValue($uid, $this->appName, 'disable_password_confirmation', false),
];
if ($params['allow_login_connect']) {
$providers = json_decode($this->config->getAppValue($this->appName, 'oauth_providers', '[]'), true);
if (is_array($providers)) {
foreach ($providers as $name => $provider) {
if ($provider['appid']) {
$params['providers'][ucfirst($name)] = $this->urlGenerator->linkToRoute($this->appName.'.login.oauth', ['provider' => $name]);
}
}
}
$params['providers'] = array_merge($params['providers'], $this->getProviders('openid'));
$params['providers'] = array_merge($params['providers'], $this->getProviders('custom_oidc'));
$params['providers'] = array_merge($params['providers'], $this->getProviders('custom_oauth2'));
$connectedLogins = $this->socialConnect->getConnectedLogins($uid);
foreach ($connectedLogins as $login) {
$params['connected_logins'][$login] = $this->urlGenerator->linkToRoute($this->appName.'.settings.disconnectSocialLogin', [
'login' => $login,
'requesttoken' => Util::callRegister(),
]);
}
}
return (new TemplateResponse($this->appName, 'personal', $params, ''))->render();
}
private function getProviders($providersType)
{
$result = [];
$providers = json_decode($this->config->getAppValue($this->appName, $providersType.'_providers', '[]'), true);
if (is_array($providers)) {
foreach ($providers as $provider) {
$name = $provider['name'];
$title = $provider['title'];
$result[$title] = $this->urlGenerator->linkToRoute($this->appName.'.login.'.$providersType, ['provider' => $name]);
}
}
return $result;
}
/**
* @NoAdminRequired
* @PasswordConfirmationRequired
......
<?php
namespace OCA\SocialLogin\Migration;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
use OCP\IDBConnection;
use OCP\IConfig;
class SeparateProvidersNameAndTitle implements IRepairStep
{
/** @var IConfig */
private $config;
/** @var IDBConnection */
private $db;
public function __construct(IConfig $config, IDBConnection $db)
{
$this->config = $config;
$this->db = $db;
}
public function getName()
{
return 'Separate user configured providers internal name and title. Also removes old unnecessary user config.';
}
public function run(IOutput $output)
{
$this->setProvidersName('openid_providers');
$this->setProvidersName('custom_oidc_providers');
//Removes old user config "password"
$sql = "DELETE FROM `*PREFIX*preferences` WHERE `appid` = 'sociallogin' AND `configkey` = 'password'";
$this->db->executeUpdate($sql);
}
private function setProvidersName($configKey)
{
$providers = json_decode($this->config->getAppValue('sociallogin', $configKey), true);
if (is_array($providers)) {
foreach ($providers as &$provider) {
if (!isset($provider['name'])) {
$provider['name'] = $provider['title'];
}
}
$this->config->setAppValue('sociallogin', $configKey, json_encode($providers));
}
}
}
......@@ -25,7 +25,7 @@ class CustomOpenIDConnect extends OAuth2
return $collection;
}
public function getUserProfile()
public function getUserProfile($idScope = null)
{
$userData = $this->getStoredData('user_data');
$user = json_decode($userData);
......@@ -54,6 +54,21 @@ class CustomOpenIDConnect extends OAuth2
}
}
if ($idScope !== null) {
if($data->get($idScope) === null) {
$profile = new Data\Collection($this->apiRequest($userInfoUrl));
$id = $profile->get($idScope);
if ($id !== null) {
$data->set($idScope, $id);
} else {
throw new \Exception('Requested id scope is unknown!');
}
}
$userProfile->data = [
$idScope => $data->get($idScope)
];
}
return $userProfile;
}
}
<?php
namespace OCA\SocialLogin\Settings;
use OCP\Settings\IIconSection;
use OCP\IURLGenerator;
use OCP\IL10N;
class AdminSection implements IIconSection
{
/** @var string */
private $appName;
/** @var IL10N */
private $l;
/** @var IURLGenerator */
private $urlGenerator;
public function __construct($appName, IL10N $l, IURLGenerator $urlGenerator) {
$this->l = $l;
$this->appName = $appName;
$this->urlGenerator = $urlGenerator;
}
/**
* returns the ID of the section. It is supposed to be a lower case string
*
* @returns string
*/
public function getID() {
return $this->appName; //or a generic id if feasible
}
/**
* returns the translated name as it should be displayed, e.g. 'LDAP / AD
* integration'. Use the L10N service to translate it.
*
* @return string
*/
public function getName() {
return $this->l->t('Social login');
}
/**
* @return int whether the form should be rather on the top or bottom of
* the settings navigation. The sections are arranged in ascending order of
* the priority values. It is required to return a value between 0 and 99.
*/
public function getPriority() {
return 5;
}
/**
* {@inheritdoc}
*/
public function getIcon() {
return $this->urlGenerator->imagePath('core', 'categories/social.svg');
}
}
......@@ -7,6 +7,7 @@ use OCP\Settings\ISettings;
use OCP\IGroupManager;
use OCP\IURLGenerator;
use OCP\IConfig;
use OCP\Template;
use OCP\Util;
class AdminSettings implements ISettings
......@@ -28,8 +29,13 @@ class AdminSettings implements ISettings
$this->groupManager = $groupManager;
}
public function getForm()
{
/**
* The panel controller method that returns a template to the UI
*
* @since 10.0
* @return TemplateResponse | Template
*/
public function getPanel() {
Util::addStyle($this->appName, 'settings');
Util::addScript($this->appName, 'settings');
$paramsNames = [
......@@ -75,27 +81,32 @@ class AdminSettings implements ISettings
$custom_oauth2Providers = [];
}
$params = [
'action_url' => $this->urlGenerator->linkToRoute($this->appName.'.settings.saveAdmin'),
'groups' => $groupNames,
'providers' => $providers,
'openid_providers' => $openIdProviders,
'custom_oidc_providers' => $custom_oidcProviders,
'custom_oauth2_providers' => $custom_oauth2Providers,
];
$template = new Template($this->appName, 'admin');
$template->assign('action_url', $this->urlGenerator->linkToRoute($this->appName.'.settings.saveAdmin'));
$template->assign('groups', $groupNames);
$template->assign('providers', $providers);
$template->assign('openid_providers', $openIdProviders);
$template->assign('custom_oidc_providers', $custom_oidcProviders);
$template->assign('custom_oauth2_providers', $custom_oauth2Providers);
foreach ($paramsNames as $paramName) {
$params[$paramName] = $this->config->getAppValue($this->appName, $paramName);
$template->assign($paramName, $this->config->getAppValue($this->appName, $paramName));
}
return new TemplateResponse($this->appName, 'admin', $params);
}
public function getSection()
{
return $this->appName;
return $template;
}
public function getPriority()
{
return 0;
}
/**
* A string to identify the section in the UI / HTML and URL
*
* @since 10.0
* @return string
*/
public function getSectionID() {
return 'security';
}
}
<?php
namespace OCA\SocialLogin\Settings;
use OCA\SocialLogin\Db\SocialConnectDAO;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IUserSession;
use OCP\Settings\ISettings;
use OCP\IGroupManager;
use OCP\IURLGenerator;
use OCP\IConfig;
use OCP\Template;
use OCP\Util;
class PersonalSettings implements ISettings
{
/** @var string */
private $appName;
/** @var IConfig */
private $config;
/** @var IURLGenerator */
private $urlGenerator;
/**
* @var IUserSession
*/
private $userSession;
/**
* @var SocialConnectDAO
*/
private $socialConnect;
public function __construct($appName, IConfig $config,
IURLGenerator $urlGenerator,
IUserSession $userSession,
SocialConnectDAO $socialConnect)
{
$this->appName = $appName;
$this->config = $config;
$this->urlGenerator = $urlGenerator;
$this->userSession = $userSession;
$this->socialConnect = $socialConnect;
}
/**
* The panel controller method that returns a template to the UI
*
* @since 10.0
* @return TemplateResponse | Template
*/
public function getPanel() {
Util::addScript($this->appName, 'personal');
$uid = $this->userSession->getUser()->getUID();
$params = [
'providers' => [],
'connected_logins' => [],
'action_url' => $this->urlGenerator->linkToRoute($this->appName.'.settings.savePersonal'),
'allow_login_connect' => $this->config->getAppValue($this->appName, 'allow_login_connect', false),
'disable_password_confirmation' => $this->config->getUserValue($uid, $this->appName, 'disable_password_confirmation', false),
];
if ($params['allow_login_connect']) {
$providers = json_decode($this->config->getAppValue($this->appName, 'oauth_providers', '[]'), true);
if (is_array($providers)) {
foreach ($providers as $name => $provider) {
if ($provider['appid']) {
$params['providers'][ucfirst($name)] = $this->urlGenerator->linkToRoute($this->appName.'.login.oauth', ['provider' => $name]);
}
}
}
$params['providers'] = array_merge($params['providers'], $this->getProviders('openid'));
$params['providers'] = array_merge($params['providers'], $this->getProviders('custom_oidc'));
$params['providers'] = array_merge($params['providers'], $this->getProviders('custom_oauth2'));
$connectedLogins = $this->socialConnect->getConnectedLogins($uid);
foreach ($connectedLogins as $login) {
$params['connected_logins'][$login] = $this->urlGenerator->linkToRoute($this->appName.'.settings.disconnectSocialLogin', [
'login' => $login,
'requesttoken' => Util::callRegister(),
]);
}
}
return new TemplateResponse($this->appName, 'personal', $params, '');
}
public function getPriority()
{
return 0;
}
/**
* A string to identify the section in the UI / HTML and URL
*
* @since 10.0
* @return string
*/
public function getSectionID() {
return 'security';
}
private function getProviders($providersType)
{
$result = [];
$providers = json_decode($this->config->getAppValue($this->appName, $providersType.'_providers', '[]'), true);
if (is_array($providers)) {
foreach ($providers as $provider) {
$name = $provider['name'];
$title = $provider['title'];
$result[$title] = $this->urlGenerator->linkToRoute($this->appName.'.login.'.$providersType, ['provider' => $name]);
}
}
return $result;
}
}
......@@ -66,6 +66,11 @@ $providersData = [
'type' => 'text',
'required' => true,
],
'idScope' => [
'title' => 'Scope to be used to identify a user (e.g. LDAP uuid)',
'type' => 'text',
'required' => false
]
]
],
'custom_oauth2' => [
......@@ -125,8 +130,8 @@ $providersData = [
],
];
?>
<div id="sociallogin" class="section">
<form id="sociallogin_settings" action="<?php print_unescaped($_['action_url']) ?>" method="post">
<form id="sociallogin_settings" class="section" action="<?php print_unescaped($_['action_url']) ?>" method="post">
<h2 class="app-name"><?php p($l->t('Social Login')); ?></h2>
<p>
<label for="new_user_group"><?php p($l->t('Default group that all new users belong')); ?></label>
......@@ -222,5 +227,3 @@ $providersData = [
<?php endforeach ?>
</div>
<?php endforeach ?>
</div>
......@@ -3,6 +3,7 @@
/** @var \OCP\IL10N $l */
?>
<div class="section sociallogin-connect">
<h2><?php p($l->t('Social login')); ?></h2>
<form id="sociallogin_personal_settings" action="<?php print_unescaped($_['action_url']) ?>" method="POST">
<input id="disable_password_confirmation" type="checkbox" class="checkbox" name="disable_password_confirmation" value="1" <?php p($_['disable_password_confirmation'] ? 'checked' : '') ?>/>
<label for="disable_password_confirmation"><?php p($l->t('Disable password confirmation on settings change')) ?></label>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment