diff --git a/CHANGELOG.md b/CHANGELOG.md index a11eec4..be61f88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Yii Framework 2 authclient extension Change Log ------------------------ - Enh #387: Use appropriate exception if client does not exist (eluhr) +- Enh #388: Added support to configure the OAuth2 access token location in requests and added a generic OAuth2 client (rhertogh) 2.2.15 December 16, 2023 diff --git a/src/OAuth2.php b/src/OAuth2.php index 137d008..f2c0240 100644 --- a/src/OAuth2.php +++ b/src/OAuth2.php @@ -8,6 +8,7 @@ namespace yii\authclient; use Yii; +use yii\base\InvalidConfigException; use yii\helpers\Json; use yii\helpers\Url; use yii\web\HttpException; @@ -37,6 +38,18 @@ */ abstract class OAuth2 extends BaseOAuth { + /** + * Apply the access token to the request header + * @since 2.2.16 + */ + const ACCESS_TOKEN_LOCATION_HEADER = 'header'; + + /** + * Apply the access token to the request body + * @since 2.2.16 + */ + const ACCESS_TOKEN_LOCATION_BODY = 'body'; + /** * @var string protocol version. */ @@ -71,6 +84,15 @@ abstract class OAuth2 extends BaseOAuth */ public $enablePkce = false; + /** + * @var string The location of the access token when it is applied to the request. + * NOTE: According to the OAuth2 specification this should be `header` by default, + * however, for backwards compatibility the default value used here is `body`. + * @since 2.2.16 + * + * @see https://datatracker.ietf.org/doc/html/rfc6749#section-7 + */ + public $accessTokenLocation = self::ACCESS_TOKEN_LOCATION_BODY; /** * Composes user authorization URL. @@ -167,12 +189,22 @@ public function fetchAccessToken($authCode, array $params = []) /** * {@inheritdoc} + * @throws InvalidConfigException */ public function applyAccessTokenToRequest($request, $accessToken) { - $data = $request->getData(); - $data['access_token'] = $accessToken->getToken(); - $request->setData($data); + switch($this->accessTokenLocation) { + case self::ACCESS_TOKEN_LOCATION_BODY: + $data = $request->getData(); + $data['access_token'] = $accessToken->getToken(); + $request->setData($data); + break; + case self::ACCESS_TOKEN_LOCATION_HEADER: + $request->getHeaders()->set('Authorization', 'Bearer ' . $accessToken->getToken()); + break; + default: + throw new InvalidConfigException('Unknown access token location: ' . $this->accessTokenLocation); + } } /** diff --git a/src/OpenIdConnect.php b/src/OpenIdConnect.php index 126e986..943e9b7 100644 --- a/src/OpenIdConnect.php +++ b/src/OpenIdConnect.php @@ -7,9 +7,9 @@ namespace yii\authclient; -use Jose\Component\Core\AlgorithmManager; use Jose\Component\Checker\AlgorithmChecker; use Jose\Component\Checker\HeaderCheckerManager; +use Jose\Component\Core\AlgorithmManager; use Jose\Component\KeyManagement\JWKFactory; use Jose\Component\Signature\JWSLoader; use Jose\Component\Signature\JWSTokenSupport; @@ -76,6 +76,11 @@ */ class OpenIdConnect extends OAuth2 { + /** + * {@inheritdoc} + */ + public $accessTokenLocation = OAuth2::ACCESS_TOKEN_LOCATION_HEADER; + /** * @var array Predefined OpenID Connect Claims * @see https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.2 @@ -352,15 +357,6 @@ protected function initUserAttributes() return $idTokenData; } - /** - * {@inheritdoc} - */ - public function applyAccessTokenToRequest($request, $accessToken) - { - // OpenID Connect requires bearer token auth for the user info endpoint - $request->getHeaders()->set('Authorization', 'Bearer ' . $accessToken->getToken()); - } - /** * {@inheritdoc} */ diff --git a/src/clients/GitHub.php b/src/clients/GitHub.php index 23965cc..69b3609 100644 --- a/src/clients/GitHub.php +++ b/src/clients/GitHub.php @@ -40,6 +40,11 @@ */ class GitHub extends OAuth2 { + /** + * {@inheritdoc} + */ + public $accessTokenLocation = OAuth2::ACCESS_TOKEN_LOCATION_HEADER; + /** * {@inheritdoc} */ diff --git a/src/clients/Oauth2Client.php b/src/clients/Oauth2Client.php new file mode 100644 index 0000000..8172fc5 --- /dev/null +++ b/src/clients/Oauth2Client.php @@ -0,0 +1,55 @@ + [ + * 'authClientCollection' => [ + * 'class' => 'yii\authclient\Collection', + * 'clients' => [ + * 'oauth2' => [ + * 'class' => 'yii\authclient\clients\Oauth2Client', + * 'authUrl' => 'https://oauth2service.com/oauth2/authorize', + * 'tokenUrl' => 'https://oauth2service.com/oauth2/authorize', + * 'apiBaseUrl' => 'https://oauth2service.com/api', + * 'clientId' => 'your_app_client_id', + * 'clientSecret' => 'your_app_client_secret', + * 'name' => 'custom name', + * 'title' => 'custom title' + * ], + * ], + * ] + * // ... + * ] + * ``` + * + * @since 2.2.16 + */ +class Oauth2Client extends OAuth2 +{ + + /** + * {@inheritdoc} + */ + public $accessTokenLocation = self::ACCESS_TOKEN_LOCATION_HEADER; + + /** + * {@inheritdoc} + */ + protected function initUserAttributes() + { + return []; // Plain Oauth 2.0 doesn't specify user attributes. + } +} diff --git a/src/clients/TwitterOAuth2.php b/src/clients/TwitterOAuth2.php index a1d73fc..73de26e 100644 --- a/src/clients/TwitterOAuth2.php +++ b/src/clients/TwitterOAuth2.php @@ -27,6 +27,11 @@ */ class TwitterOAuth2 extends OAuth2 { + /** + * {@inheritdoc} + */ + public $accessTokenLocation = OAuth2::ACCESS_TOKEN_LOCATION_HEADER; + /** * {@inheritdoc} */ @@ -64,12 +69,4 @@ protected function defaultTitle() { return 'Twitter'; } - - /** - * {@inheritdoc} - */ - public function applyAccessTokenToRequest($request, $accessToken) - { - $request->getHeaders()->set('Authorization', 'Bearer '. $accessToken->getToken()); - } -} \ No newline at end of file +} diff --git a/tests/clients/FacebookTest.php b/tests/clients/FacebookTest.php new file mode 100644 index 0000000..1fafb75 --- /dev/null +++ b/tests/clients/FacebookTest.php @@ -0,0 +1,20 @@ + [ - 'request' => [ - 'hostInfo' => 'http://testdomain.com', - 'scriptUrl' => '/index.php', - ], - ] - ]; - $this->mockApplication($config, '\yii\web\Application'); + return new Google(); + } + + protected function getExpectedTokenLocation() + { + return OAuth2::ACCESS_TOKEN_LOCATION_BODY; } public function testAuthenticateUserJwt() @@ -45,11 +43,6 @@ public function testAuthenticateUserJwt() $this->assertNotEmpty($token->getToken()); } - protected function createClient() - { - return new Google(); - } - /** * Data provider for [[testDefaultReturnUrl]]. * @return array test data. diff --git a/tests/clients/LinkedInTest.php b/tests/clients/LinkedInTest.php new file mode 100644 index 0000000..7505601 --- /dev/null +++ b/tests/clients/LinkedInTest.php @@ -0,0 +1,25 @@ + [ + 'request' => [ + 'hostInfo' => 'http://testdomain.com', + 'scriptUrl' => '/index.php', + ], + ], + ]; + $this->mockApplication($config, '\yii\web\Application'); + } + + public function testTokenLocation() + { + $tokenLocation = $this->getExpectedTokenLocation(); + $client = $this->createClient(); + $testToken = 'test-token'; + $client->setAccessToken(new OAuthToken(['token' => $testToken])); + $request = $client->createApiRequest(); + $request->beforeSend(); // injects the access token + + if ($tokenLocation == OAuth2::ACCESS_TOKEN_LOCATION_HEADER) { + $authorizationHeader = $request->getHeaders()->get('authorization'); + $this->assertEquals($this->getAccessTokenHeaderTypeName() . ' ' . $testToken, $authorizationHeader); + } elseif ($tokenLocation == OAuth2::ACCESS_TOKEN_LOCATION_BODY) { + $accessTokenBodyParamName = $this->getAccessTokenBodyParamName(); + $data = $request->getData(); + $this->assertArrayHasKey($accessTokenBodyParamName, $data); + $this->assertEquals($testToken, $data[$accessTokenBodyParamName]); + } else { + throw new InvalidConfigException('Unknown token location "' . $tokenLocation . '".'); + } + } +}