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

Authenticator refactor #1

Merged
merged 8 commits into from Jun 10, 2017
Merged

Authenticator refactor #1

merged 8 commits into from Jun 10, 2017

Conversation

Firesphere
Copy link
Owner

No description provided.

@@ -104,6 +106,8 @@ protected function setUp()
// basis.
BasicAuth::protect_entire_site(false);

$this->logOut();
Copy link
Owner Author

Choose a reason for hiding this comment

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

Tractorcow: Could just use $this->logOut();.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

}

$this->session()->inst_set('loggedInAs', $memberID);
$this->logIn($member);
Copy link
Owner Author

Choose a reason for hiding this comment

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

Should we make this call logIn() on the current IdentityStore, rather than assuming session?
We should see if we can remove the session assignment and see if we can just rely on Security::setCurrentUser().

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done, calling the internal login method.

*/
public function logOut()
{
$this->session()->inst_clear('loggedInAs');
Copy link
Owner Author

@Firesphere Firesphere Jun 1, 2017

Choose a reason for hiding this comment

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

@tractorcow
Should we make this call logOut() on the current IdentityStore, instead of assuming session?
In fact, for testing, maybe we just use Security::setCurrentUser(), and don't modify session / persistant state?
++++

No, we actually need to clear the session for some tests, thus I would prefer to keep it.

Copy link
Owner Author

Choose a reason for hiding this comment

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

As per below the plusses

@@ -228,23 +228,23 @@ public function httpSubmission($request)
// First, try a handler method on the controller (has been checked for allowed_actions above already)
$controller = $this->form->getController();
if ($controller && $controller->hasMethod($funcName)) {
return $controller->$funcName($vars, $this->form, $request);
return $controller->$funcName($vars, $this->form, $request, $this);
Copy link
Owner Author

Choose a reason for hiding this comment

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

If you add a 4th argument to the request handler, please make sure to add it consistently to each of the other callbacks (there are 3 other places $funcName is called).

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done.

@@ -57,7 +59,7 @@
* @property string $DateFormat
* @property string $TimeFormat
Copy link
Owner Author

Choose a reason for hiding this comment

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

Later in this file, Member::logOut() doesn't do a proper logout through the current IdentityStore; But tries to manually set cookies / session.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed, with deprecation notice.

@@ -391,33 +393,6 @@ public function isLockedOut()
}
Copy link
Owner Author

Choose a reason for hiding this comment

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

First of all, I dislike the fact that a can* method returns a validationresult, and not a boolean, as per the convention we've set across the framework.

Secondly, I agree highly with @robbieaverill . canLogin() should only be used when trying to login, but isLockedOut() could be used in many other cases (e.g. an admin dashboard for a list of users), or a summary_fields() column in a group member table.

Making it protected just because we don't currently use it outside of that class isn't a very compelling reason. If it stands alone as a unit-testable method, and isn't used solely as a "can login" synonym (which it is not) then it should be kept public.

Copy link
Owner Author

Choose a reason for hiding this comment

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

2 against 1. Okay :) Moved back to public.

@@ -447,59 +422,41 @@ public function isPasswordExpired()
}

/**
* Logs this member in
* @deprecated Use Security::setCurrentUser() or IdentityStore::logIn()
Copy link
Owner Author

Choose a reason for hiding this comment

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

You can use @deprecated 4.0..5.0 to declare the deprecation assignment version, as well as the target API removal date.

$this->extend('beforeMemberLoggedIn');

self::session_regenerate_id();
Deprecation::notice('5.0.0',
Copy link
Owner Author

Choose a reason for hiding this comment

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

Please use the proper deprecation API, and ensure that your deprecation notice provides the user with the correct upgrade path.

Deprecation::notice("5.0.0", "This method is deprecated and now does not persist. Please use ");

Copy link
Owner Author

Choose a reason for hiding this comment

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

I think this is done here?

if (!$id && !self::$_already_tried_to_auto_log_in) {
self::autoLogin();
$id = Session::get("loggedInAs");
if ($member = Security::getCurrentUser()) {
Copy link
Owner Author

Choose a reason for hiding this comment

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

Please add deprecation API notice to the body of the method. See my other comment on how to add this (add phpdoc, target version, upgrade note).

Copy link
Owner Author

Choose a reason for hiding this comment

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

This method is deprecated, but you are still calling it internally (e.g. from Member:: logged_in_session_exists()). If you deprecate any method make sure you rewrite all usages to avoid deprecated API.

I suggest to deprecate Member::logged_in_session_exists() as well

Copy link
Owner Author

Choose a reason for hiding this comment

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

Agreed, deprecated. Also removed usages.

@@ -850,25 +704,16 @@ public function getValidator()
/**
* Returns the current logged in user
*
* @deprecated use Security::getCurrentUser()
Copy link
Owner Author

Choose a reason for hiding this comment

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

Please use Deprecation API for this method. (see earlier)

SilverStripe\Security\MemberLoginForm:
---
Name: coresecurity
---
Copy link
Owner Author

Choose a reason for hiding this comment

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

Can we please add a Name: coresecurity block to the header, so that users can extend this block using ordered overrides?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

properties:
authenticators:
default: %$SilverStripe\Security\MemberAuthenticator\Authenticator
Copy link
Owner Author

Choose a reason for hiding this comment

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

tractorcow 2 days ago Contributor

As I commented on SilverStripe\Security\AuthenticationRequestFilter, please shift these from injecting service names to injecting the services themselves directly.

setup getAuthenticators() / setAuthenticators() on Security class, and assign an array of named handlers.

My suggestion is that you make these basic getters / setters on a protected $authenticators property, and rename the current getAuthenticators($service) to getApplicableAuthenticators($service).
Firesphere 2 days ago Contributor

That would disable the option to override the default authenticator. If I implement my own authenticator and want to override, I have to awkwardly use the YML, where as

authenticators:
default: My\Authenticator\Class

Makes more sense to me. As this would also leave the option of a custom implementation, to fall back to the default authenticator (say I lost my FB/Twitter login, 2FA thing, etc.)
tractorcow a day ago Contributor

No, it wouldn't. you can key your array just as you have above, and you can override it with a like yml config block. All that you are changing is moving from injecting a class name to a class instance, and the config is moved up one level to the Injector top level key.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done, took some annoying test-fixes though. But meh :D


SilverStripe\Security\AuthenticationRequestFilter:
handlers:
Copy link
Owner Author

Choose a reason for hiding this comment

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

Inject these dependencies directly: you can implement setHandlers() that takes an array of handlers (you can still key them with named identifiers), and move this up into the SilverStripe\Core\Injector\Injector configuration above.

handlers:
session: SilverStripe\Security\MemberAuthenticator\SessionAuthenticationHandler
alc: SilverStripe\Security\MemberAuthenticator\CookieAuthenticationHandler
Copy link
Owner Author

Choose a reason for hiding this comment

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

This is already true in the private statics; We can omit the yml duplication here.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

* @param HTTPRequest $request The request of the visitor that is logging in, to get, for example, cookies.
* @return HTTPResponse $response The response object to modify, if needed.
*/
public function logIn(Member $member, $persistent = false, HTTPRequest $request = null);
Copy link
Owner Author

Choose a reason for hiding this comment

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

Additionally, can we make $persistant and $request optional args? We may need to programmatically invoke logIn() for users where no HTTPRequest is available (e.g. unit tests).

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

* @param HTTPRequest $request The request of the visitor that is logging out, to get, for example, cookies.
* @return HTTPResponse $response The response object to modify, if needed.
*/
public function logOut(HTTPRequest $request = null);
Copy link
Owner Author

Choose a reason for hiding this comment

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

As above, can we please make $request optional argument?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

use SilverStripe\ORM\ValidationException;

class AuthenticationRequestFilter implements RequestFilter, IdentityStore
{
Copy link
Owner Author

Choose a reason for hiding this comment

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

I would feel a lot more comfortable about splitting this into two classes; AuthenticationRequestFilter should implement only RequestFilter, but should take a core IdentityStore as a dependency (e.g. IdentityStoreChain, as it contains chained nested identitystore services).

Copy link
Owner Author

Choose a reason for hiding this comment

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

@tractorcow
Although I agree, I don't think it's current form would allow for that. I would happily discuss it with you and see where we could get to. I don't see much possibility on that perspective here personally.

/**
* @return array|IdentityStore[]
*/
protected function getHandlers()
Copy link
Owner Author

Choose a reason for hiding this comment

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

See my comment on security.yml; Instead of trying to construct them here, simply have getHandlers() / setHandlers(), and DI a list of named services, configured via yaml.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

break;
}
}
} catch (ValidationException $e) {
Copy link
Owner Author

Choose a reason for hiding this comment

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

tractorcow 2 days ago Contributor

We should probably catch only ValidationException; if the exception is already a HTTPResponse_Exception it should bubble up automatically.

This also allows us to get the validation error for the response.
Firesphere 2 days ago Contributor

ValidationException is currently an ORM exception, hence the comment. Happy to switch :)

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done, but might need it's own AuthenticationException?

}

Security::setCurrentUser($member);
// @todo Coupling here isn't ideal.
Copy link
Owner Author

Choose a reason for hiding this comment

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

It's probably ok. :) we can remove this todo.

/**
* @var array available authenticators
*/
protected static $authenticators = [];
Copy link
Owner Author

Choose a reason for hiding this comment

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

Config must be private static not protected static.
However, I've suggested this should be changed from a config var to an injected dependency, so shall we type this as @var Authenticator[] and inject these instead?

Copy link
Owner Author

Choose a reason for hiding this comment

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

As above, now injected. Do you still want it to be private @tractorcow?

* @var array available authenticators
*/
protected static $authenticators = [];

Copy link
Owner Author

Choose a reason for hiding this comment

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

Actually, default_authenticator isn't even used anywhere. Let's remove it entirely.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Removed

/**
* @var Member Currently logged in user (if available)
*/
protected static $currentUser;
Copy link
Owner Author

Choose a reason for hiding this comment

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

Positioning is awkward. Please fix.

Please use protected static; this will otherwise be treated as config.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

* @return Authenticator Class name of Authenticator
* @throws LogicException
*/
protected function getAuthenticator($name = 'default')
Copy link
Owner Author

Choose a reason for hiding this comment

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

Shall we make arg $name = 'default'?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

* @param int $service The type of service that is requested
* @return array Return an array of Authenticator objects
*/
public function getApplicableAuthenticators($service = Authenticator::LOGIN)
Copy link
Owner Author

Choose a reason for hiding this comment

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

You will need to change Security::getAuthenticators() to Security::singleton()->getApplicableAuthenticators(). We are trying to move away from static services to injected services.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

$authenticators = static::$authenticators;

/** @var Authenticator $class */
foreach ($authenticators as $name => $class) {
Copy link
Owner Author

Choose a reason for hiding this comment

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

Re-assignment of array value by-ref makes this code really awkward to understand. If you implement my above suggestion (inject service instances, not register service names) you can simplify this logic greatly.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

Session::set('MemberLoginForm.force_message', 1);
$loginResponse = $me->login();
Security::singleton()->setLoginMessage($message, ValidationResult::TYPE_WARNING);
$loginResponse = Security::singleton()->login(new HTTPRequest('GET', '/'));
Copy link
Owner Author

Choose a reason for hiding this comment

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

I suggest to make Security::login() method take all-optional arguments, meaning $request can be null. At the moment, the $request passed in is pretty much only used to detect a custom login arg (Security/login/cms) so it should be safe enough to make it optional.

Instead of creating a new object, should we just use the singleton (with all DI configured properly?).

I.e.

$loginResponse = Security::singleton()->login();

* @throws LogicException
*/
protected function getAuthenticator()
public static function setCurrentUser($currentUser = null)
Copy link
Owner Author

Choose a reason for hiding this comment

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

I suggest to type-cast the argument as a nullable Member instance.
public static function setCurrentUser(Member $currentUser = null)

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

*/
public function GetLoginForms()
public function getLoginForms()
Copy link
Owner Author

Choose a reason for hiding this comment

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

Shouldn't this be

return $authenticator->getloginHandler($this->Link())->loginForm()

?

This method is ment to return a list of forms, not a list of HTTPResponses
Firesphere 2 days ago Contributor

It seems to be not even called anywhere anymore...
tractorcow a day ago Contributor

There are login forms that have multiple login methods (e.g. oauth / member login). It's ment to display multiple forms, grouped with tabs.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Not sure, I think getLoginForms is now delegated to delegateToHandlers (or whatever we called it). That builds a tabbed group of forms now.


if ($member) { // If we don't have a member, there's not much to log out.
/** @var Authenticator $authenticator */
$authenticator = $this->getAuthenticator('default'); // Always use the default authenticator to log out
Copy link
Owner Author

Choose a reason for hiding this comment

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

Shouldn't this be

$authenticators = $this->getAuthenticators(Authenticator::LOGOUT);

Which is the point of exposing capability-gnostic authenticators?
+++
Yes, yes it should.

* The logout process destroys all traces of the member on the server (not the actual computer user
* at the other end of the line, don't worry)
*
* @package SilverStripe\Security\MemberAuthenticator
Copy link
Owner Author

Choose a reason for hiding this comment

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

We don't need @Package anymore.

if ($this->performLogin($data)) {
return $this->logInUserAndRedirect($data);
}
$this->link = $link;
Copy link
Owner Author

Choose a reason for hiding this comment

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

Changing it to link() is 100x less impact than changing it to getLink(). So we'll probably start with that.

I concur with @sminnee. Given that link($action) is a non-basic getter (it takes an argument), it doesn't fit the convention for get methods.

However, for this pull request, please keep consistent case, unless you are willing to rename the method framework-wide. :)

Copy link
Owner Author

Choose a reason for hiding this comment

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

Leaving as is. It works for now, but worth looking into later.

new FieldList(
new FormAction(
'forgotPassword',
_t('SilverStripe\\Security\\Security.BUTTONSEND', 'Send me the password reset link')
Copy link
Owner Author

Choose a reason for hiding this comment

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

Namespaced localisation was incorrectly de-namespaced here

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done for every _t() I could find

);
$changedPasswordLink = Security::singleton()->Link('changepassword');
return $this->redirect($this->addBackURLParam($changedPasswordLink));
}
}
Copy link
Owner Author

Choose a reason for hiding this comment

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

As above, let's just shift this to RequestHandler

Copy link
Owner Author

Choose a reason for hiding this comment

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

Done

}

/* TO DO - reenable
public function testGenerateCMSLoginForm()
Copy link
Owner Author

Choose a reason for hiding this comment

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

Re-enable this please. :D

Copy link
Owner Author

Choose a reason for hiding this comment

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

What is it exactly supposed to do? The CMSLoginForm is horrible currently, but the test itself is a bit unclear on what it is trying to achieve @dhensby @sminnee @tractorcow @chillu

// If the user has never logged in, then the tempid should be empty
$tempID = $member->TempIDHash;
$this->assertEmpty($tempID);

// If the user logs in then they have a temp id
$member->logIn(true);
Injector::inst()->get(IdentityStore::class)->logIn($member, true, new HTTPRequest('GET', '/'));
Copy link
Owner Author

Choose a reason for hiding this comment

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

Make $request optional and let's stop passing in dummy objects.

@Firesphere Firesphere force-pushed the authenticator-refactor branch 11 times, most recently from c9055f9 to c8eb1cf Compare June 7, 2017 07:31
Sam Minnee and others added 5 commits June 7, 2017 21:11
Further down the line, I'm only returning the `Member` on the doLogin, so it's possible for the Handler or Extending Handler to move to a second step.
Also cleaned up some minor typos I ran in to. Nothing major.

This solution works and is manually tested for now. Supports multiple login forms that end up in the correct handler. I haven't gotten past the handler yet, as I've yet to refactor my Yubiauth implementation.

FIX: Corrections to the multi-login-form support.

Importantly, the system provide a URL-space for each handler, e.g.
“Security/login/default” and “Security/login/other”. This is much
cleaner than identifying the active authenticator by a get parameter,
and means that the tabbed interface is only needed on the very first view.

Note that you can test this without a module simply by loading the
default authenticator twice:

SilverStripe\Security\Security:
  authenticators:
    default: SilverStripe\Security\MemberAuthenticator\Authenticator
    other: SilverStripe\Security\MemberAuthenticator\Authenticator

FIX: Refactor delegateToHandler / delegateToHandlers to have less
duplicated code.
Authenticators is now a map of keys -> service names. The key is used
in things such as URL segments. The “default_authenticator” value has
been replaced with the key “default” in this map, although in time a
default authenticator may not be needed.
IX: Refactor login() to avoid code duplication on single/multiple handlers
IX: Refactor LoginHandler to be more amenable to extension
IX: Fixed permissionFailure hack
his LoginHandler is expected to be the starting point for other
custom authenticators so it should be easier to repurpose components
`of it.
IX: Fix database-is-ready checks in tests.
IX: Fixed MemberAuthenticatorTest to match the new API
IX: Update security URLs in MemberTest
Move to canLogin in the authentication check. Protected isLockedOut

Enable login to be called with a different login service (CMSLogin), enabling CMS Log in. Seems the styling and/or output is still broken.

logOut could be managed from the Authenticator instead of the member
NEW: Add IdentityStore for registering log-in / log-out data
NEW: Add AuthenticationRequestFilter for managing login
NEW: Add Security:setCurrentUser() / Security::getCurrentUser()
NEW: Add FunctionalTest::logOut()
Repairing tests and regressions
Consistently use `Security::getCurrentUser()` and `Security::setCurrentUser()`
Fix for the logout handler to properly logout, some minor wording updates
Remove the login hashes for the member when logging out.
BasicAuth to use `HTTPRequest`
@Firesphere Firesphere force-pushed the authenticator-refactor branch 2 times, most recently from 19f0e57 to 2a76c8e Compare June 7, 2017 09:30
- Move the success and message to a validationresult
- Fix tests for validationresult return
- We need to clear the session in Test logOut method
- Rename to MemberAuthenticator and CMSMemberAuthenticator for consistency.
- Unify all to getCurrentUser on Security
- ChangePasswordHandler removed from Security
- Update SapphireTest for CMS login/logout
- Get the Member ID correctly, if it's an object.
- Only enable "remember me" when it's allowed.
- Add flag to disable password logging
- Remove Subsites coupling, give it an extension hook to disable itself
- Change cascadeLogInTo to cascadeInTo for the logout method logic naming
- Docblocks
- Basicauth config
- Moved the Authenticators from statics to normal
- Moved MemberLoginForm methods to the getFormFields as they make more sense there
- Did some spring-cleaning on the LostPasswordHandler
- Removed the BuildResponse from ChangePasswordHandler after spring cleaning
@Firesphere Firesphere merged commit 62753b3 into master Jun 10, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
1 participant