diff --git a/composer.lock b/composer.lock index 8bbe1b338..5204cc9ed 100644 --- a/composer.lock +++ b/composer.lock @@ -4868,16 +4868,16 @@ }, { "name": "thenetworg/oauth2-azure", - "version": "v2.1.1", + "version": "v2.2.2", "source": { "type": "git", "url": "https://github.com/TheNetworg/oauth2-azure.git", - "reference": "06fb2d620fb6e6c934f632c7ec7c5ea2e978a844" + "reference": "be204a5135f016470a9c33e82ab48785bbc11af2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TheNetworg/oauth2-azure/zipball/06fb2d620fb6e6c934f632c7ec7c5ea2e978a844", - "reference": "06fb2d620fb6e6c934f632c7ec7c5ea2e978a844", + "url": "https://api.github.com/repos/TheNetworg/oauth2-azure/zipball/be204a5135f016470a9c33e82ab48785bbc11af2", + "reference": "be204a5135f016470a9c33e82ab48785bbc11af2", "shasum": "" }, "require": { @@ -4887,6 +4887,9 @@ "league/oauth2-client": "~2.0", "php": "^7.1|^8.0" }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, "type": "library", "autoload": { "psr-4": { @@ -4919,9 +4922,9 @@ ], "support": { "issues": "https://github.com/TheNetworg/oauth2-azure/issues", - "source": "https://github.com/TheNetworg/oauth2-azure/tree/v2.1.1" + "source": "https://github.com/TheNetworg/oauth2-azure/tree/v2.2.2" }, - "time": "2022-06-23T10:35:36+00:00" + "time": "2023-12-19T12:10:48+00:00" }, { "name": "twig/twig", diff --git a/lib/composer/installed.json b/lib/composer/installed.json index 3fb72eb1a..a8b851ca7 100644 --- a/lib/composer/installed.json +++ b/lib/composer/installed.json @@ -5284,17 +5284,17 @@ }, { "name": "thenetworg/oauth2-azure", - "version": "v2.1.1", - "version_normalized": "2.1.1.0", + "version": "v2.2.2", + "version_normalized": "2.2.2.0", "source": { "type": "git", "url": "https://github.com/TheNetworg/oauth2-azure.git", - "reference": "06fb2d620fb6e6c934f632c7ec7c5ea2e978a844" + "reference": "be204a5135f016470a9c33e82ab48785bbc11af2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/TheNetworg/oauth2-azure/zipball/06fb2d620fb6e6c934f632c7ec7c5ea2e978a844", - "reference": "06fb2d620fb6e6c934f632c7ec7c5ea2e978a844", + "url": "https://api.github.com/repos/TheNetworg/oauth2-azure/zipball/be204a5135f016470a9c33e82ab48785bbc11af2", + "reference": "be204a5135f016470a9c33e82ab48785bbc11af2", "shasum": "" }, "require": { @@ -5304,7 +5304,10 @@ "league/oauth2-client": "~2.0", "php": "^7.1|^8.0" }, - "time": "2022-06-23T10:35:36+00:00", + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "time": "2023-12-19T12:10:48+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -5338,7 +5341,7 @@ ], "support": { "issues": "https://github.com/TheNetworg/oauth2-azure/issues", - "source": "https://github.com/TheNetworg/oauth2-azure/tree/v2.1.1" + "source": "https://github.com/TheNetworg/oauth2-azure/tree/v2.2.2" }, "install-path": "../thenetworg/oauth2-azure" }, diff --git a/lib/composer/installed.php b/lib/composer/installed.php index f20c14699..0c789fb7e 100644 --- a/lib/composer/installed.php +++ b/lib/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'combodo/itop', 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '1400bdb25295b0e50adbd66ed9c2f3ea857bcf71', + 'reference' => '195d4137172d9169005b6b7f0569866480e6260f', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -22,7 +22,7 @@ 'combodo/itop' => array( 'pretty_version' => 'dev-develop', 'version' => 'dev-develop', - 'reference' => '1400bdb25295b0e50adbd66ed9c2f3ea857bcf71', + 'reference' => '195d4137172d9169005b6b7f0569866480e6260f', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -717,9 +717,9 @@ ), ), 'thenetworg/oauth2-azure' => array( - 'pretty_version' => 'v2.1.1', - 'version' => '2.1.1.0', - 'reference' => '06fb2d620fb6e6c934f632c7ec7c5ea2e978a844', + 'pretty_version' => 'v2.2.2', + 'version' => '2.2.2.0', + 'reference' => 'be204a5135f016470a9c33e82ab48785bbc11af2', 'type' => 'library', 'install_path' => __DIR__ . '/../thenetworg/oauth2-azure', 'aliases' => array(), diff --git a/lib/thenetworg/oauth2-azure/.devcontainer/Dockerfile b/lib/thenetworg/oauth2-azure/.devcontainer/Dockerfile deleted file mode 100644 index 35ab8f9c4..000000000 --- a/lib/thenetworg/oauth2-azure/.devcontainer/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/php/.devcontainer/base.Dockerfile -ARG VARIANT="7" -FROM mcr.microsoft.com/vscode/devcontainers/php:0-${VARIANT} - -# [Optional] Install a version of Node.js using nvm for front end dev -ARG INSTALL_NODE="true" -ARG NODE_VERSION="lts/*" -RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi - -# [Optional] Uncomment this section to install additional OS packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends - -# [Optional] Uncomment this line to install global node packages. -# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 \ No newline at end of file diff --git a/lib/thenetworg/oauth2-azure/.devcontainer/devcontainer.json b/lib/thenetworg/oauth2-azure/.devcontainer/devcontainer.json deleted file mode 100644 index 676de3dcb..000000000 --- a/lib/thenetworg/oauth2-azure/.devcontainer/devcontainer.json +++ /dev/null @@ -1,29 +0,0 @@ -// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/php -{ - "name": "PHP", - "build": { - "dockerfile": "Dockerfile", - "args": { - // Update VARIANT to pick a PHP version: 7, 7.4, 7.3 - "VARIANT": "7", - "INSTALL_NODE": "false", - "NODE_VERSION": "lts/*" - } - }, - // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.shell.linux": "/bin/bash" - }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "felixfbecker.php-debug", - "felixfbecker.php-intellisense" - ], - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "php -v", - // Comment out to connect as root instead. - "remoteUser": "vscode" -} \ No newline at end of file diff --git a/lib/thenetworg/oauth2-azure/.gitignore b/lib/thenetworg/oauth2-azure/.gitignore deleted file mode 100644 index 57a23bd06..000000000 --- a/lib/thenetworg/oauth2-azure/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -/build -/vendor -composer.phar -composer.lock -.DS_Store - -# IDE -/.idea -/.vscode diff --git a/lib/thenetworg/oauth2-azure/.php_cs b/lib/thenetworg/oauth2-azure/.php_cs deleted file mode 100644 index 47be3dfc0..000000000 --- a/lib/thenetworg/oauth2-azure/.php_cs +++ /dev/null @@ -1,70 +0,0 @@ -in(__DIR__ . '/src'); - -return PhpCsFixer\Config::create() - ->setRiskyAllowed(true) - ->setUsingCache(false) - ->setRules([ - '@PSR2' => true, - 'align_multiline_comment' => true, - 'array_indentation' => true, - 'array_syntax' => ['syntax' => 'short'], - 'binary_operator_spaces' => ['default' => 'align_single_space_minimal'], - 'blank_line_after_opening_tag' => true, - 'class_attributes_separation' => true, - 'combine_consecutive_issets' => true, - 'general_phpdoc_annotation_remove' => ['annotations' => ['author', 'package', 'subpackage']], - 'declare_equal_normalize' => ['space' => 'single'], - 'dir_constant' => true, - 'fully_qualified_strict_types' => true, - 'function_typehint_space' => true, - 'heredoc_to_nowdoc' => true, - 'include' => true, - 'is_null' => ['use_yoda_style' => true], - 'linebreak_after_opening_tag' => true, - 'lowercase_cast' => true, - 'modernize_types_casting' => true, - 'new_with_braces' => true, - 'no_alias_functions' => true, - 'no_alternative_syntax' => true, - 'no_blank_lines_after_phpdoc' => true, - 'no_empty_comment' => true, - 'no_empty_phpdoc' => true, - 'no_empty_statement' => true, - 'no_leading_import_slash' => true, - 'no_leading_namespace_whitespace' => true, - 'no_mixed_echo_print' => ['use' => 'echo'], - 'no_multiline_whitespace_before_semicolons' => true, - 'no_null_property_initialization' => true, - 'no_php4_constructor' => true, - 'no_short_echo_tag' => false, - 'no_unreachable_default_argument_value' => true, - 'no_useless_else' => true, - 'no_useless_return' => true, - 'ordered_class_elements' => true, - 'ordered_imports' => true, - 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], - 'phpdoc_order' => true, - 'phpdoc_return_self_reference' => true, - 'phpdoc_scalar' => true, - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_to_comment' => true, - 'phpdoc_trim' => true, - 'phpdoc_types' => true, - 'phpdoc_types_order' => ['null_adjustment' => 'always_last'], - 'phpdoc_var_without_name' => true, - 'short_scalar_cast' => true, - 'simplified_null_return' => true, - 'single_blank_line_before_namespace' => true, - 'single_line_comment_style' => true, - 'single_quote' => ['strings_containing_single_quote_chars' => true], - 'standardize_increment' => true, - 'standardize_not_equals' => true, - 'trailing_comma_in_multiline_array' => true, - 'trim_array_spaces' => true, - 'whitespace_after_comma_in_array' => true, - 'yoda_style' => true, - ]) - ->setFinder($finder); diff --git a/lib/thenetworg/oauth2-azure/.vscode/launch.json b/lib/thenetworg/oauth2-azure/.vscode/launch.json deleted file mode 100644 index c69965a9e..000000000 --- a/lib/thenetworg/oauth2-azure/.vscode/launch.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Launch application", - "type": "php", - "request": "launch", - "program": "${workspaceFolder}/index.php", - "cwd": "${workspaceFolder}", - "port": 9000 - }, - { - "name": "Listen for XDebug", - "type": "php", - "request": "launch", - "port": 9000 - }, - { - "name": "Launch currently open script", - "type": "php", - "request": "launch", - "program": "${file}", - "cwd": "${fileDirname}", - "port": 9000 - } - ] -} \ No newline at end of file diff --git a/lib/thenetworg/oauth2-azure/CHANGELOG.md b/lib/thenetworg/oauth2-azure/CHANGELOG.md index 689c66685..ac3e4283f 100644 --- a/lib/thenetworg/oauth2-azure/CHANGELOG.md +++ b/lib/thenetworg/oauth2-azure/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -All Notable changes to `oauth2-azure` will be documented in this file +This file is not actively maintained. All Notable changes to `oauth2-azure` are documented at https://github.com/TheNetworg/oauth2-azure/releases ## v1.0.0 - 16NOV2015 -- Initial release \ No newline at end of file +- Initial release diff --git a/lib/thenetworg/oauth2-azure/README.md b/lib/thenetworg/oauth2-azure/README.md index ee6f06d3f..ef1546ac2 100644 --- a/lib/thenetworg/oauth2-azure/README.md +++ b/lib/thenetworg/oauth2-azure/README.md @@ -45,6 +45,10 @@ $provider = new TheNetworg\OAuth2\Client\Provider\Azure([ 'clientId' => '{azure-client-id}', 'clientSecret' => '{azure-client-secret}', 'redirectUri' => 'https://example.com/callback-url', + //Optional using key pair instead of secret + 'clientCertificatePrivateKey' => '{azure-client-certificate-private-key}', + //Optional using key pair instead of secret + 'clientCertificateThumbprint' => '{azure-client-certificate-thumbprint}', //Optional 'scopes' => ['openid'], //Optional @@ -128,6 +132,19 @@ $authUrl = $provider->getAuthorizationUrl([ ``` You can find additional parameters [here](https://msdn.microsoft.com/en-us/library/azure/dn645542.aspx). +#### Using a certificate key pair instead of the shared secret + +- Generate a key pair, e.g. with: +```bash +openssl genrsa -out private.key 2048 +openssl req -new -x509 -key private.key -out publickey.cer -days 365 +``` +- Upload the `publickey.cer` to your app in the Azure portal +- Note the displayed thumbprint for the certificate (it looks like `B4A94A83092455AC4D3AC827F02B61646EAAC43D`) +- Put that thumbprint into the `clientCertificateThumbprint` constructor option +- Put the contents of `private.key` into the `clientCertificatePrivateKey` constructor option +- You can omit the `clientSecret` constructor option + ### Logging out If you need to quickly generate a logout URL for the user, you can do following: ```php diff --git a/lib/thenetworg/oauth2-azure/composer.json b/lib/thenetworg/oauth2-azure/composer.json index 4fb348463..51cdafb5c 100644 --- a/lib/thenetworg/oauth2-azure/composer.json +++ b/lib/thenetworg/oauth2-azure/composer.json @@ -32,5 +32,13 @@ "psr-4": { "TheNetworg\\OAuth2\\Client\\": "src/" } + }, + "autoload-dev": { + "psr-4": { + "TheNetworg\\OAuth2\\Client\\Tests\\": "tests/" + } + }, + "require-dev": { + "phpunit/phpunit": "^9.6" } } diff --git a/lib/thenetworg/oauth2-azure/src/Provider/Azure.php b/lib/thenetworg/oauth2-azure/src/Provider/Azure.php index 50aec8235..bf9c7539d 100644 --- a/lib/thenetworg/oauth2-azure/src/Provider/Azure.php +++ b/lib/thenetworg/oauth2-azure/src/Provider/Azure.php @@ -11,6 +11,7 @@ use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Provider\ResourceOwnerInterface; use League\OAuth2\Client\Token\AccessTokenInterface; use League\OAuth2\Client\Tool\BearerAuthorizationTrait; +use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use TheNetworg\OAuth2\Client\Grant\JwtBearer; use TheNetworg\OAuth2\Client\Token\AccessToken; @@ -44,6 +45,20 @@ class Azure extends AbstractProvider public $authWithResource = true; + public $defaultAlgorithm = null; + + /** + * The contents of the private key used for app authentication + * @var string + */ + protected $clientCertificatePrivateKey = ''; + + /** + * The hexadecimal certificate thumbprint as displayed in the azure portal + * @var string + */ + protected $clientCertificateThumbprint = ''; + public function __construct(array $options = [], array $collaborators = []) { parent::__construct($options, $collaborators); @@ -54,6 +69,9 @@ class Azure extends AbstractProvider in_array($options['defaultEndPointVersion'], self::ENDPOINT_VERSIONS, true)) { $this->defaultEndPointVersion = $options['defaultEndPointVersion']; } + if (isset($options['defaultAlgorithm'])) { + $this->defaultAlgorithm = $options['defaultAlgorithm']; + } $this->grantFactory->setGrant('jwt_bearer', new JwtBearer()); } @@ -103,6 +121,32 @@ class Azure extends AbstractProvider return $openIdConfiguration['token_endpoint']; } + protected function getAccessTokenRequest(array $params): RequestInterface + { + if ($this->clientCertificatePrivateKey && $this->clientCertificateThumbprint) { + $header = [ + 'x5t' => base64_encode(hex2bin($this->clientCertificateThumbprint)), + ]; + $now = time(); + $payload = [ + 'aud' => "https://login.microsoftonline.com/{$this->tenant}/oauth2/v2.0/token", + 'exp' => $now + 360, + 'iat' => $now, + 'iss' => $this->clientId, + 'jti' => bin2hex(random_bytes(20)), + 'nbf' => $now, + 'sub' => $this->clientId, + ]; + $jwt = JWT::encode($payload, str_replace('\n', "\n", $this->clientCertificatePrivateKey), 'RS256', null, $header); + + unset($params['client_secret']); + $params['client_assertion_type'] = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'; + $params['client_assertion'] = $jwt; + } + + return parent::getAccessTokenRequest($params); + } + /** * @inheritdoc */ @@ -134,7 +178,8 @@ class Azure extends AbstractProvider */ public function getResourceOwnerDetailsUrl(\League\OAuth2\Client\Token\AccessToken $token): string { - return ''; // shouldn't that return such a URL? + $openIdConfiguration = $this->getOpenIdConfiguration($this->tenant, $this->defaultEndPointVersion); + return $openIdConfiguration['userinfo_endpoint']; } public function getObjects($tenant, $ref, &$accessToken, $headers = []) @@ -178,8 +223,8 @@ class Azure extends AbstractProvider $version = $this->defaultEndPointVersion; } else { $idTokenClaims = $accessToken->getIdTokenClaims(); - $tenant = array_key_exists('tid', $idTokenClaims) ? $idTokenClaims['tid'] : $this->tenant; - $version = array_key_exists('ver', $idTokenClaims) ? $idTokenClaims['ver'] : $this->defaultEndPointVersion; + $tenant = is_array($idTokenClaims) && array_key_exists('tid', $idTokenClaims) ? $idTokenClaims['tid'] : $this->tenant; + $version = is_array($idTokenClaims) && array_key_exists('ver', $idTokenClaims) ? $idTokenClaims['ver'] : $this->defaultEndPointVersion; } $openIdConfiguration = $this->getOpenIdConfiguration($tenant, $version); return 'https://' . $openIdConfiguration['msgraph_host']; @@ -295,7 +340,7 @@ class Azure extends AbstractProvider public function validateAccessToken($accessToken) { $keys = $this->getJwtVerificationKeys(); - $tokenClaims = (array)JWT::decode($accessToken, $keys, ['RS256']); + $tokenClaims = (array)JWT::decode($accessToken, $keys); $this->validateTokenClaims($tokenClaims); @@ -376,13 +421,18 @@ class Azure extends AbstractProvider $keys[$keyinfo['kid']] = new Key($publicKey, 'RS256'); } } else if (isset($keyinfo['n']) && isset($keyinfo['e'])) { - $pkey_object = JWK::parseKey($keyinfo); + $alg = $this->defaultAlgorithm; + if (is_null($alg) && isset($keyinfo['kty'])) { + $alg = $keyinfo['kty']; + } + + $pkey_object = JWK::parseKey($keyinfo, $alg); if ($pkey_object === false) { throw new \RuntimeException('An attempt to read a public key from a ' . $keyinfo['n'] . ' certificate failed.'); } - $pkey_array = openssl_pkey_get_details($pkey_object); + $pkey_array = openssl_pkey_get_details($pkey_object->getKeyMaterial()); if ($pkey_array === false) { throw new \RuntimeException('An attempt to get a public key as an array from a ' . $keyinfo['n'] . ' certificate failed.'); diff --git a/lib/thenetworg/oauth2-azure/src/Token/AccessToken.php b/lib/thenetworg/oauth2-azure/src/Token/AccessToken.php index 7de80f9e5..a268c2038 100644 --- a/lib/thenetworg/oauth2-azure/src/Token/AccessToken.php +++ b/lib/thenetworg/oauth2-azure/src/Token/AccessToken.php @@ -3,9 +3,8 @@ namespace TheNetworg\OAuth2\Client\Token; use Firebase\JWT\JWT; -use InvalidArgumentException; -use League\OAuth2\Client\Tool\RequestFactory; use RuntimeException; +use TheNetworg\OAuth2\Client\Provider\Azure; class AccessToken extends \League\OAuth2\Client\Token\AccessToken { @@ -13,6 +12,9 @@ class AccessToken extends \League\OAuth2\Client\Token\AccessToken protected $idTokenClaims; + /** + * @param Azure $provider + */ public function __construct(array $options, $provider) { parent::__construct($options); @@ -27,7 +29,7 @@ class AccessToken extends \League\OAuth2\Client\Token\AccessToken $tks = explode('.', $this->idToken); // Check if the id_token contains signature if (3 == count($tks) && !empty($tks[2])) { - $idTokenClaims = (array)JWT::decode($this->idToken, $keys, ['RS256']); + $idTokenClaims = (array)JWT::decode($this->idToken, $keys); } else { // The id_token is unsigned (coming from v1.0 endpoint) - https://msdn.microsoft.com/en-us/library/azure/dn645542.aspx