8000 feat(auth.json): support for authorization with client TLS-certificates by rtm-ctrlz · Pull Request #12406 · composer/composer · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat(auth.json): support for authorization with client TLS-certificates #12406

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

Open
wants to merge 3 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
32 changes: 32 additions & 0 deletions doc/articles/authentication-for-private-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ for credentials and save them (or a token if Composer is able to retrieve one).
|[gitlab-token](#gitlab-token)|yes|
|[github-oauth](#github-oauth)|yes|
|[bitbucket-oauth](#bitbucket-oauth)|yes|
|[Client TLS certificates](#client-tls-certificates)|no|

Sometimes automatic authentication is not possible, or you may want to predefine
authentication credentials.
Expand Down Expand Up @@ -397,3 +398,34 @@ php composer.phar config [--global] --editor --auth
}
}
```

## Client TLS certificates

Accessing private repositories that require client TLS certificates.

For global/project-wide configuration see [Handling private packages: Security section](handling-private-packages.md#security).

### Manual client certificates

```shell
php composer.phar config [--global] --editor --auth
```

```json
{
"client-certificate": {
"repo.example.org": {
"local_cert": "/path/to/certificate",
"local_pk": "/path/to/key",
"passphrase": "MySecretPassword"
}
}
}
```

Supported options are `local_cert` (required), `local_pk`, `passphrase`.
More information for options can be found at [SSL context options](https://www.php.net/manual/en/context.ssl.php)

Options could be omitted:
- `local_pk`: in case of keeping certificate and private key in a single file;
- `passphrase`: in case of passwordless private key.
8 changes: 8 additions & 0 deletions phpstan/baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4446,6 +4446,14 @@ parameters:
count: 1
path: ../tests/Composer/Test/TestCase.php

-
message: """
#^Call to deprecated method addAuthenticationHeader\\(\\) of class Composer\\\\Util\\\\AuthHelper\\:
use addAuthenticationOptions instead$#
"""
count: 3
path: ../tests/Composer/Test/Util/AuthHelperTest.php

-
message: "#^Cannot access an offset on array\\<int, array\\<string, array\\<int, string\\>\\|int\\|string\\>\\>\\|false\\.$#"
count: 2
Expand Down
22 changes: 22 additions & 0 deletions res/composer-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,28 @@
}
}
},
"client-certificate": {
"type": "object",
"description": "An object of dom 8000 ain name => {\"local_cert\": \"...\", \"local_pk\"?: \"...\", \"passphrase\"?: \"...\"} to provide client certificate.",
"additionalProperties": {
"type": "object",
"required": ["local_cert"],
"properties": {
"local_cert": {
"type": "string",
"description": "Path to a certificate (pem) or pair certificate+key (pem)"
},
"local_pk": {
"type": "string",
"description": "Path to a private key file (pem)"
},
"passphrase": {
"type": "string",
"description": "Passphrase for private key"
}
}
}
},
"store-auths": {
"type": ["string", "boolean"],
"description": "What to do after prompting for authentication, one of: true (store), false (do not store) or \"prompt\" (ask every time), defaults to prompt."
Expand Down
3 changes: 2 additions & 1 deletion src/Composer/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class Config
'custom-headers' => [],
'bump-after-update' => false,
'allow-missing-requirements' => false,
'client-certificate' => [],
];

/** @var array<string, mixed> */
Expand Down Expand Up @@ -192,7 +193,7 @@ public function merge(array $config, string $source = self::SOURCE_UNKNOWN): voi
// override defaults with given config
if (!empty($config['config']) && is_array($config['config'])) {
foreach ($config['config'] as $key => $val) {
if (in_array($key, ['bitbucket-oauth', 'github-oauth', 'gitlab-oauth', 'gitlab-token', 'http-basic', 'bearer'], true) && isset($this->config[$key])) {
if (in_array($key, ['bitbucket-oauth', 'github-oauth', 'gitlab-oauth', 'gitlab-token', 'http-basic', 'bearer', 'client-certificate'], true) && isset($this->config[$key])) {
$this->config[$key] = array_merge($this->config[$key], $val);
$this->setSourceOfConfigValue($val, $key, $source);
} elseif (in_array($key, ['allow-plugins'], true) && isset($this->config[$key]) && is_array($this->config[$key]) && is_array($val)) {
Expand Down
23 changes: 23 additions & 0 deletions src/Composer/IO/BaseIO.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public function loadConfiguration(Config $config)
$httpBasic = $config->get('http-basic');
$bearerToken = $config->get('bearer');
$customHeaders = $config->get('custom-headers');
$clientCertificate = $config->get('client-certificate');

// reload oauth tokens from config if available

Expand Down Expand Up @@ -180,6 +181,28 @@ public function loadConfiguration(Config $config)
}
}

// reload ssl client certificate credentials from config if available
foreach ($clientCertificate as $domain => $cred) {
$sslOptions = array_filter(
[
'local_cert' => $cred['local_cert'] ?? null,
'local_pk' => $cred['local_pk'] ?? null,
'passphrase' => $cred['passphrase'] ?? null,
],
static function (?string $value): bool { return $value !== null; }
);
if (!isset($sslOptions['local_cert'])) {
$this->writeError(
sprintf(
'<warning>Warning: Client certificate configuration is missing key `local_cert` for %s.</warning>',
$domain
)
);
continue;
}
$this->checkAndSetAuthentication($domain, 'client-certificate', (string)json_encode($sslOptions));
}

// setup process timeout
ProcessExecutor::setTimeout($config->get('process-timeout'));
}
Expand Down
34 changes: 30 additions & 4 deletions src/Composer/Util/AuthHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,35 @@ public function promptAuthIfNeeded(string $url, string $origin, int $statusCode,
}

/**
* @deprecated use addAuthenticationOptions instead
*
* @param string[] $headers
*
* @return string[] updated headers array
*/
public function addAuthenticationHeader(array $headers, string $origin, string $url): array
{
trigger_error('AuthHelper::addAuthenticationHeader is deprecated since Composer 2.9 use addAuthenticationOptions instead.', E_USER_DEPRECATED);

$options = ['http' => ['header' => &$headers]];
$options = $this->addAuthenticationOptions($options, $origin, $url);
return $options['http']['header'];
}

/**
* @param array<string, mixed> $options
*
* @return array<string, mixed> updated options
*/
public function addAuthenticationOptions(array $options, string $origin, string $url): array
{
if (!isset($options['http'])) {
$options['http'] = [];
}
if (!isset($options['http']['header'])) {
$options['http']['header'] = [];
}
$headers = &$options['http']['header'];
if ($this->io->hasAuthentication($origin)) {
$authenticationDisplayMessage = null;
$auth = $this->io->getAuthentication($origin);
Expand All @@ -257,8 +280,8 @@ public function addAuthenticationHeader(array $headers, string $origin, string $
$authenticationDisplayMessage = 'Using GitHub token authentication';
}
} elseif (
in_array($origin, $this->config->get('gitlab-domains'), true)
&& in_array($auth['password'], ['oauth2', 'private-token', 'gitlab-ci-token'], true)
in_array($auth['password'], ['oauth2', 'private-token', 'gitlab-ci-token'], true)
&& in_array($origin, $this->config->get('gitlab-domains'), true)
) {
if ($auth['password'] === 'oauth2') {
$headers[] = 'Authorization: Bearer '.$auth['username'];
Expand All @@ -276,6 +299,9 @@ public function addAuthenticationHeader(array $headers, string $origin, string $
$headers[] = 'Authorization: Bearer ' . $auth['password'];
$authenticationDisplayMessage = 'Using Bitbucket OAuth token authentication';
}
} elseif ('client-certificate' === $auth['password']) {
$options['ssl'] = array_merge($options['ssl'] ?? [], json_decode((string)$auth['username'], true));
$authenticationDisplayMessage = 'Using SSL client certificate';
} else {
$authStr = base64_encode($auth['username'] . ':' . $auth['password']);
$headers[] = 'Authorization: Basic '.$authStr;
Expand All @@ -287,10 +313,10 @@ public function addAuthenticationHeader(array $headers, string $origin, string $
$this->displayedOriginAuthentications[$origin] = $authenticationDisplayMessage;
}
} elseif (in_array($origin, ['api.bitbucket.org', 'api.github.com'], true)) {
return $this->addAuthenticationHeader($headers, str_replace('api.', '', $origin), $url);
return $this->addAuthenticationOptions($options, str_replace('api.', '', $origin), $url);
}

return $headers;
return $options;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Composer/Util/Http/CurlDownloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ private function initDownload(callable $resolve, callable $reject, string $origi
curl_setopt($curlHandle, CURLOPT_ENCODING, "gzip");
}

$options['http']['header'] = $this->authHelper->addAuthenticationHeader($options['http']['header'], $origin, $url);
$options = $this->authHelper->addAuthenticationOptions($options, $origin, $url);
$options = StreamContextFactory::initOptions($url, $options, true);

foreach (self::$options as $type => $curlOptions) {
Expand Down
8 changes: 4 additions & 4 deletions src/Composer/Util/RemoteFilesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -632,13 +632,13 @@ protected function getOptionsForUrl(string $originUrl, array $additionalOptions)
$headers[] = 'Connection: close';
}

$headers = $this->authHelper->addAuthenticationHeader($headers, $originUrl, $this->fileUrl);

$options['http']['follow_location'] = 0;

if (isset($options['http']['header']) && !is_array($options['http']['header'])) {
$options['http']['header'] = explode("\r\n", trim($options['http']['header'], "\r\n"));
}
$options = $this->authHelper->addAuthenticationOptions($options, $originUrl, $this->fileUrl);

$options['http']['follow_location'] = 0;

foreach ($headers as $header) {
$options['http']['header'][] = $header;
}
Expand Down
Loading
0