Skip to content

[Security] [RememberMe] Add support for parallel requests doing remember-me re-authentication #41175

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 0 commits into from
May 19, 2021

Conversation

Seldaek
Copy link
Member

@Seldaek Seldaek commented May 11, 2021

Q A
Branch? 5.x
Bug fix? yes
New feature? yes ish
Deprecations? no
Tickets Fix #40971, Fix #28314, Fix #18384
License MIT
Doc PR symfony/symfony-docs#...

This is a possible implementation to gather feedback mostly..

TokenVerifierInterface naming is kinda bad perhaps.. But my goal would be to merge it in TokenProviderInterface for 6.0 so it's not so important. Not sure if/how to best indicate this in terms of deprecation notices.

Anyway wondering if this would be an acceptable implementation (ideally in an application I would probably override the new methods from DoctrineTokenProvider to something like this which is less of a hack and does expiration properly:

    public function verifyToken(PersistentTokenInterface $token, string $tokenValue)
    {
        if (hash_equals($token->getTokenValue(), $tokenValue)) {
            return true;
        }

        if (!$this->cache->hasItem('rememberme-' . $token->getSeries())) {
            return false;
        }

        /** @var CacheItem $item */
        $item = $this->cache->getItem('rememberme-' . $token->getSeries());
        $oldToken = $item->get();
            
        return hash_equals($oldToken, $tokenValue);
    }

    public function updateExistingToken(PersistentTokenInterface $token, string $tokenValue, \DateTimeInterface $lastUsed): void
    {
        $this->updateToken($token->getSeries(), $tokenValue, $lastUsed);

        /** @var CacheItem $item */
        $item = $this->cache->getItem('rememberme-'.$token->getSeries());
        $item->set($token->getTokenValue());
        $item->expiresAfter(60);
        $this->cache->save($item);
    }

If you think it'd be fine to require optionally the cache inside DoctrineTokenProvider to enable this feature instead of the hackish way I did it, that'd be ok for me too.

The current DoctrineTokenProvider implementation of TokenVerifierInterface relies on the lucky fact that series are generated using base64_encode(random_bytes(64)) which always ends in the == padding of base64, so that allowed me to store an alternative token value temporarily by replacing == with _.

Alternative implementation options:

  1. Inject cache in DoctrineTokenProvider and do a proper implementation (as shown above) that way
  2. Do not implement at all in DoctrineTokenProvider and let users who care implement this themselves.
  3. Implement as a new token_verifier option that could be configured on the firewall->remember_me key so you can pass an implementation if needed, and possibly ship a default one using cache that could be autoconfigured
  4. Add events that allow modifying the token to be verified, and allow receiving the newly updated token incl series, instead of TokenVerifierInterface, but then we need to inject a dispatcher in RememberMeAuthenticator.

@chalasr @wouterj sorry for the long description but in the hope of getting this included in 5.3.0, if you can provide guidance I will happily work on this further tomorrow to try and wrap it up ASAP.

@Seldaek Seldaek requested review from chalasr and wouterj as code owners May 11, 2021 14:30
@carsonbot carsonbot changed the title [Security][RememberMe] Add support for parallel requests doing remember-me re-a… [Security] [RememberMe] Add support for parallel requests doing remember-me re-a… May 11, 2021
@Seldaek Seldaek changed the title [Security] [RememberMe] Add support for parallel requests doing remember-me re-a… [Security] [RememberMe] Add support for parallel requests doing remember-me re-authentication May 11, 2021
Copy link
Contributor

@heiglandreas heiglandreas left a comment

Choose a reason for hiding this comment

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

Looks good to me. Only the comment regarding the hardcoded 60 seconds is something that could be considered. But that is not a reason to not merge this from my point of view.

@derrabus derrabus added this to the 5.x milestone May 11, 2021
Copy link
Member

@derrabus derrabus left a comment

Choose a reason for hiding this comment

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

Please make use of return types for brand-new methods. Also, can you please update the component's changelog?

@Seldaek
Copy link
Member Author

Seldaek commented May 12, 2021

I updated the implementation to make token_verifier configurable in the security config (definitely would appreciate a review of that bit from @wouterj). By default if the token_provider implements TokenVerifierInterface it will also be used as token verifier, so that makes the default implementation in DoctrineTokenProvider work by default.

I could remove the DoctrineTokenProvider implementation though if you rather avoid having a default fix which is slightly hackish (I fully documented the implementation now so it's hopefully clearer what is going on).

I also added a CacheTokenVerifier implementation which is the ideal one I would configure in prod, but I guess this can not be enabled by default as it requires a configured cache? I'm not sure what the baseline is here.

Edit: I'm also not sure why Psalm fails, it complains about some code I did not change, not sure if I should apply those fixes or not.

@chalasr chalasr modified the milestones: 5.x, 5.3 May 12, 2021
@Seldaek Seldaek force-pushed the rememberme-hooks branch from 62ee18b to af92066 Compare May 12, 2021 08:51
@wouterj
Copy link
Member

wouterj commented May 12, 2021

fyi: I don't have time for a detailed review today, it's on my list for tomorrow

@Seldaek Seldaek force-pushed the rememberme-hooks branch from 29b8f48 to 8c1d0cc Compare May 14, 2021 21:05
@Seldaek
Copy link
Member Author

Seldaek commented May 14, 2021

Thanks for the hints @chalasr - I hope my implementation in 8c1d0cc is correct - I'm not entirely sure if the nested null on invalid ref will work here https://github.com/symfony/symfony/pull/41175/files#diff-c790b1d460cd1f019b3eefa9c0322710d078dbadd510dbb1ee631c4c668e0dbcR323-R328 I'm hoping it would set the cache ref to null if the cache service gets deleted by the pass, which would then mark the whole verifier service invalid as the cache arg is not nullable, turning the whole verifier into a null itself.

@wouterj I think we can argue either way.. but if the current implementation works I'd say why not ship a fix by default and save people from having to find out about this issue before they can configure the verifier service.

@chalasr
Copy link
Member

chalasr commented May 14, 2021

I'm not entirely sure if the nested null on invalid ref will work here https://github.com/symfony/symfony/pull/41175/files#diff-c790b1d460cd1f019b3eefa9c0322710d078dbadd510dbb1ee631c4c668e0dbcR323-R328 I'm hoping it would set the cache ref to null if the cache service gets deleted by the pass, which would then mark the whole verifier service

@Seldaek NULL_ON_INVALID_REFERENCE means that if the referenced service does not exist, null will be injected as argument to the referencing service.
If we want to remove the CacheTokenVerifier service (I think so), we need to call removeDefinition() from the compiler pass you added.

@chalasr
Copy link
Member

chalasr commented May 14, 2021

You will probably need a DI tag to collect the cache-based token verifier definitions and remove them from the compiler pass.

@Seldaek
Copy link
Member Author

Seldaek commented May 14, 2021

Yes but injecting null for $cache means the CacheTokenVerifier definition becomes invalid, so I believe it will automatically be removed/nulled, and the RememberMeAuthenticator receives null as verifier.

I tested with this and it seems to work as expected, I get an App\Test2 instance with null arg:

services:
    App\Test: # this would be the verifier service
        arguments:
            - '@?cache.foo'

    App\Test2: # and this the authenticator
        arguments:
            - '@?App\Test'

@chalasr
Copy link
Member

chalasr commented May 14, 2021

In your example App\Test2 should either be given a App\Test instance or an exception should be thrown if the arg is not nullable (which is the case here), but App\Test won't be removed:

App\Test2 {#1367 ▼
  -test: App\Test {#6687 ▼
    -cacheItemPool: null
  }

Here is a reproducer https://github.com/chalasr/repro-null-on-invalid (clone, run symfony serve and browse localhost:8000). Do I miss something?

@Seldaek
Copy link
Member Author

Seldaek commented May 15, 2021

Nope, App\Test2 accepts a nullable App\Test in my example, because that's what the PersistentRememberMeHandler accepts https://github.com/symfony/symfony/pull/41175/files#diff-5128596229172c9c8dc0dda0e9846231169b9d239c65366cb13ce6ffc2a913daR39 (a nullable TokenVerifierInterface). Anyway if you want I can tweak the compiler pass to explicitly nullify the service, but to me it does not seem necessary.

Edit: You got the example backwards I now realize, App\Test should get a non-nullable cache pool, because that's also what CacheTokenVerifier expects.

@chalasr
Copy link
Member

chalasr commented May 17, 2021

Ok got it, thanks!

@nicolas-grekas
Copy link
Member

That's for 5.4, or 5.3?

@Seldaek
Copy link
Member Author

Seldaek commented May 18, 2021

I was hoping for 5.3 but that's just my opinion ;)

@zerkms
Copy link
Contributor

zerkms commented May 18, 2021

So it won't be backported to 4.4?

@wouterj
Copy link
Member

wouterj commented May 18, 2021

@zerkms no, Symfony only fixes bugs in already released versions.

I'm a bit divided on 5.3/5.4. On one hand, it's a nice and mostly hidden "bug fix" that fits nicely with the other remember me changes in 5.3. On the other hand, the change is fully BC and thus has no reason (other than another 6 months waiting) to bypass the feature-freeze deadline (unless I'm missing something).

@zerkms
Copy link
Contributor

zerkms commented May 18, 2021

@wouterj it says "3 year support for bugs and security fixes." at https://symfony.com/releases for 4.4 🤷

@chalasr
Copy link
Member

chalasr commented May 18, 2021

@zerkms It won't be backported because the patch is not compatible with the 4.4 implementation of the remember-me feature.

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.

This is fine-tuning the 5.3 rewrite of the remember-me feature, while removing a long standing limitation of that feature, so I think it's fine for 5.3.

@zerkms
Copy link
Contributor

zerkms commented May 18, 2021

@chalasr then

3 year support for bugs and security fixes.

should be reworded, to be like "3 year support for some bugs and security fixes"?

If a bug is reported against v4.4 (which also is affected) would it be fixed independently?

@chalasr
Copy link
Member

chalasr commented May 18, 2021

If someone is able to submit a valid fix for 4.4, we could probably accept it.
But this seems to be design issue that is hard to fix on the 4.4 implementation.

@fabpot
Copy link
Member

fabpot commented May 19, 2021

Tests seem to be broken.

@Seldaek Seldaek force-pushed the rememberme-hooks branch from 04b27a5 to 2e602ca Compare May 19, 2021 07:32
@fabpot
Copy link
Member

fabpot commented May 19, 2021

Thank you @Seldaek.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
9 participants