Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API Add change password functionality via LDAP server #5

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ before_script:
script:
- if [[ $PHPUNIT_TEST ]]; then vendor/bin/phpunit; fi
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then phpdbg -qrr vendor/bin/phpunit --coverage-clover=coverage.xml; fi
- if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs --standard=framework/phpcs.xml.dist src/ tests/ ; fi
- if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs --standard=vendor/silverstripe/framework/phpcs.xml.dist src/ tests/ ; fi

after_success:
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then bash <(curl -s https://codecov.io/bash) -f coverage.xml; fi
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"autoload": {
"psr-4": {
"SilverStripe\\LDAP\\": "src/",
"SilverStripe\\LDAP\\Tests\\": "tests/"
"SilverStripe\\LDAP\\Tests\\": "tests/php/"
}
},
"minimum-stability": "dev",
Expand Down
22 changes: 16 additions & 6 deletions docs/en/developer.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Developer guide

This guide will step you through configuring your SilverStripe project to function as a SAML 2.0 Service Provider (SP). It will also show you a typical way to synchronise user details and group memberships from LDAP.
This guide will step you through configuring your SilverStripe project to use an LDAP authentication backend. It will also show you a typical way to synchronise user details and group memberships from LDAP.

As a SilverStripe developer after reading this guide, you should be able to correctly configure your site to integrate with the Identity Provider (IdP). You will also be able to authorise users based on their AD group memberships, and synchronise their personal details.

Expand Down Expand Up @@ -48,7 +48,15 @@ We assume ADFS 2.0 or greater is used as an IdP.

First step is to add this module into your SilverStripe project. You can use composer for this:

composer require "silverstripe/activedirectory:*"
```
composer require silverstripe/ldap
```

You may need to specify a version constraint, e.g.:

```
composer require silverstripe/ldap ~1.0
```

Commit the changes.

Expand Down Expand Up @@ -511,10 +519,12 @@ that the "Username" field must be filled in, otherwise it will not be created, d

You can also programatically create a user. For example:

$member = new \SilverStripe\Security\Member();
$member->FirstName = 'Joe';
$member->Username = 'jbloggs';
$member->write();
```php
$member = new \SilverStripe\Security\Member();
$member->FirstName = 'Joe';
$member->Username = 'jbloggs';
$member->write();
```
Copy link
Contributor Author

@robbieaverill robbieaverill Oct 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More extensive docs rewrite is coming next edit #6


If you enable `update_ldap_from_local` saving a user in the Security section of the CMS or calling `write()` on
a Member object will push up the mapped fields to LDAP, assuming that Member record has a `GUID` field.
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<phpunit bootstrap="framework/tests/bootstrap.php" colors="true">
<phpunit bootstrap="vendor/silverstripe/framework/tests/bootstrap.php" colors="true">
<testsuite name="Default">
<directory>tests/</directory>
</testsuite>
Expand Down
24 changes: 22 additions & 2 deletions src/Authenticators/LDAPAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,10 @@ public function getLoginHandler($link)

public function supportedServices()
{
$result = Authenticator::LOGIN | Authenticator::LOGOUT | Authenticator::RESET_PASSWORD;
$result = Authenticator::LOGIN | Authenticator::LOGOUT | Authenticator::CHECK_PASSWORD;

if ((bool)LDAPService::config()->get('allow_password_change')) {
$result |= Authenticator::CHANGE_PASSWORD;
$result |= Authenticator::RESET_PASSWORD | Authenticator::CHANGE_PASSWORD;
}
return $result;
}
Expand All @@ -219,4 +219,24 @@ public function getChangePasswordHandler($link)
{
return LDAPChangePasswordHandler::create($link, $this);
}

public function checkPassword(Member $member, $password, ValidationResult &$result = null)
{
$result = $result ?: ValidationResult::create();

/** @var LDAPService $service */
$service = Injector::inst()->get(LDAPService::class);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could replace that via Setter Dependency Injection to get the server and allow a custom LDAP Service.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, #7


// Support email or username
$handle = Config::inst()->get(self::class, 'allow_email_login') === 'yes' ? 'Email' : 'Username';

/** @var array $ldapResult */
$ldapResult = $service->authenticate($member->{$handle}, $password);

if (empty($ldapResult['success'])) {
$result->addError($ldapResult['message']);
}

return $result;
}
}
142 changes: 1 addition & 141 deletions src/Authenticators/LDAPChangePasswordHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,11 @@

namespace SilverStripe\LDAP\Authenticators;

use Exception;
use Psr\Log\LoggerInterface;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTP;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\LDAP\Forms\LDAPChangePasswordForm;
use SilverStripe\LDAP\Services\LDAPService;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Member;
use SilverStripe\Security\MemberAuthenticator\ChangePasswordHandler;
use SilverStripe\Security\Security;

class LDAPChangePasswordHandler extends ChangePasswordHandler
{
/**
* @var array Allowed Actions
*/
private static $allowed_actions = [
'changepassword',
'changePasswordForm',
Expand All @@ -28,137 +15,10 @@ class LDAPChangePasswordHandler extends ChangePasswordHandler
/**
* Factory method for the lost password form
*
* @return LDAPChangePasswordForm Returns the lost password form
* @return LDAPChangePasswordForm
*/
public function changePasswordForm()
{
return LDAPChangePasswordForm::create($this, 'ChangePasswordForm');
}

/**
* Change the password
*
* @param array $data The user submitted data
* @param LDAPChangePasswordForm $form
* @return HTTPResponse
*/
public function doChangePassword(array $data, $form)
{
/**
* @var LDAPService $service
*/
$service = Injector::inst()->get(LDAPService::class);
$member = Security::getCurrentUser();
if ($member) {
try {
$userData = $service->getUserByGUID($member->GUID);
} catch (Exception $e) {
Injector::inst()->get(LoggerInterface::class)->error($e->getMessage());

$form->clearMessage();
$form->sessionMessage(
_t(
__CLASS__ . '.NOUSER',
'Your account hasn\'t been setup properly, please contact an administrator.'
),
'bad'
);
return $form->getController()->redirect($form->getController()->Link('changepassword'));
}
$loginResult = $service->authenticate($userData['samaccountname'], $data['OldPassword']);
if (!$loginResult['success']) {
$form->clearMessage();
$form->sessionMessage(
_t(
'SilverStripe\\Security\\Member.ERRORPASSWORDNOTMATCH',
'Your current password does not match, please try again'
),
'bad'
);
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $form->getController()->redirect($form->getController()->Link('changepassword'));
}
}

if (!$member) {
if ($this->getRequest()->getSession()->get('AutoLoginHash')) {
$member = Member::member_from_autologinhash($this->getRequest()->getSession()->get('AutoLoginHash'));
}

// The user is not logged in and no valid auto login hash is available
if (!$member) {
$this->getRequest()->getSession()->clear('AutoLoginHash');
return $form->getController()->redirect($form->getController()->Link('login'));
}
}

// Check the new password
if (empty($data['NewPassword1'])) {
$form->clearMessage();
$form->sessionMessage(
_t(
'SilverStripe\\Security\\Member.EMPTYNEWPASSWORD',
"The new password can't be empty, please try again"
),
'bad'
);

// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $form->getController()->redirect($form->getController()->Link('changepassword'));
} elseif ($data['NewPassword1'] == $data['NewPassword2']) {
// Providing OldPassword to perform password _change_ operation. This will respect the
// password history policy. Unfortunately we cannot support password history policy on password _reset_
// at the moment, which means it will not be enforced on SilverStripe-driven email password reset.
$oldPassword = !empty($data['OldPassword']) ? $data['OldPassword']: null;

/** @var ValidationResult $validationResult */
$validationResult = $service->setPassword($member, $data['NewPassword1'], $oldPassword);

// try to catch connection and other errors that the ldap service can through
if ($validationResult->isValid()) {
Security::setCurrentUser($member);

$this->getRequest()->getSession()->clear('AutoLoginHash');

// Clear locked out status
$member->LockedOutUntil = null;
$member->FailedLoginCount = null;
$member->write();

if (!empty($this->getRequest()->requestVar('BackURL'))
// absolute redirection URLs may cause spoofing
&& Director::is_site_url($this->getRequest()->requestVar('BackURL'))
) {
$url = Director::absoluteURL($this->getRequest()->requestVar('BackURL'));
return $form->getController()->redirect($url);
} else {
// Redirect to default location - the login form saying "You are logged in as..."
$redirectURL = HTTP::setGetVar(
'BackURL',
Director::absoluteBaseURL(),
$form->getController()->Link('login')
);
return $form->getController()->redirect($redirectURL);
}
} else {
$form->clearMessage();
$messages = implode('. ', array_column($validationResult->getMessages(), 'message'));
$form->sessionMessage($messages, 'bad');
// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $form->getController()->redirect($form->getController()->Link('changepassword'));
}
} else {
$form->clearMessage();
$form->sessionMessage(
_t(
'SilverStripe\\Security\\Member.ERRORNEWPASSWORD',
'You have entered your new password differently, try again'
),
'bad'
);

// redirect back to the form, instead of using redirectBack() which could send the user elsewhere.
return $form->getController()->redirect($form->getController()->Link('changepassword'));
}
}
}
Loading