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

fix: Make OAuth2PKCEClient work with latest league/oauth2-client release #407

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
"symfony/routing": "^4.4|^5.0|^6.0",
"symfony/http-foundation": "^4.4|^5.0|^6.0",
"league/oauth2-client": "^2.0"
"league/oauth2-client": "^2.7"
},
"autoload": {
"psr-4": { "KnpU\\OAuth2ClientBundle\\": "src/" }
Expand Down
19 changes: 11 additions & 8 deletions src/Client/OAuth2PKCEClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,14 @@ public function __construct(AbstractProvider $provider, RequestStack $requestSta
*/
public function redirect(array $scopes = [], array $options = [])
{
$this->getSession()->set(static::VERIFIER_KEY, $code_verifier = bin2hex(random_bytes(64)));
$pkce = [
'code_challenge' => rtrim(strtr(base64_encode(hash('sha256', $code_verifier, true)), '+/', '-_'), '='),
'code_challenge_method' => 'S256',
];
$response = parent::redirect($scopes, $options);

return parent::redirect($scopes, $options + $pkce);
$codeVerifier = $this->getOAuth2Provider()->getPkceCode();
Copy link
Member

Choose a reason for hiding this comment

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

I suppose this is the new method upstream that was not available on the moment when this client was written.

UPD: Actually, I see it was released on the latest 2.7 of league/oauth2-client, right? So, we literally pushed this dependency to the latest here. But it might be ok, at least tests are happy about it

if (null !== $codeVerifier) {
$this->getSession()->set(static::VERIFIER_KEY, $codeVerifier);
}

return $response;
}

/**
Expand All @@ -69,10 +70,12 @@ public function getAccessToken(array $options = [])
if (!$this->getSession()->has(static::VERIFIER_KEY)) {
throw new \LogicException('Unable to fetch token from OAuth2 server because there is no PKCE code verifier stored in the session');
}
$pkce = ['code_verifier' => $this->getSession()->get(static::VERIFIER_KEY)];

$codeVerifier = $this->getSession()->get(static::VERIFIER_KEY);
$this->getOAuth2Provider()->setPkceCode($codeVerifier);
$this->getSession()->remove(static::VERIFIER_KEY);

return parent::getAccessToken($options + $pkce);
return parent::getAccessToken($options);
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 we want to add PRCE to the options?

}

/**
Expand Down
48 changes: 39 additions & 9 deletions tests/Client/OAuth2PKCEClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@

namespace KnpU\OAuth2ClientBundle\tests\Client;

use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Response as Response;
use KnpU\OAuth2ClientBundle\Client\OAuth2PKCEClient;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use League\OAuth2\Client\Token\AccessToken;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -28,6 +31,7 @@ class OAuth2PKCEClientTest extends TestCase
private Request $request;
private Session $session;
private AbstractProvider $provider;
private ClientInterface $httpClient;

public function setup(): void
{
Expand All @@ -36,7 +40,13 @@ public function setup(): void
$this->request->setSession($this->session);
$this->requestStack = new RequestStack();
$this->requestStack->push($this->request);
$this->provider = new class extends AbstractProvider {
$this->httpClient = $this->createMock(ClientInterface::class);
$this->provider = new class(
[],
[
'httpClient' => $this->httpClient,
]
) extends AbstractProvider {

protected function checkResponse(ResponseInterface $response, $data): void
{
Expand All @@ -56,23 +66,22 @@ protected function getDefaultScopes(): array

public function getBaseAccessTokenUrl(array $params): string
{
// not needed
return 'http://coolOAuthServer.com/token';
}

public function getBaseAuthorizationUrl(): string
{
return 'http://coolOAuthServer.com/authorize';
}

public function getResourceOwnerDetailsUrl(AccessToken $token): string
public function getPkceMethod(): ?string
{
// not needed
return 'S256';
}

public function getAccessToken($grant, array $options = [])
public function getResourceOwnerDetailsUrl(AccessToken $token): string
{
// hijack this method to just create a new AccessToken using the $options
return new AccessToken($options + ['access_token' => 'DUMMY_ACCESS_TOKEN']);
// not needed
}
};
}
Expand Down Expand Up @@ -109,11 +118,32 @@ public function testGetAccessToken()

// ensure code verifier is generated by redirect() and stored in session first
$client->redirect();
$codeVerifier = $client->getOAuth2Provider()->getPkceCode();

// OAuth2Client::getAccessToken() requires 'code' query parameter
$this->request->query->set('code', 'DUMMY_CODE');

// assert the code_verifier parameter is passed and returned back by the hijacked provider
// assert the same code_verifier was passed to the authorization code exchange endpoint
$this->httpClient
->expects($this->once())
->method('send')
->with(
$this->callback(
function (RequestInterface $request) use ($codeVerifier) {
$url = (string) $request->getBody();
$queries = [];
parse_str($url, $queries);

$this->assertArrayHasKey('code_verifier', $queries);

return $codeVerifier === $queries['code_verifier'];
}
)
)
->willReturn(
new Response(200, [], '{"access_token": "DUMMY_ACCESS_TOKEN"}')
);

$accessToken = $client->getAccessToken();
$this->assertArrayHasKey('code_verifier', $accessToken->getValues());
}
}