Repository navigation
feat: allow signing verification URLs without the host - #142
bocharsky-bw merged 18 commits into
Conversation
|
This configuration option would be great. I have a use case where I want to target to a completely different host running on another framework; the hosts endpoint then proxies the request (or rather re-creates it) to the Symfony Controller. |
|
@MichaelBrauner could you rebase for resolve the conflicts? Thanks |
b434387 to
93d5392
Compare
93d5392 to
90ec899
Compare
|
Hi, this PR is now ready for review. However, I wasn't able to run the test suite locally – vendor/bin/simple-phpunit fails with:
I tried with PHP 8.1 and 8.2, removed and reinstalled the vendor folder and the PHPUnit Bridge multiple times, but no success. Thanks! |
|
Can you rerun the checks? |
|
Sorry for the delay. I reran the CI, but we still have errors here |
|
@bocharsky-bw would you be so kind to re-run CI so that we can see the errors? The old logs have been archived. |
|
@melbings I reran the CI for you, I hope this helps! Though I see this PR now should be rebased due to some changes in master, and there are some conflicts that should be resolved too. UPD: Yeah, probably the conflicts should be resolved first. If you want to follow up on this PR and push it forward - please, feel free to create a new PR based on these changes |
|
@bocharsky-bw I'm afraid we're abandoning this pull request on our side. We will use Symfony's UriSigner component to deal with signed URLs instead. Nevertheless, thank you for your work and your support. |
|
@bocharsky-bw @jrushlow Any chance this can still be merged? |
I'm afraid that someone would need to resolve merge issues that have come up since we originally worked on this. Unfortunately, my company cannot work on this anymore. I assume that Claude & co. should have quite an easy time finishing this up, though. |
146a026 to
9c06fb4
Compare
|
Rebased onto Could you kick off the checks? 40 tests green locally, PHPStan and CS at parity with |
Renames the option to sign_without_host. When it is enabled, validateEmailConfirmation() and validateEmailConfirmationFromRequest() now check the path instead of the full URL.
9c06fb4 to
0e29805
Compare
|
The Could you re-run when you get a chance? |
bocharsky-bw
left a comment
There was a problem hiding this comment.
Thank you for returning to work on it! I see many people are interested in it, so I would like to see this feature merged. I left some comments, could you take a look at them too?
Some extra tests like next ones would be very welcome:
- In
testValidateEmailSignatureFromRequestWithoutHostwe doRequest::create($signedUrl)that uses localhost. It worth checking on a different host as well, e.g.Request::create('https://other.example'.$signedUrl). - A regression test for
sign_without_host: falseto make sure the link with different host is declined. That's very important security concern. - A test to make sure a changed path or query is declined for
sign_without_host: false VerifyEmailHelperFunctionalTest::getHelper()now has a new arg but no tests are tested it withtrue
|
@bocharsky-bw Great news, thanks for the review! I’ll look into your comments and the suggested tests in the next few days. |
|
Awesome, thanks for working on it! I would like to see it finally merged :) |
The property can hold the legacy HttpKernel\UriSigner, which PHPStan cannot resolve on current Symfony. Both classes share the same API, so annotating it as the new one removes four suppressed errors without changing behaviour.
The returned URL still carries scheme and host from the router context, so it stays a working link that can go straight into an email. Routes that cannot be generated as a path - those declaring schemes or their own host - now throw a LogicException instead of producing a signature that silently covers more than the path.
With the option off, a changed host, path or query must be rejected. Switching the option in either direction invalidates links that have already been sent.
| { | ||
| // creating a new RequestStack is fine here, because it keeps UrlHelper on the router context, | ||
| // the same source generate() uses | ||
| return (new UrlHelper(new RequestStack(), $this->router))->getAbsoluteUrl($pathAndQuery); |
There was a problem hiding this comment.
Could you also explain in the comment above why we didn't inject this service instead? I think it's easy to miss, would be important to clarify
| private $signWithoutHost; | ||
|
|
||
| public function __construct(UrlGeneratorInterface $router, /* no typehint for BC with legacy PHP */ $uriSigner, VerifyEmailQueryUtility $queryUtility, VerifyEmailTokenGenerator $generator, int $lifetime) | ||
| public function __construct(UrlGeneratorInterface $router, /* no typehint for BC with legacy PHP */ $uriSigner, VerifyEmailQueryUtility $queryUtility, VerifyEmailTokenGenerator $generator, int $lifetime, bool $signWithoutHost = false) |
There was a problem hiding this comment.
Do you think we should duplicate setting false here explicitly? The option should be defaulted to false anyway, so we can expect it here, no need to make it optional, right?
There was a problem hiding this comment.
Yes, in the DI container the service gets its parameters with the defaults from the configuration. But the class isn't marked as internal, so it may be instantiated directly in projects — dropping the default would be a BC break in a minor version.
I've added a @legacy note on the constructor so it doesn't get lost, and we can make it required in 2.0. What do you think?
|
|
||
| if (\is_string($host)) { | ||
| throw new \LogicException(\sprintf('The route "%s" generates a URL with the host "%s" instead of a path, so it cannot be signed while the "sign_without_host" option is enabled. Remove the host or scheme from the route, or set "framework.router.default_uri" to match it.', $routeName, $host)); | ||
| } |
There was a problem hiding this comment.
Fairly speaking, I'm not sure we should throw in this case. We're signing without host on purpose (expclicitly), so I think it's safe to take the path if the uri was generated with host and sign only the path. I.e just force ignoring host and schema if it exists. Wdyt?
But your solution is defensive and I'm fine with it too, just trying to find the best DX path here. So it's up to you
There was a problem hiding this comment.
Let's take the case where a developer declares a route with its own host. If we strip it and then prepend the host from the router context, the link in the email points at the wrong domain and 404s. You would only notice that in production. That's why I think staying defensive is the right call here. What do you think?
bocharsky-bw
left a comment
There was a problem hiding this comment.
This looks solid to me! The only question left is in test stability.
Before this PR every test kernel had the same config, so that was harmless. Now kernels differ by customConfig and routes, and some tests boot 2–3 of them. I think it's possible that a test could end up running with anothertest's sign_without_host value and fail randomly.
The chance is small, but flaky CI is no fun to debug. A per-instance unique id would make it impossible:
private $uniqueId;
public function __construct()
{
$this->uniqueId = uniqid('', true);
parent::__construct();
}
public function getCacheDir()
{
return sys_get_temp_dir().'/cache/'.$this->uniqueId;
}
public function getLogDir()
{
return sys_get_temp_dir().'/logs/'.$this->uniqueId;
}Each kernel gets a fresh compile, which is exactly what happens today anyway, just guaranteed now. Wdyt?
You're right — there are nicer things in life than debugging flaky CI. I'll do exactly that. |
|
Thank you, Victor! It was a pleasure working with you on this. |
|
Good job, guys 👍 @MichaelBrauner @bocharsky-bw |
Closes #136.
Verification links are signed as absolute URLs, so a link is bound to the host it was generated for. This PR adds a
sign_without_hostoption: with it enabled the path is signed instead, and the link can be redeemed under any host the application answers on.