Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Fix PHP Warning if no colon is present in decoded basic-auth payload #9

Merged
merged 19 commits into from Jul 25, 2018
Merged

Conversation

BreiteSeite
Copy link
Contributor

@BreiteSeite BreiteSeite commented Jul 24, 2018

This fixes #8

If the authorization header contains a base64 encoded string, that contains no colon when decoded, a PHP Warning is thrown in PHP 7.2. The new/fixed behaviour is that null is returned as authentication is not possible.

I also refactored the tests along with the authentication method. The test now include much more test-cases with increased readability by using named data-providers in PHPUnit.

I also added a test-case for a password that contains colons which also did not work before. I did not open a issue for this and fixed it silently in this PR.

  • Are you fixing a bug?
    • Detail how the bug is invoked currently.
    • Detail the original, incorrect behavior.
    • Detail the new, expected behavior.
    • Base your feature on the master branch, and submit against that branch.
    • Add a regression test that demonstrates the bug, and proves the fix.
    • Add a CHANGELOG.md entry for the fix.

* use dataProviders
* added more test-cases
* added return-type hints
* remove unnecessary assertion
* use strict parameter of base64_decode instead of maintaining list of valid characters in regexp
* use limit in explode() to allow passwords with colons
* check if base64_decode failed
* check that array index is set
return null;
}

if (! preg_match('/Basic (?P<credentials>[a-zA-Z0-9\+\/\=]+)/', $authHeader[0], $match)) {
if (! preg_match('/Basic (?P<credentials>.+)/', $authHeader[0], $match)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think its not good to maintain a list of valid base64 character in the regexp, because this list is also already maintained in PHP itself and checked with the strict argument in base64_decode()

Copy link
Member

Choose a reason for hiding this comment

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

Don't assume PHP is checking the header values. The code may be run under different SAPIs or alternative process managers that may or may not do such checks (e.g. Swoole, ReactPHP).

Copy link
Contributor Author

@BreiteSeite BreiteSeite Jul 24, 2018

Choose a reason for hiding this comment

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

Can you elaborate what you mean exactly here? Are you implying, that the strict argument from base64_decode() may be a no-op in some environments? Because i can't see that currently. https://github.com/php/php-src/blob/a5e80b22e11c2db7fd5ff07b5b7a28f80745e89b/ext/standard/base64.c#L115

@@ -53,15 +53,31 @@ public function __construct(
public function authenticate(ServerRequestInterface $request) : ?UserInterface
{
$authHeader = $request->getHeader('Authorization');
if (empty($authHeader)) {
if (! isset($authHeader[0])) {
Copy link
Member

Choose a reason for hiding this comment

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

Is isset correct or do mean array_key_exists?

Copy link
Contributor Author

@BreiteSeite BreiteSeite Jul 24, 2018

Choose a reason for hiding this comment

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

@froschdesign i guess both would work here. I can change it to array_key_exists if you like, but why would i want the execution to continue here, if it's (somehow) containing null?

Copy link
Member

Choose a reason for hiding this comment

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

From the PSR-7 spec for getHeader():

@return string[] An array of string values as provided for the given header. If the header does not appear in the message, this method MUST return an empty array.

As such, you will always have an array using this method. The question is if the array is empty; this is why we were using empty($authHeader) previously. I see no reason to change that.

Copy link
Contributor Author

@BreiteSeite BreiteSeite Jul 24, 2018

Choose a reason for hiding this comment

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

@weierophinney i agree with that. However, just because it's not-empty strictly does not implies that array index 0 is set. thats why i explicitly check for this, because index 0 is accessed later in the code. If someone manipulated the headers, it could be not empty while the "first" header-value is found in index 1 or 2 or whatever.

Do you still want me to revert it to the check with empty()?

Copy link
Member

Choose a reason for hiding this comment

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

According to RFC 7235, the Authorization header MUST only be a single header; it cannot be repeated. As such, $request->getHeader('Authorization') can only ever return 0 or 1 item in the array.

We can also use $request->getHeaderLine('Authorization'), which will return a string with the header contents, or an empty string if the header is not found.

As such, we have two options:

$authHeader = $request->getHeader('Authorization');
if (empty($authHeader)) {
    return null;
}
$value = array_shift($authHeader);

or

$value = $request->getHeaderLine('Authorization');
if ('' === $value) {
    return null;

And from there, use $value in the regex when retrieving the credentials from the value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@weierophinney Ah. You would avoid that by using array_shift(). I see.

I changed the code to the getHeaderLine() suggestion as it's more neat and readable in my opinion. Just pushed it. Thank you.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@weierophinney actually, after giving it second thoughts, i changed it back to use getHeader() to be easily able to early-exit in case someone sends multiple authorization headers.

After reading RFC 2617, i saw that user-id can be optional as well as the password. making the smallest valid Auth-string only the colon (:). I moved some assertions accordingly, as i previously declared some (spec-wise) valid authentication request as invalid.

I also added the test-assertion that invalid data actually never tries to hit the user-repository.

Sorry for the noise. Can you please take a 2nd look now?

If the commit-history is too polluted, i'm happy to squash them if you want.

Thank you very much.

I think this makes for a very robust basic-auth class. :)

@BreiteSeite BreiteSeite changed the title Issue/8 Fix PHP Warning if no colon is present in decoded basic-auth payload Jul 24, 2018
Copy link
Member

@weierophinney weierophinney left a comment

Choose a reason for hiding this comment

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

The approach for fixing the problem looks fine. I've marked a number of changes I would like rolled back (as they are either out-of-scope or based on incorrect assumptions); once those are done, I can review and merge.

CHANGELOG.md Outdated


- [#9](https://github.com/zendframework/zend-expressive-authentication-basic/pull/9)
It was not possible to use passwords that contained colons
Copy link
Member

Choose a reason for hiding this comment

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

Please use the same format as previous changelogs:

## 0.3.1 - TBD

### Added

- Nothing.

### Changed

- Nothing.

### Deprecated

- Nothing.

### Removed

- Nothing

### Fixed

- ...

Notes:

  • There is a line between a header and the first line of text.
  • Only one empty line between list items.
  • Assume a bugfix version unless you are proposing a new feature or a breaking change.

return null;
}

if (! preg_match('/Basic (?P<credentials>[a-zA-Z0-9\+\/\=]+)/', $authHeader[0], $match)) {
if (! preg_match('/Basic (?P<credentials>.+)/', $authHeader[0], $match)) {
Copy link
Member

Choose a reason for hiding this comment

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

Don't assume PHP is checking the header values. The code may be run under different SAPIs or alternative process managers that may or may not do such checks (e.g. Swoole, ReactPHP).

@@ -46,7 +46,7 @@ protected function setUp()
};
}

public function testConstructor()
public function testConstructor(): void
Copy link
Member

Choose a reason for hiding this comment

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

We don't generally add return type hints within test classes, unless the method is a data provider or a helper method. For test cases, it's not necessary. Please remove it here and elsewhere where you have added them.

$this->responseFactory
);
$this->assertNull($basicAccess->authenticate($this->request->reveal()));
}
Copy link
Member

Choose a reason for hiding this comment

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

Just a note for anybody else reviewing: this test becomes redundant, as it is covered by the empty set of the provideInvalidAuthenticationHeader provider used by the new testIsAuthenticatedWithInvalidData test case.

@@ -109,7 +107,7 @@ public function testIsAuthenticatedWithValidCredential()

$user = $basicAccess->authenticate($this->request->reveal());
$this->assertInstanceOf(UserInterface::class, $user);
$this->assertEquals('Aladdin', $user->getIdentity());
$this->assertEquals($username, $user->getIdentity());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this assertion is only testing the mock and therefor worthless. Should i remove it?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, please.

Copy link
Member

@weierophinney weierophinney 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!

I saw comments in Slack and in my emails around the regex + base64_decode rationale as well as the question about what the Authorization header can contain, but for some reason, the comments are not present in the UI.

I had missed that base64_decode() has the second $strict argument; knowing that, your approach makes sense to me, as we can rely on that behavior and return early for invalid data.

With regards to the authorization header requirements, I agree with the approach of using getHeader() + array_shift(). Additionally, since explode() will return false for absence of the delimiter in the string, and you're now checking for that, this patch resolves the originally reported issue.

I've indicated that we can remove the assertion against the mock. If you want to do that, I can wait for you to push the change; otherwise, I can take care of it during merge.

@@ -109,7 +107,7 @@ public function testIsAuthenticatedWithValidCredential()

$user = $basicAccess->authenticate($this->request->reveal());
$this->assertInstanceOf(UserInterface::class, $user);
$this->assertEquals('Aladdin', $user->getIdentity());
$this->assertEquals($username, $user->getIdentity());
Copy link
Member

Choose a reason for hiding this comment

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

Yes, please.

@BreiteSeite
Copy link
Contributor Author

for some reason, the comments are not present in the UI.

@weierophinney to see the discussion about the regexp, you need to expand the comment of @froschdesign's review. GitHubs UI is a bit confusion here.

I've indicated that we can remove the assertion against the mock. If you want to do that, I can wait for you to push the change; otherwise, I can take care of it during merge.

I've pushed that.

Sounds like you approve the changes.

It would be great if you can change your review status from request-changes to approved then or tell me what else is to change to get your approval. :)

This PR will also prevent a lot of other bugs and warnings i've found when i wrote the extensive test providers. So i guess this makes the component a lot more stable, which is good, because authentication is a critical part in web applications. :)

Thanks for all the suggestions and comments!

@weierophinney weierophinney merged commit b7a8388 into zendframework:master Jul 25, 2018
weierophinney added a commit that referenced this pull request Jul 25, 2018
- Rephrases them slightly to follow wording/phrasing conventions we
  generally use to detail changes due to pull requests.
- Fixes some markdown whitespace issues that were causing rendering
  issues.
- Sets date for 0.3.1 release.
weierophinney added a commit that referenced this pull request Jul 25, 2018
@weierophinney
Copy link
Member

Thanks, @BreiteSeite!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PHP Warning thrown when format of the decoded authentication doesn't contain a colon
3 participants