Skip to content

[Security] Add Guard authenticator <supports> method #16835

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

Merged
merged 2 commits into from
Oct 5, 2017
Merged

[Security] Add Guard authenticator <supports> method #16835

merged 2 commits into from
Oct 5, 2017

Conversation

Amo
Copy link
Contributor

@Amo Amo commented Dec 4, 2015

This method will be called before starting an authentication against a guard authenticator.
The authentication will be tried only if the supports method returned true
This improves understanding of code, increase consistency and removes responsability for getCredentials method to decide if the current request should be supported or not.

Q A
Bug fix? no
New feature? yes
BC breaks? yes
Deprecations? yes
Tests pass? yes
Fixed tickets none
License MIT
Doc PR

@Amo
Copy link
Contributor Author

Amo commented Dec 4, 2015

Note: as being not familiar with the recent deprecation mechanisms, i'm not sure i did it well in src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php, line 109

@Amo
Copy link
Contributor Author

Amo commented Dec 4, 2015

About tests, i get, i'm not sure how is this wrong or ok.

$ ./phpunit --filter GuardAuthenticationListenerTest
PHPUnit 4.8.19 by Sebastian Bergmann and contributors.

Testing Symfony Test Suite
.......

Time: 4.1 seconds, Memory: 173.00Mb

OK (7 tests, 22 assertions)

Remaining deprecation notices (6)

Class Mock_GuardAuthenticatorInterface_a5f0cf1d should provide a method <supports(Request $request)>. see GuardAuthenticatorSupportInterface: 6x
    2x in GuardAuthenticationListenerTest::testReturnNullToSkipAuth from Symfony\Component\Security\Guard\Tests\Firewall
    1x in GuardAuthenticationListenerTest::testHandleCatchesAuthenticationException from Symfony\Component\Security\Guard\Tests\Firewall
    1x in GuardAuthenticationListenerTest::testHandleSuccessStopsAfterResponseIsSet from Symfony\Component\Security\Guard\Tests\Firewall
    1x in GuardAuthenticationListenerTest::testHandleSuccess from Symfony\Component\Security\Guard\Tests\Firewall
    1x in GuardAuthenticationListenerTest::testHandleSuccessWithRememberMe from Symfony\Component\Security\Guard\Tests\Firewall

KO symfony

if (method_exists($guardAuthenticator, 'supports')) {
if (true !== $guardAuthenticator->supports($request)) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

You add an interface but you check if the method exists here. Why not simply check if the interface is in use?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was because i did not handled it this way originally. In fact now it would make more sense to check the interface implementation.

@Amo
Copy link
Contributor Author

Amo commented Dec 4, 2015

by the way @weaverryan , that's what i was talking with you this morning.

@Amo Amo changed the title Add Guard authenticator <supports> method [Security] Add Guard authenticator <supports> method Dec 4, 2015
@stof
Copy link
Member

stof commented Dec 5, 2015

I'm not sure we need this. It would probably have to duplicate some of the work done in getCredentials in many time.

@weaverryan what do you think ?

@stof
Copy link
Member

stof commented Dec 5, 2015

@Amo tests are failing because you are using the deprecated way in a non-legacy test.

@weaverryan
Copy link
Member

It's "clearer" in some ways because supports() can return a clean true/false value and then getCredentials() can cause an AuthException if it returns null, which would make it more consistent with getUser and checkCredentials.

I discussed this at the conference - I'm mixed on it - the only negative I see is exactly what Stof mentions, but that is a practical concern. For an API token auth, I'll either have to duplicate code that gets/checks for the API token value in supports() and getUser() or just always return true from supports, which defeats the purpose.

@Amo Can you convince us that this will be a better user experience?

Thx!

@Amo
Copy link
Contributor Author

Amo commented Dec 7, 2015

The idea is to separate the responsabilities of

  • Identifying if the authentication context is elligible/supported
  • Extracting the credentials from the context

and doing so, improve the readability, support of variations and testability of an authentication method.

Supports would be responsible to detect a specific context, guaranteeing that when getCredentials is called, this context has been met.

As @stof said, it could end-up in duplicating the same checks in supports and getCredentials. But, imho, that would occurs if the authentication system supports many context and is poorly designed.

Let's take an example :

Access to /api requires a X-API-Token header.
An authenticator is created :

    // ...

    /**
     * Ensure the given request provides an API token in the headers
     * 
     * @param  Request $request
     * 
     * @return null|mixed
     */
    public function supports(Request $request)
    {
        if (false === $request->headers->has('X-API-Token')) {
            return false;
        }

        return true;
    }

    /**
     * Extract the credentials from the request.
     * 
     * @param  Request $request
     * 
     * @return string
     */
    public function getCredentials(Request $request)
    {
        return $request->headers->get('X-API-Token'));
    }

If additional credentials retrieval methods are required (qs, payload ...) it is likely that the getCredentials method will get many if and else, which would make the code harder to read, maintain, test ...

This separation supports vs getCredentials will spur the developer to create dedicated guard authenticator for each specificities. A common abstract class or a trait could be used to share the common stages of the authentication. But concrete final classes will explicitly indicates with use-cases they manage :

Security
└── Authenticator
    ├── ApiBaseAuthenticator.php
    ├── ApiHeaderAuthenticator.php
    ├── ApiPayloadAuthenticator.php
    └── ApiQueryAuthenticator.php

Where ApiBaseAuthenticator.php handles the common logic for

getUser
checkCredentials
createAuthenticatedToken
onAuthenticationFailure
onAuthenticationSuccess
supportsRememberMe

And each dedicated authenticator provides the dedicated logic to retrieve credentials from header, query string or payload

@Amo
Copy link
Contributor Author

Amo commented Dec 7, 2015

@Amo tests are failing because you are using the deprecated way in a non-legacy test.

How can i avoid it ? googling about Symfony 3 and deprecation conventions didn't give any results.

@weaverryan
Copy link
Member

@Amo I think your arguments are quite good. So, let's see if we can complete this PR :).

Forgetting about BC, how would you want the interface to look in 4.0? You added a new GuardAuthenticatorSupportsInterface, but I imagine that you'd really want supports() to be in the GuardAuthenticatorInterface, correct? There will be some work that will need to be done to create a new interface (that has the new method) and deprecate the old interface.

@weaverryan
Copy link
Member

@Amo about the tests, the GuardAuthenticationListenerTest is mocking the GuardAuthenticatorInterface, which then triggers your deprecation warning. This mock should be changed to GuardAuthenticatorSupportsInterface so that the deprecated functionality isn't triggered (or changed to whatever final interface we decide on that has the new supports() method). There should also be a test that verifies that if you use the original GuardAuthenticatorInterface, then the old behavior is used. Put an @legacy above tests that specifically test how legacy functionality works.

@Amo
Copy link
Contributor Author

Amo commented Dec 8, 2015

ok thanks, i'm on it

@Amo
Copy link
Contributor Author

Amo commented Dec 8, 2015

Tests are still running.
But, (take a deep breath), i moved the interface into the Authenticator sub namespace, Symfony\Component\Security\Guard\Authenticator\GuardAuthenticatorInterface which makes sense to me.
This interface adds the supports method and keep the same the signature as Symfony\Component\Security\Guard\GuardAuthenticatorInterface. It extends Symfony\Component\Security\Guard\GuardAuthenticatorInterfaceas GuardAuthenticatorLegacyInterface to avoid BC break. the Symfony\Component\Security\Guard\GuardAuthenticatorInterface has now a @deprecated annotation and is still used as type hint in Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener alongside the new Symfony\Component\Security\Guard\Authenticator\GuardAuthenticatorInterface interface.

@Amo
Copy link
Contributor Author

Amo commented Dec 8, 2015

Should i fixup and git push --force all the changes on the Amo:symfony branch ?

@linaori
Copy link
Contributor

linaori commented Dec 9, 2015

I notice the handle()/executeGuardAuthenticator() are becoming big_(ger with those changes)_. I think the flow and changes are good, but this will become harder and harder to maintain. Some suggestions I have for this method which could be done (after this PR is merged):

Using a null logger instead of a PSR logger
It will improve readability and reduce complexity and would remove 8 if statements in just those 2 methods

Move code out of the try catch which is unrelated
This try catch is pretty much just a sort of "failsafe" at the moment. However, things like supports() should not throw an AuthenticationException. A list of method calls:

  • $this->logger->debug('...');
  • $guardAuthenticator->supports($request)
  • $guardAuthenticator->getCredentials($request)
  • $this->logger->debug('...');
  • $this->authenticationManager->authenticate($token)
  • $this->logger->info('...');
  • $this->guardHandler->authenticateWithToken($token, $request);

Out of all available methods in the authenticator, only getUser() and checkCredentials() throw an exception and they are not being called here. In fact, the only method that throws an exception is $this->authenticationManager->authenticate($token). It's actually making it possible for people to throw all kinds of AuthenticationExceptions which can cause unwanted behavior or break when someone decides this try/catch can be narrowed down in say 3.4.

Make GuardAuthenticationListener final
We've created a very nice extension point which people will abuse in the future. My suggestion is to make this class final as it makes it easier for people to fix things here. Behavioral changes should still be avoided as much as possible though.

After this the code will still be quite massive but it will at least be easier to read and understand what it does.

@fabpot
Copy link
Member

fabpot commented Jan 25, 2016

@weaverryan What's the status of this one?

@fabpot
Copy link
Member

fabpot commented Feb 18, 2016

@weaverryan I'd like to move this one forward. Can you have a look at it? Thanks.

}

// otherwise something went wrong and the authentication must fail
throw new \InvalidArgumentException('Guard authenticator must provide user credentials');
Copy link
Member

Choose a reason for hiding this comment

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

A RuntimeException (or even better its child UnexpectedValueException) would be better. This is not an invalid argument here, but an invalid return value

Copy link
Member

Choose a reason for hiding this comment

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

And with the message:

Did you forget to return some credentials from Foo\Bar\ClassName::getCredentials()? Credentials can be anything: an array, string or object.

@Amo
Copy link
Contributor Author

Amo commented Feb 18, 2016

Updated.

*
* @return mixed|null
*/
public function getCredentials(Request $request);
Copy link
Member

Choose a reason for hiding this comment

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

@Amo What do you think about actually removing getCredentials() now? And instead changing getUser() to be getUser(Request $request, UserProviderInterface $userProvider);

Now that getCredentials() only has the responsibility of returning the credentials, it seems simpler just to ask "give me a user" instead of breaking things into these 2 pieces.

Btw, if you agree, we will need to rename getUser() to something like fetchUser() so we can add it to the new interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It could be simpler, but it would add much more complexity in the fetchUser method.
This "merge" of the two responsabilities will make the fetchUser method a bit more complex to unit test.
Aditionnaly, the UserProviderInterface is known by the GuardAuthenticationProvider and here we are in the GuardAuthenticationListener so it needs many changes.

Copy link
Member

Choose a reason for hiding this comment

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

It's just a question of "DX", which is subjective:

Before:

public function getCredentials(Request $request)
{
    return $request->headers->get('Authorization');
}

public function getUser($credentials, UserProviderInterface $userProvider)
{
    $authorizationHeader = $credentials;
    // ...
}

After

public function fetchUser(Request $request, UserProviderInterface $userProvider)
{
    $authorizationHeader = $request->headers->get('Authorization');
    // ...
}

@javiereguiluz @iltar Do you guys have a preference?

@Amo This new method would be called from GuardAuthenticationProvider - we would set the entire request in the PreAuthenticationGuardToken so that we could pass it to the new method.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not 100% sure about the "fetch" part, it deviates from the currently popular find and get (and makes me feel like talking to my dog). DX wise I'd go for the second solution, however the first option gives you the ability to make a chain credential provider in symfony. That might be overkill and people can still implement a similar pattern in their fetchUser. If getCredentials only purpose is to be passed to getUser, they should be merged imo.

Copy link
Member

Choose a reason for hiding this comment

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

Then how about we name it findUser()? I agree with you on the chaining - there may be a use for this, but I was also thinking: implement it yourself.

Copy link
Contributor

Choose a reason for hiding this comment

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

Personally I'd lean more towards getUser(), this because it's rather confusing with the Repositories otherwise. By default those start with find and it might confuse developers when those names start getting mixed up. Ideally I'd throw an exception in a get* method if unable to get whatever was expected.

How would that work out if the could would always throw an exception if the user could not be found?

Copy link
Member

Choose a reason for hiding this comment

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

@iltar The problem with getUser() is that we already have a method named that - and this would cause a signature change. So we need a new method name so we can keep the old one for BC. So, it's not idea. Perhaps getUserFromRequest()?

Copy link
Contributor

Choose a reason for hiding this comment

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

It's starting to sound more like a builder pattern;

$guard->buildUser(new UserFromRequestBuilder($request), $userProvider);

// potential to decouple from the request
$guard->buildUser(new UserFromConsoleBuilder($input, $output), $userProvider);

Anyway, getUserFromRequest() sounds decent enough, what will happen to getUser()?

Copy link
Member

Choose a reason for hiding this comment

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

We can re-use the same method name. With get_func_args(), it's possible to detect which version the user is using, like we've done in the past elsewhere in the framework. I'd go with the current getUser() method name.

@weaverryan
Copy link
Member

Thanks for the quick update @Amo - and sorry for the extra comments - I had to pause in the middle of reviewing :). Btw, when I talked about Guard at the last conference, someone else suggested this exact change afterwards.

@Amo
Copy link
Contributor Author

Amo commented Feb 23, 2016

I've been updating the code according to the discussion about getUserFromRequest.
Unit tests need to be updated.

@chalasr chalasr changed the base branch from master to 3.4 October 4, 2017 12:14
Copy link
Member

@chalasr chalasr left a comment

Choose a reason for hiding this comment

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

👍

UPGRADE-3.4.md Outdated
@@ -294,7 +294,10 @@ Security

* Deprecated the HTTP digest authentication: `NonceExpiredException`,
`DigestAuthenticationListener` and `DigestAuthenticationEntryPoint` will be
removed in 4.0. Use another authentication system like `http_basic` instead.
removed in 4.0. Use another authentication system like `http_basic` instead.
Copy link
Member

@nicolas-grekas nicolas-grekas Oct 4, 2017

Choose a reason for hiding this comment

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

extra space at end of line (same on the line below)

// abort the execution of the authenticator if it doesn't support the request.
if ($guardAuthenticator instanceof GuardAuthenticatorInterface) {
// deprecated since version 3.4, to be removed in 4.0
@trigger_error(sprintf('The %s interface is deprecated since version 3.4 and will be removed in 4.0. Implement %s instead.', GuardAuthenticatorInterface::class, AuthenticatorInterface::class), E_USER_DEPRECATED);
Copy link
Member

Choose a reason for hiding this comment

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

this runtime deprecation should be removed: it has already been trigger by DebugClassLoader

}

// otherwise something went wrong and the authentication must fail
throw new \UnexpectedValueException(sprintf(
Copy link
Member

Choose a reason for hiding this comment

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

should be on one line

*
* @return null|Response
*/
public function handleAuthenticationSuccess(TokenInterface $token, Request $request, GuardAuthenticatorInterface $guardAuthenticator, $providerKey)
public function handleAuthenticationSuccess(TokenInterface $token, Request $request, AuthenticatorInterface $guardAuthenticator, $providerKey)
Copy link
Member

Choose a reason for hiding this comment

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

that's a BC break unfortunately (same for all public interfaces)

*/
interface GuardAuthenticatorInterface extends AuthenticationEntryPointInterface
interface GuardAuthenticatorInterface extends AuthenticatorInterface
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be done the other way around, to still allow passing legacy type-hints.
this would mean removing the runtime notice above, but DebugClassLoader will take care or triggering it anyway

Copy link
Member

Choose a reason for hiding this comment

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

AuthenticatorInterface extends GuardAuthenticatorInterface

@@ -142,6 +142,6 @@ private function authenticateViaGuard(GuardAuthenticatorInterface $guardAuthenti

public function supports(TokenInterface $token)
{
return $token instanceof GuardTokenInterface;
return $token instanceof TokenInterface;
Copy link
Member

Choose a reason for hiding this comment

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

why this change?

@@ -56,7 +56,7 @@ public function __construct($guardAuthenticators, UserProviderInterface $userPro
/**
* Finds the correct authenticator for the token and calls it.
*
* @param GuardTokenInterface $token
* @param TokenInterface|GuardTokenInterface $token
Copy link
Member

Choose a reason for hiding this comment

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

I reverted this change as it should have been TokenInterface from day 1. I'm going to fix it on 2.7

@chalasr
Copy link
Member

chalasr commented Oct 4, 2017

@nicolas-grekas comments addressed, thanks

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

(but there are more FQCN strings that could be replaced by ...::class)

@chalasr
Copy link
Member

chalasr commented Oct 5, 2017

Remaining FQCNs fixed

@chalasr chalasr added the Ready label Oct 5, 2017
*/
private function triggerRememberMe(GuardAuthenticatorInterface $guardAuthenticator, Request $request, TokenInterface $token, Response $response = null)
private function triggerRememberMe($guardAuthenticator, Request $request, TokenInterface $token, Response $response = null)
Copy link
Member

Choose a reason for hiding this comment

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

IMO we should be consistent in removing the type hint (like here) or not (like in GuardAuthenticatorHandler below). Keeping the type hint might be a good idea to catch all places where we need to adjust them when removing code in 4.0.

Copy link
Member

Choose a reason for hiding this comment

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

👍 I re-added the typehint in this class so that it breaks if we forget to change them when removing the deprecated interface.

@fabpot
Copy link
Member

fabpot commented Oct 5, 2017

Thank you @Amo.

@fabpot fabpot merged commit 78eecba into symfony:3.4 Oct 5, 2017
fabpot added a commit that referenced this pull request Oct 5, 2017
…Amo, chalasr)

This PR was merged into the 3.4 branch.

Discussion
----------

[Security] Add Guard authenticator <supports> method

This method will be called before starting an authentication against a guard authenticator.
The authentication will be tried only if the supports method returned `true`
This improves understanding of code, increase consistency and removes responsability for `getCredentials` method to decide if the current request should be supported or not.

| Q | A |
| --- | --- |
| Bug fix? | no |
| New feature? | yes |
| BC breaks? | yes |
| Deprecations? | yes |
| Tests pass? | yes |
| Fixed tickets | none |
| License | MIT |
| Doc PR |  |

Commits
-------

78eecba Fix BC layer
a7a6f8a [Security] Add Guard authenticator <supports> method
@Amo
Copy link
Contributor Author

Amo commented Oct 5, 2017

Thank you ! Now i'm a proud contributor of Symfony

@chalasr
Copy link
Member

chalasr commented Oct 5, 2017

@Amo Thanks for this and congrats for your first PR on github! You did it right, in the right repo :)

fabpot added a commit that referenced this pull request Oct 5, 2017
This PR was merged into the 4.0-dev branch.

Discussion
----------

[Security] Remove GuardAuthenticatorInterface

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | yes
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | symfony/symfony-docs#8485

Removes BC layers for #16835.

Commits
-------

3408152 [Security][Guard] Remove GuardAuthenticatorInterface
This was referenced Oct 18, 2017
@kbond
Copy link
Member

kbond commented Oct 24, 2017

I am getting what I think is a false positive deprecation notice in 3.4:

User Deprecated: The "Symfony\Component\Security\Guard\AbstractGuardAuthenticator::supports()" 
Method is deprecated since version 3.4, to be removed in 4.0. You should not extend it from 
"...\MyAuthenticator".

I have added MyAuthenticator::supports() but am still getting this notice. Did I not upgrade correctly or is this a bug?

{
/**
* {@inheritdoc}
*
* @deprecated since version 3.4, to be removed in 4.0
Copy link
Member

Choose a reason for hiding this comment

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

If I remove this line, the false positive deprecation notice is removed. Should I open a PR?

Copy link
Member

Choose a reason for hiding this comment

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

Please do. This method must be implemented by concrete subclasses and it is unlikely to be called directly.

fabpot added a commit that referenced this pull request Oct 25, 2017
…ardAuthenticator::supports() (kbond)

This PR was merged into the 3.4 branch.

Discussion
----------

[Security] remove invalid deprecation notice on AbstractGuardAuthenticator::supports()

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #16835 (comment)
| License       | MIT
| Doc PR        | n/a

This deprecation flag causes a false positive.

Commits
-------

5fb44e7 [Guard] remove invalid deprecation notice
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.