This commit is contained in:
Andrey Zvonov 2024-08-27 17:28:52 -07:00 committed by GitHub
commit 92e6631c31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 1259 additions and 38 deletions

3
.gitmodules vendored
View File

@ -369,3 +369,6 @@
[submodule "contrib/numactl"]
path = contrib/numactl
url = https://github.com/ClickHouse/numactl.git
[submodule "contrib/jwt-cpp"]
path = contrib/jwt-cpp
url = https://github.com/Thalhammer/jwt-cpp.git

View File

@ -80,7 +80,7 @@ add_contrib (openldap-cmake openldap)
add_contrib (grpc-cmake grpc)
add_contrib (msgpack-c-cmake msgpack-c)
add_contrib (libarchive-cmake libarchive)
add_contrib (jwt-cpp-cmake jwt-cpp)
add_contrib (corrosion-cmake corrosion)
if (ENABLE_FUZZING)

1
contrib/jwt-cpp vendored Submodule

@ -0,0 +1 @@
Subproject commit a6927cb8140858c34e05d1a954626b9849fbcdfc

View File

@ -0,0 +1,3 @@
add_library(_jwt-cpp INTERFACE)
target_include_directories(_jwt-cpp SYSTEM BEFORE INTERFACE "${ClickHouse_SOURCE_DIR}/contrib/jwt-cpp/include/")
add_library(ch_contrib::jwt-cpp ALIAS _jwt-cpp)

View File

@ -16,4 +16,5 @@ The following external authenticators and directories are supported:
- [LDAP](./ldap.md#external-authenticators-ldap) [Authenticator](./ldap.md#ldap-external-authenticator) and [Directory](./ldap.md#ldap-external-user-directory)
- Kerberos [Authenticator](./kerberos.md#external-authenticators-kerberos)
- [SSL X.509 authentication](./ssl-x509.md#ssl-external-authentication)
- HTTP [Authenticator](./http.md)
- HTTP [Authenticator](./http.md)
- JWT [Authenticator](./jwt.md)

View File

@ -0,0 +1,205 @@
---
slug: /en/operations/external-authenticators/jwt
---
# JWT
import SelfManaged from '@site/docs/en/_snippets/_self_managed_only_no_roadmap.md';
<SelfManaged />
Existing and properly configured ClickHouse users can be authenticated via JWT.
Currently, JWT can only be used as an external authenticator for existing users, which are defined in `users.xml` or in local access control paths.
JWT shall contain the name of the ClickHouse user under "sub" claim.
The username will be extracted from the JWT after validating the token expiration and against the signature. Signature can be validated by:
- static public key
- static JWKS
- received from the JWKS servers
Additionally, each user can be verified using a JWT payload. In this case, after running general checks, the occurrence of CLAIMS from the user settings in the JWT payload is checked.
For this approach, JWT validators must be configured in the system and must be enabled in ClickHouse config.
## Enabling JWT validators in ClickHouse {#enabling-jwt-validators-in-clickhouse}
To enable JWT validators, one should include `jwt_verifiers` section in `config.xml`. This section may contain several JWT verifiers, minimum is 1.
### Verifying JWT signature using static key {$verifying-jwt-signature-using-static-key}
**Example**
```xml
<clickhouse>
<!- ... -->
<jwt_verifiers>
<basic_jwt_validator>
<algo>HS256</algo>
<single_key>c3VwZXIKa2V5</single_key>
<single_key_in_base64>true</single_key_in_base64>
</basic_jwt_validator>
</jwt_verifiers>
</clickhouse>
```
#### Parameters:
- `algo` - Algorithm for validate signature. Supported:
| HMSC | RSA | ECDSA | PSS | EdDSA |
| ----- | ----- | ------ | ----- | ------- |
| HS256 | RS256 | ES256 | PS256 | Ed25519 |
| HS384 | RS384 | ES384 | PS384 | Ed448 |
| HS512 | RS512 | ES512 | PS512 | |
| | | ES256K | | |
Also support None.
- `single_key` - key for HS* algorithms. Required in these algorithms.
- `single_key_in_base64` - a sign that the `single_key` key is encoded in base64
- `public_key` - public key for validate in all algorithms except HS* family and None. Required in these algorithms.
- `private_key` - private key for validate in all algorithms except HS* family and None. Optional in these algorithms.
- `public_key_password` - public key password for verification in all algorithms except the HS* family and None. Optional in these algorithms.
- `private_key_password` - private key password for verification in all algorithms except the HS* family and None. Optional in these algorithms.
### Verifying JWT signature using static JWKS {$verifying-jwt-signature-using-static-jwks}
**Example**
```xml
<clickhouse>
<!- ... -->
<jwt_verifiers>
<basic_jwt_validator>
<static_jwks>CONTENT_OF_JWKS</static_jwks>
</basic_jwt_validator>
</jwt_verifiers>
</clickhouse>
```
#### Parameters:
- `static_jwks` - content of JWKS in json
- `static_jwks_file` - path to file with JWKS
:::note
Only RS* family algorithms are supported
:::
:::note
Only one of `static_jwks` or `static_jwks_file` keys must be present in one verifier
:::
### Verifying JWT signature using JWKS servers {$verifying-jwt-signature-using-static-jwks}
**Example**
```xml
<clickhouse>
<!- ... -->
<jwt_verifiers>
<basic_auth_server>
<uri>http://localhost:8000/jwks.json</uri>
<connection_timeout_ms>1000</connection_timeout_ms>
<receive_timeout_ms>1000</receive_timeout_ms>
<send_timeout_ms>1000</send_timeout_ms>
<max_tries>3</max_tries>
<retry_initial_backoff_ms>50</retry_initial_backoff_ms>
<retry_max_backoff_ms>1000</retry_max_backoff_ms>
<refresh_ms>300000</refresh_ms>
</basic_auth_server>
</jwt_verifiers>
</clickhouse>
```
#### Parameters:
- `uri` - URI for making authentication request
- `refresh_ms` - Period for resend request for refreshing JWKS. Default: 300000 ms.
Timeouts in milliseconds on the socket used for communicating with the server:
- `connection_timeout_ms` - Default: 1000 ms.
- `receive_timeout_ms` - Default: 1000 ms.
- `send_timeout_ms` - Default: 1000 ms.
Retry parameters:
- `max_tries` - The maximum number of attempts to make an authentication request. Default: 3
- `retry_initial_backoff_ms` - The backoff initial interval on retry. Default: 50 ms
- `retry_max_backoff_ms` - The maximum backoff interval. Default: 1000 ms
Note, that you can define multiple HTTP servers inside the `jwt_verifiers` section using distinct names.
### Enabling JWT authentication in `users.xml` {#enabling-jwt-auth-in-users-xml}
In order to enable HTTP authentication for the user, specify `jwt` section instead of `password` or similar sections in the user definition.
Parameters:
- `claims` - An optional string containing a json object that should be contained in the token payload.
Example (goes into `users.xml`):
```xml
<clickhouse>
<!- ... -->
<my_user>
<!- ... -->
<jwt>
<claims>{"resource_access":{"account": {"roles": ["view-profile"]}}}</claims>
</jwt>
</my_user>
</clickhouse>
```
In this sample payload must contains "view-profile" in array on path resource_access.account.roles like
```
{
...
"realm_access": {
"roles": [
"default-roles-master",
"offline_access",
"uma_authorization"
]
},
...
}
```
:::note
Note that JWT authentication cannot be used alongside with any other authentication mechanism. The presence of any other sections like `password` alongside `jwt` will force ClickHouse to shutdown.
:::
### Enabling JWT authentication using SQL {#enabling-jwt-auth-using-sql}
When [SQL-driven Access Control and Account Management](/docs/en/guides/sre/user-management/index.md#access-control) is enabled in ClickHouse, users identified by JWT authentication can also be created using SQL statements.
```sql
CREATE USER my_user IDENTIFIED WITH jwt CLAIMS '{"resource_access":{"account": {"roles": ["view-profile"]}}}'
```
...or, without additional JWT payload checks
```sql
CREATE USER my_user IDENTIFIED WITH jwt
```
## JWT authorization examples {#jwt-authorization-examples}
#### Console client
```
clickhouse-client -jwt <token>
```
#### HTTP requests
```
curl 'http://localhost:8080/?add_http_cors_header=1&default_format=JSONCompact&max_result_rows=1000&max_result_bytes=10000000&result_overflow_mode=break' \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content type: text/plain;charset=UTF-8' \
--data-raw 'SELECT current_user()'
```
:::note
The token can be obtained (by priority):
- header X-ClickHouse-JWT-Token
- header Authorization
- request parameter "token". In this case, the "Bearer" prefix should not exist.
:::
### Passing session settings {#passing-session-settings}
If `settings_key` exists in the `jwt_verifiers` section or exists in the verifier section and the payload contains a sub-object of that `settings_key`, ClickHouse will attempt to parse its key:value pairs as string values and set them as session settings for the currently authenticated user. If parsing fails, the JWT payload will be ignored.
The `settings_key` in the verifier section takes precedence over the `settings_key` from the `jwt_verifiers` section. If `settings_key` in the verifier section does not exist, the `settings_key` from the `jwt_verifiers` section will be used.

View File

@ -598,7 +598,7 @@ AuthResult AccessControl::authenticate(const Credentials & credentials, const Po
try
{
const auto auth_result = MultipleAccessStorage::authenticate(credentials, address, *external_authenticators, allow_no_password,
allow_plaintext_password);
allow_plaintext_password, isJWTAllowed());
if (authentication_quota)
authentication_quota->reset(QuotaType::FAILED_SEQUENTIAL_AUTHENTICATIONS);
@ -689,6 +689,11 @@ bool AccessControl::isNoPasswordAllowed() const
return allow_no_password;
}
bool AccessControl::isJWTAllowed() const
{
return external_authenticators->isJWTAllowed();
}
void AccessControl::setPlaintextPasswordAllowed(bool allow_plaintext_password_)
{
allow_plaintext_password = allow_plaintext_password_;

View File

@ -148,6 +148,8 @@ public:
void setNoPasswordAllowed(bool allow_no_password_);
bool isNoPasswordAllowed() const;
bool isJWTAllowed() const;
/// Allows users with plaintext password (by default it's allowed).
void setPlaintextPasswordAllowed(bool allow_plaintext_password_);
bool isPlaintextPasswordAllowed() const;

View File

@ -110,7 +110,7 @@ bool Authentication::areCredentialsValid(
throw Authentication::Require<BasicCredentials>("ClickHouse Basic Authentication");
case AuthenticationType::JWT:
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "JWT is available only in ClickHouse Cloud");
throw Authentication::Require<JWTCredentials>("ClickHouse JWT Authentication");
case AuthenticationType::KERBEROS:
return external_authenticators.checkKerberosCredentials(auth_data.getKerberosRealm(), *gss_acceptor_context);
@ -154,7 +154,7 @@ bool Authentication::areCredentialsValid(
throw Authentication::Require<BasicCredentials>("ClickHouse X.509 Authentication");
case AuthenticationType::JWT:
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "JWT is available only in ClickHouse Cloud");
throw Authentication::Require<JWTCredentials>("ClickHouse JWT Authentication");
case AuthenticationType::SSH_KEY:
#if USE_SSH
@ -201,7 +201,7 @@ bool Authentication::areCredentialsValid(
#endif
case AuthenticationType::JWT:
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "JWT is available only in ClickHouse Cloud");
throw Authentication::Require<JWTCredentials>("ClickHouse JWT Authentication");
case AuthenticationType::BCRYPT_PASSWORD:
return checkPasswordBcrypt(basic_credentials->getPassword(), auth_data.getPasswordHashBinary());
@ -292,6 +292,38 @@ bool Authentication::areCredentialsValid(
}
#endif
if (const auto * jwt_credentials = typeid_cast<const JWTCredentials *>(&credentials))
{
switch (auth_data.getType())
{
case AuthenticationType::NO_PASSWORD:
case AuthenticationType::PLAINTEXT_PASSWORD:
case AuthenticationType::SHA256_PASSWORD:
case AuthenticationType::DOUBLE_SHA1_PASSWORD:
case AuthenticationType::BCRYPT_PASSWORD:
case AuthenticationType::LDAP:
case AuthenticationType::HTTP:
case AuthenticationType::KERBEROS:
throw Authentication::Require<BasicCredentials>("ClickHouse Basic Authentication");
case AuthenticationType::JWT:
return external_authenticators.checkJWTCredentials(auth_data.getJWTClaims(), *jwt_credentials, settings);
case AuthenticationType::SSL_CERTIFICATE:
throw Authentication::Require<BasicCredentials>("ClickHouse X.509 Authentication");
case AuthenticationType::SSH_KEY:
#if USE_SSH
throw Authentication::Require<SshCredentials>("SSH Keys Authentication");
#else
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "SSH is disabled, because ClickHouse is built without libssh");
#endif
case AuthenticationType::MAX:
break;
}
}
if ([[maybe_unused]] const auto * always_allow_credentials = typeid_cast<const AlwaysAllowCredentials *>(&credentials))
return true;

View File

@ -14,7 +14,9 @@
#include <base/hex.h>
#include <boost/algorithm/hex.hpp>
#include <boost/algorithm/string/case_conv.hpp>
#include <picojson/picojson.h>
#include "Access/Common/AuthenticationType.h"
#include <Access/Common/SSLCertificateSubjects.h>
#include "config.h"
@ -332,7 +334,10 @@ std::shared_ptr<ASTAuthenticationData> AuthenticationData::toAST() const
}
case AuthenticationType::JWT:
{
throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "JWT is available only in ClickHouse Cloud");
const auto &claims = getJWTClaims();
if (!claims.empty())
node->children.push_back(std::make_shared<ASTLiteral>(claims));
break;
}
case AuthenticationType::KERBEROS:
{
@ -541,6 +546,20 @@ AuthenticationData AuthenticationData::fromAST(const ASTAuthenticationData & que
auth_data.setHTTPAuthenticationServerName(server);
auth_data.setHTTPAuthenticationScheme(scheme);
}
else if (query.type == AuthenticationType::JWT)
{
if (!args.empty())
{
String value = checkAndGetLiteralArgument<String>(args[0], "claims");
picojson::value json_obj;
auto error = picojson::parse(json_obj, value);
if (!error.empty())
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Bad JWT claims: {}", error);
if (!json_obj.is<picojson::object>())
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Bad JWT claims: is not an object");
auth_data.setJWTClaims(value);
}
}
else
{
throw Exception(ErrorCodes::LOGICAL_ERROR, "Unexpected ASTAuthenticationData structure");

View File

@ -74,6 +74,9 @@ public:
const String & getHTTPAuthenticationServerName() const { return http_auth_server_name; }
void setHTTPAuthenticationServerName(const String & name) { http_auth_server_name = name; }
const String & getJWTClaims() const { return jwt_claims; }
void setJWTClaims(const String &jwt_claims_) { jwt_claims = jwt_claims_; }
friend bool operator ==(const AuthenticationData & lhs, const AuthenticationData & rhs);
friend bool operator !=(const AuthenticationData & lhs, const AuthenticationData & rhs) { return !(lhs == rhs); }
@ -106,6 +109,7 @@ private:
/// HTTP authentication properties
String http_auth_server_name;
HTTPAuthenticationScheme http_auth_scheme = HTTPAuthenticationScheme::BASIC;
String jwt_claims;
};
}

View File

@ -1,6 +1,9 @@
#include <Access/Credentials.h>
#include <Access/Common/SSLCertificateSubjects.h>
#include <Common/Exception.h>
#include <Common/logger_useful.h>
#include <jwt-cpp/jwt.h>
namespace DB
{
@ -97,4 +100,26 @@ const String & BasicCredentials::getPassword() const
return password;
}
namespace
{
String extractSubjectFromToken(const String& token)
{
try
{
auto decoded_jwt = jwt::decode(token);
return decoded_jwt.get_subject();
}
catch (...)
{
throw Exception(ErrorCodes::LOGICAL_ERROR, "Failed to validate jwt");
}
}
}
JWTCredentials::JWTCredentials(const String& token_)
: Credentials(extractSubjectFromToken(token_))
, token(token_)
{
is_ready = !user_name.empty();
}
}

View File

@ -123,4 +123,20 @@ private:
};
#endif
class JWTCredentials: public Credentials
{
public:
explicit JWTCredentials(const String& token_);
const String & getToken() const
{
if (!isReady())
{
throwNotReady();
}
return token;
}
private:
String token;
};
}

View File

@ -2,14 +2,21 @@
#include <Access/LDAPClient.h>
#include <Access/SettingsAuthResponseParser.h>
#include <Access/resolveSetting.h>
#include "Common/Logger.h"
#include "Common/logger_useful.h"
#include <Common/Exception.h>
#include <Common/SettingsChanges.h>
#include <Common/SipHash.h>
#include <Common/quoteString.h>
#include "Access/AccessControl.h"
#include "Access/Credentials.h"
#include "Access/JWTVerifier.h"
#include <boost/algorithm/string/case_conv.hpp>
#include <Poco/Util/AbstractConfiguration.h>
#include <map>
#include <memory>
#include <optional>
#include <utility>
@ -254,6 +261,68 @@ HTTPAuthClientParams parseHTTPAuthParams(const Poco::Util::AbstractConfiguration
return http_auth_params;
}
std::unique_ptr<DB::IJWTVerifier> makeJWTVerifier(const Poco::Util::AbstractConfiguration & config, const String & prefix, const String &name, const String &global_settings_key)
{
auto settings_key = String(global_settings_key);
if (config.hasProperty(prefix + ".settings_key"))
settings_key = config.getString(prefix + ".settings_key");
if (config.hasProperty(prefix + ".algo"))
{
SimpleJWTVerifierParams params = {};
params.settings_key = settings_key;
params.algo = config.getString(prefix + ".algo");
params.single_key = config.getString(prefix + ".single_key", "");
params.single_key_in_base64 = config.getBool(prefix + ".single_key_in_base64", false);
params.public_key = config.getString(prefix + ".public_key", "");
params.private_key = config.getString(prefix + ".private_key", "");
params.public_key_password = config.getString(prefix + ".public_key_password", "");
params.private_key_password = config.getString(prefix + ".private_key_password", "");
params.validate();
auto result = std::make_unique<SimpleJWTVerifier>(name);
result->init(params);
return result;
}
std::shared_ptr<IJWKSProvider> provider;
if (config.hasProperty(prefix + ".uri"))
{
JWKSAuthClientParams params;
params.uri = config.getString(prefix + ".uri");
size_t connection_timeout_ms = config.getInt(prefix + ".connection_timeout_ms", 1000);
size_t receive_timeout_ms = config.getInt(prefix + ".receive_timeout_ms", 1000);
size_t send_timeout_ms = config.getInt(prefix + ".send_timeout_ms", 1000);
params.timeouts = ConnectionTimeouts()
.withConnectionTimeout(Poco::Timespan(connection_timeout_ms * 1000))
.withReceiveTimeout(Poco::Timespan(receive_timeout_ms * 1000))
.withSendTimeout(Poco::Timespan(send_timeout_ms * 1000));
params.max_tries = config.getInt(prefix + ".max_tries", 3);
params.retry_initial_backoff_ms = config.getInt(prefix + ".retry_initial_backoff_ms", 50);
params.retry_max_backoff_ms = config.getInt(prefix + ".retry_max_backoff_ms", 1000);
params.refresh_ms = config.getInt(prefix + ".refrest_ms", 300000);
provider = std::make_shared<JWKSClient>(params);
}
else if (config.hasProperty(prefix + ".static_jwks") || config.hasProperty(prefix + ".static_jwks_file"))
{
StaticJWKSParams params;
params.static_jwks = config.getString(prefix + ".static_jwks", "");
params.static_jwks_file = config.getString(prefix + ".static_jwks_file", "");
params.validate();
auto instance = std::make_shared<StaticJWKS>();
instance->init(params);
provider = instance;
}
else
throw DB::Exception(ErrorCodes::BAD_ARGUMENTS, "unsupported configuration");
auto result = std::make_unique<JWKSVerifier>(name, provider);
JWTVerifierParams params = {.settings_key = settings_key};
result->init(params);
return result;
}
}
void parseLDAPRoleSearchParams(LDAPClient::RoleSearchParams & params, const Poco::Util::AbstractConfiguration & config, const String & prefix)
@ -271,6 +340,13 @@ void ExternalAuthenticators::resetImpl()
ldap_client_params_blueprint.clear();
ldap_caches.clear();
kerberos_params.reset();
jwt_verifiers.clear();
}
bool ExternalAuthenticators::isJWTAllowed() const
{
std::lock_guard lock(mutex);
return !jwt_verifiers.empty();
}
void ExternalAuthenticators::reset()
@ -290,8 +366,10 @@ void ExternalAuthenticators::setConfiguration(const Poco::Util::AbstractConfigur
std::size_t ldap_servers_key_count = 0;
std::size_t kerberos_keys_count = 0;
std::size_t http_auth_server_keys_count = 0;
std::size_t jwt_verifiers_count = 0;
const String http_auth_servers_config = "http_authentication_servers";
const String jwt_verifiers_config = "jwt_verifiers";
for (auto key : all_keys)
{
@ -304,6 +382,7 @@ void ExternalAuthenticators::setConfiguration(const Poco::Util::AbstractConfigur
ldap_servers_key_count += (key == "ldap_servers");
kerberos_keys_count += (key == "kerberos");
http_auth_server_keys_count += (key == http_auth_servers_config);
jwt_verifiers_count += (key == jwt_verifiers_config);
}
if (ldap_servers_key_count > 1)
@ -315,6 +394,9 @@ void ExternalAuthenticators::setConfiguration(const Poco::Util::AbstractConfigur
if (http_auth_server_keys_count > 1)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Multiple http_authentication_servers sections are not allowed");
if (jwt_verifiers_count > 1)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Multiple jwt_verifiers sections are not allowed");
Poco::Util::AbstractConfiguration::Keys http_auth_server_names;
config.keys(http_auth_servers_config, http_auth_server_names);
http_auth_servers.clear();
@ -369,6 +451,26 @@ void ExternalAuthenticators::setConfiguration(const Poco::Util::AbstractConfigur
{
tryLogCurrentException(log, "Could not parse Kerberos section");
}
Poco::Util::AbstractConfiguration::Keys jwt_verifiers_keys;
config.keys(jwt_verifiers_config, jwt_verifiers_keys);
jwt_verifiers.clear();
String jwt_verifier_settings_key;
if (config.has(jwt_verifiers_config + ".settings_key"))
jwt_verifier_settings_key = config.getString(jwt_verifiers_config + ".settings_key");
for (const auto & jwt_verifier : jwt_verifiers_keys)
{
if (jwt_verifier == "settings_key") continue;
String prefix = fmt::format("{}.{}", jwt_verifiers_config, jwt_verifier);
try
{
jwt_verifiers[jwt_verifier] = makeJWTVerifier(config, prefix, jwt_verifier, jwt_verifier_settings_key);
}
catch (...)
{
tryLogCurrentException(log, "Could not parse JWT verifier" + backQuote(jwt_verifier));
}
}
}
UInt128 computeParamsHash(const LDAPClient::Params & params, const LDAPClient::RoleSearchParamsList * role_search_params)
@ -547,6 +649,24 @@ HTTPAuthClientParams ExternalAuthenticators::getHTTPAuthenticationParams(const S
return it->second;
}
bool ExternalAuthenticators::checkJWTCredentials(const String &claims, const JWTCredentials & credentials, SettingsChanges &settings) const
{
std::lock_guard lock{mutex};
const auto token = String(credentials.getToken());
const auto &user_name = credentials.getUserName();
for (const auto &it : jwt_verifiers)
{
if (it.second->verify(claims, token, settings))
{
LOG_DEBUG(getLogger("JWTAuth"), "success auth with JWT for {} by {}", user_name, it.first);
return true;
}
LOG_TRACE(getLogger("JWTAuth"), "failed auth with JWT for {} by {}", user_name, it.first);
}
return false;
}
bool ExternalAuthenticators::checkHTTPBasicCredentials(
const String & server, const BasicCredentials & credentials, SettingsChanges & settings) const
{

View File

@ -4,6 +4,7 @@
#include <Access/GSSAcceptor.h>
#include <Access/HTTPAuthClient.h>
#include <Access/LDAPClient.h>
#include <Access/JWTVerifier.h>
#include <base/defines.h>
#include <base/extended_types.h>
#include <base/types.h>
@ -12,6 +13,7 @@
#include <chrono>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <unordered_map>
@ -31,6 +33,7 @@ namespace DB
{
class SettingsChanges;
class AccessControl;
class ExternalAuthenticators
{
@ -43,9 +46,12 @@ public:
const LDAPClient::RoleSearchParamsList * role_search_params = nullptr, LDAPClient::SearchResultsList * role_search_results = nullptr) const;
bool checkKerberosCredentials(const String & realm, const GSSAcceptorContext & credentials) const;
bool checkHTTPBasicCredentials(const String & server, const BasicCredentials & credentials, SettingsChanges & settings) const;
bool checkJWTCredentials(const String &claims, const JWTCredentials & credentials, SettingsChanges & settings) const;
GSSAcceptorContext::Params getKerberosParams() const;
bool isJWTAllowed() const;
private:
HTTPAuthClientParams getHTTPAuthenticationParams(const String& server) const;
@ -65,6 +71,7 @@ private:
mutable LDAPCaches ldap_caches TSA_GUARDED_BY(mutex) ;
std::optional<GSSAcceptorContext::Params> kerberos_params TSA_GUARDED_BY(mutex) ;
std::unordered_map<String, HTTPAuthClientParams> http_auth_servers TSA_GUARDED_BY(mutex) ;
std::unordered_map<String, std::unique_ptr<IJWTVerifier>> jwt_verifiers TSA_GUARDED_BY(mutex) ;
void resetImpl() TSA_REQUIRES(mutex);
};

View File

@ -7,6 +7,7 @@
#include <Common/Exception.h>
#include <Common/quoteString.h>
#include <Common/callOnce.h>
#include "Access/Common/AuthenticationType.h"
#include <IO/WriteHelpers.h>
#include <Interpreters/Context.h>
#include <Poco/UUIDGenerator.h>
@ -491,9 +492,10 @@ AuthResult IAccessStorage::authenticate(
const Poco::Net::IPAddress & address,
const ExternalAuthenticators & external_authenticators,
bool allow_no_password,
bool allow_plaintext_password) const
bool allow_plaintext_password,
bool allow_jwt) const
{
return *authenticateImpl(credentials, address, external_authenticators, /* throw_if_user_not_exists = */ true, allow_no_password, allow_plaintext_password);
return *authenticateImpl(credentials, address, external_authenticators, /* throw_if_user_not_exists = */ true, allow_no_password, allow_plaintext_password, allow_jwt);
}
@ -503,9 +505,10 @@ std::optional<AuthResult> IAccessStorage::authenticate(
const ExternalAuthenticators & external_authenticators,
bool throw_if_user_not_exists,
bool allow_no_password,
bool allow_plaintext_password) const
bool allow_plaintext_password,
bool allow_jwt) const
{
return authenticateImpl(credentials, address, external_authenticators, throw_if_user_not_exists, allow_no_password, allow_plaintext_password);
return authenticateImpl(credentials, address, external_authenticators, throw_if_user_not_exists, allow_no_password, allow_plaintext_password, allow_jwt);
}
@ -515,7 +518,8 @@ std::optional<AuthResult> IAccessStorage::authenticateImpl(
const ExternalAuthenticators & external_authenticators,
bool throw_if_user_not_exists,
bool allow_no_password,
bool allow_plaintext_password) const
bool allow_plaintext_password,
bool allow_jwt) const
{
if (auto id = find<User>(credentials.getUserName()))
{
@ -527,7 +531,8 @@ std::optional<AuthResult> IAccessStorage::authenticateImpl(
auto auth_type = user->auth_data.getType();
if (((auth_type == AuthenticationType::NO_PASSWORD) && !allow_no_password) ||
((auth_type == AuthenticationType::PLAINTEXT_PASSWORD) && !allow_plaintext_password))
((auth_type == AuthenticationType::PLAINTEXT_PASSWORD) && !allow_plaintext_password) ||
((auth_type == AuthenticationType::JWT) && !(allow_jwt && user->auth_data.getType() == AuthenticationType::JWT)))
throwAuthenticationTypeNotAllowed(auth_type);
if (!areCredentialsValid(*user, credentials, external_authenticators, auth_result.settings))

View File

@ -194,14 +194,16 @@ public:
const Poco::Net::IPAddress & address,
const ExternalAuthenticators & external_authenticators,
bool allow_no_password,
bool allow_plaintext_password) const;
bool allow_plaintext_password,
bool allow_jwt) const;
std::optional<AuthResult> authenticate(
const Credentials & credentials,
const Poco::Net::IPAddress & address,
const ExternalAuthenticators & external_authenticators,
bool throw_if_user_not_exists,
bool allow_no_password,
bool allow_plaintext_password) const;
bool allow_plaintext_password,
bool allow_jwt) const;
/// Returns true if this storage can be stored to or restored from a backup.
virtual bool isBackupAllowed() const { return false; }
@ -225,7 +227,8 @@ protected:
const ExternalAuthenticators & external_authenticators,
bool throw_if_user_not_exists,
bool allow_no_password,
bool allow_plaintext_password) const;
bool allow_plaintext_password,
bool allow_jwt) const;
virtual bool areCredentialsValid(
const User & user,
const Credentials & credentials,

494
src/Access/JWTVerifier.cpp Normal file
View File

@ -0,0 +1,494 @@
#include "JWTVerifier.h"
#include <exception>
#include <fstream>
#include <map>
#include <utility>
#include <absl/strings/match.h>
#include <jwt-cpp/jwt.h>
#include <jwt-cpp/traits/kazuho-picojson/traits.h>
#include <picojson/picojson.h>
#include "Poco/StreamCopier.h"
#include <Poco/String.h>
#include "Common/Base64.h"
#include "Common/Exception.h"
#include "Common/logger_useful.h"
#include <Common/SettingsChanges.h>
namespace DB
{
namespace ErrorCodes
{
extern const int JWT_ERROR;
}
namespace
{
bool check_claims(const picojson::value &claims, const picojson::value &payload, const String &path);
bool check_claims(const picojson::value::object &claims, const picojson::value::object &payload, const String &path)
{
for (const auto &it : claims)
{
const auto &payload_it = payload.find(it.first);
if (payload_it == payload.end())
{
LOG_TRACE(getLogger("JWTAuthentication"), "Key '{}.{}' not found in JWT payload", path, it.first);
return false;
}
if (!check_claims(it.second, payload_it->second, path + "." + it.first))
{
return false;
}
}
return true;
}
bool check_claims(const picojson::value::array &claims, const picojson::value::array &payload, const String &path)
{
if (claims.size() > payload.size())
{
LOG_TRACE(getLogger("JWTAuthentication"), "JWT payload too small for claims key '{}'", path);
return false;
}
for (size_t claims_i = 0; claims_i < claims.size(); ++claims_i)
{
bool found = false;
const auto &claims_val = claims.at(claims_i);
for (const auto &payload_val : payload)
{
if (!check_claims(claims_val, payload_val, path + "[" + std::to_string(claims_i) + "]"))
continue;
found = true;
}
if (!found)
{
LOG_TRACE(getLogger("JWTAuthentication"), "JWT payload does not contain an object matching claims key '{}[{}]'", path, claims_i);
return false;
}
}
return true;
}
bool check_claims(const picojson::value &claims, const picojson::value &payload, const String &path)
{
if (claims.is<picojson::array>())
{
if (!payload.is<picojson::array>())
{
LOG_TRACE(getLogger("JWTAuthentication"), "JWT payload does not match key type 'array' in claims '{}'", path);
return false;
}
return check_claims(claims.get<picojson::array>(), payload.get<picojson::array>(), path);
}
if (claims.is<picojson::object>())
{
if (!payload.is<picojson::object>())
{
LOG_TRACE(getLogger("JWTAuthentication"), "JWT payload does not match key type 'object' in claims '{}'", path);
return false;
}
return check_claims(claims.get<picojson::object>(), payload.get<picojson::object>(), path);
}
if (claims.is<bool>())
{
if (!payload.is<bool>())
{
LOG_TRACE(getLogger("JWTAuthentication"), "JWT payload does not match key type 'bool' in claims '{}'", path);
return false;
}
if (claims.get<bool>() != payload.get<bool>())
{
LOG_TRACE(getLogger("JWTAuthentication"), "JWT payload does not match the value in the '{}' assertions. Expected '{}' but given '{}'", path, claims.get<bool>(), payload.get<bool>());
return false;
}
return true;
}
if (claims.is<double>())
{
if (!payload.is<double>())
{
LOG_TRACE(getLogger("JWTAuthentication"), "JWT payload does not match key type 'double' in claims '{}'", path);
return false;
}
if (claims.get<double>() != payload.get<double>())
{
LOG_TRACE(getLogger("JWTAuthentication"), "JWT payload does not match the value in the '{}' assertions. Expected '{}' but given '{}'", path, claims.get<double>(), payload.get<double>());
return false;
}
return true;
}
if (claims.is<std::string>())
{
if (!payload.is<std::string>())
{
LOG_TRACE(getLogger("JWTAuthentication"), "JWT payload does not match key type 'std::string' in claims '{}'", path);
return false;
}
if (claims.get<std::string>() != payload.get<std::string>())
{
LOG_TRACE(getLogger("JWTAuthentication"), "JWT payload does not match the value in the '{}' assertions. Expected '{}' but given '{}'", path, claims.get<std::string>(), payload.get<std::string>());
return false;
}
return true;
}
#ifdef PICOJSON_USE_INT64
if (claims.is<int64_t>())
{
if (!payload.is<int64_t>())
{
LOG_TRACE(getLogger("JWTAuthentication"), "JWT payload does not match key type 'int64_t' in claims '{}'", path);
return false;
}
if (claims.get<int64_t>() != payload.get<int64_t>())
{
LOG_TRACE(getLogger("JWTAuthentication"), "JWT payload does not match the value in claims '{}'. Expected '{}' but given '{}'", path, claims.get<int64_t>(), payload.get<int64_t>());
return false;
}
return true;
}
#endif
LOG_ERROR(getLogger("JWTAuthentication"), "JWT claim '{}' does not match any known type", path);
return false;
}
bool check_claims(const String &claims, const picojson::value::object &payload)
{
if (claims.empty())
return true;
picojson::value json;
auto errors = picojson::parse(json, claims);
if (!errors.empty())
throw Exception(ErrorCodes::JWT_ERROR, "Bad JWT claims: {}", errors);
if (!json.is<picojson::object>())
throw Exception(ErrorCodes::JWT_ERROR, "Bad JWT claims: is not an object");
return check_claims(json.get<picojson::value::object>(), payload, "");
}
std::map<String, Field> stringify_params(const picojson::value &params, const String &path);
std::map<String, Field> stringify_params(const picojson::value::array &params, const String &path)
{
std::map<String, Field> result;
for (size_t i = 0; i < params.size(); ++i)
{
const auto tmp_result = stringify_params(params.at(i), path + "[" + std::to_string(i) + "]");
result.insert(tmp_result.begin(), tmp_result.end());
}
return result;
}
std::map<String, Field> stringify_params(const picojson::value::object &params, const String &path)
{
auto add_path = String(path);
if (!add_path.empty())
add_path = add_path + ".";
std::map<String, Field> result;
for (const auto &it : params)
{
const auto tmp_result = stringify_params(it.second, add_path + it.first);
result.insert(tmp_result.begin(), tmp_result.end());
}
return result;
}
std::map<String, Field> stringify_params(const picojson::value &params, const String &path)
{
std::map<String, Field> result;
if (params.is<picojson::array>())
return stringify_params(params.get<picojson::array>(), path);
if (params.is<picojson::object>())
return stringify_params(params.get<picojson::object>(), path);
if (params.is<bool>())
{
result[path] = Field(params.get<bool>());
return result;
}
if (params.is<std::string>())
{
result[path] = Field(params.get<std::string>());
return result;
}
if (params.is<double>())
{
result[path] = Field(params.get<double>());
return result;
}
#ifdef PICOJSON_USE_INT64
if (params.is<int64_t>())
{
result[path] = Field(params.get<int64_t>());
return result;
}
#endif
return result;
}
}
void IJWTVerifier::init(const JWTVerifierParams &_params)
{
params = _params;
}
bool IJWTVerifier::verify(const String &claims, const String &token, SettingsChanges & settings) const
{
// try
// {
auto decoded_jwt = jwt::decode(token);
if (!verify_impl(decoded_jwt))
return false;
if (!check_claims(claims, decoded_jwt.get_payload_json()))
return false;
if (params.settings_key.empty())
return true;
const auto &payload_obj = decoded_jwt.get_payload_json();
const auto &payload_settings = payload_obj.at(params.settings_key);
const auto string_settings = stringify_params(payload_settings, "");
for (const auto &it : string_settings)
settings.insertSetting(it.first, it.second);
return true;
// }
// catch (const std::exception &ex)
// {
// throw Exception(ErrorCodes::JWT_ERROR, "{}: Failed to validate JWT with exception {}", name, ex.what());
// }
}
void SimpleJWTVerifierParams::validate() const
{
auto lower_algo = Poco::toLower(algo);
if (lower_algo == "none")
return;
if (algo == "ps256" ||
algo == "ps384" ||
algo == "ps512" ||
algo == "ed25519" ||
algo == "ed448" ||
algo == "rs256" ||
algo == "rs384" ||
algo == "rs512" ||
algo == "es256" ||
algo == "es256k" ||
algo == "es384" ||
algo == "es512" )
{
if (!public_key.empty())
return;
throw Exception(ErrorCodes::JWT_ERROR, "`public_key` parameter required for {}", algo);
}
if (algo == "hs256" ||
algo == "hs384" ||
algo == "hs512" )
{
if (!single_key.empty())
return;
throw DB::Exception(ErrorCodes::JWT_ERROR, "`single_key` parameter required for {}", algo);
}
throw DB::Exception(ErrorCodes::JWT_ERROR, "Unknown algorithm {}", algo);
}
SimpleJWTVerifier::SimpleJWTVerifier(const String & _name)
: IJWTVerifier(_name)
, verifier(jwt::verify())
{}
void SimpleJWTVerifier::init(const SimpleJWTVerifierParams & _params)
{
auto algo = Poco::toLower(_params.algo);
IJWTVerifier::init(_params);
verifier = jwt::verify();
if (algo == "none")
verifier = verifier.allow_algorithm(jwt::algorithm::none());
else if (algo == "ps256")
verifier = verifier.allow_algorithm(jwt::algorithm::ps256(_params.public_key, _params.private_key, _params.private_key_password, _params.private_key_password));
else if (algo == "ps384")
verifier = verifier.allow_algorithm(jwt::algorithm::ps384(_params.public_key, _params.private_key, _params.private_key_password, _params.private_key_password));
else if (algo == "ps512")
verifier = verifier.allow_algorithm(jwt::algorithm::ps512(_params.public_key, _params.private_key, _params.private_key_password, _params.private_key_password));
else if (algo == "ed25519")
verifier = verifier.allow_algorithm(jwt::algorithm::ed25519(_params.public_key, _params.private_key, _params.private_key_password, _params.private_key_password));
else if (algo == "ed448")
verifier = verifier.allow_algorithm(jwt::algorithm::ed448(_params.public_key, _params.private_key, _params.private_key_password, _params.private_key_password));
else if (algo == "rs256")
verifier = verifier.allow_algorithm(jwt::algorithm::rs256(_params.public_key, _params.private_key, _params.private_key_password, _params.private_key_password));
else if (algo == "rs384")
verifier = verifier.allow_algorithm(jwt::algorithm::rs384(_params.public_key, _params.private_key, _params.private_key_password, _params.private_key_password));
else if (algo == "rs512")
verifier = verifier.allow_algorithm(jwt::algorithm::rs512(_params.public_key, _params.private_key, _params.private_key_password, _params.private_key_password));
else if (algo == "es256")
verifier = verifier.allow_algorithm(jwt::algorithm::es256(_params.public_key, _params.private_key, _params.private_key_password, _params.private_key_password));
else if (algo == "es256k")
verifier = verifier.allow_algorithm(jwt::algorithm::es256k(_params.public_key, _params.private_key, _params.private_key_password, _params.private_key_password));
else if (algo == "es384")
verifier = verifier.allow_algorithm(jwt::algorithm::es384(_params.public_key, _params.private_key, _params.private_key_password, _params.private_key_password));
else if (algo == "es512")
verifier = verifier.allow_algorithm(jwt::algorithm::es512(_params.public_key, _params.private_key, _params.private_key_password, _params.private_key_password));
else if (algo.starts_with("hs"))
{
auto key = _params.single_key;
if (_params.single_key_in_base64)
key = base64Decode(key);
if (algo == "hs256")
verifier = verifier.allow_algorithm(jwt::algorithm::hs256(key));
else if (algo == "hs384")
verifier = verifier.allow_algorithm(jwt::algorithm::hs384(key));
else if (algo == "hs512")
verifier = verifier.allow_algorithm(jwt::algorithm::hs512(key));
else
throw Exception(ErrorCodes::JWT_ERROR, "Unknown algorithm {}", _params.algo);
}
else
throw Exception(ErrorCodes::JWT_ERROR, "Unknown algorithm {}", _params.algo);
}
bool SimpleJWTVerifier::verify_impl(const jwt::decoded_jwt<jwt::traits::kazuho_picojson> &token) const
{
verifier.verify(token);
return true;
}
JWKSVerifier::JWKSVerifier(const String & _name, std::shared_ptr<IJWKSProvider> _provider)
: IJWTVerifier(_name)
, provider(_provider)
{}
bool JWKSVerifier::verify_impl(const jwt::decoded_jwt<jwt::traits::kazuho_picojson> &token) const
{
auto jwk = provider->getJWKS().get_jwk(token.get_key_id());
auto subject = token.get_subject();
auto algo = Poco::toLower(token.get_algorithm());
auto verifier = jwt::verify();
String public_key;
try
{
auto issuer = token.get_issuer();
auto x5c = jwk.get_x5c_key_value();
if (!x5c.empty() && !issuer.empty())
{
LOG_TRACE(getLogger("JWTAuthentication"), "{}: Verifying {} with 'x5c' key", name, subject);
public_key = jwt::helper::convert_base64_der_to_pem(x5c);
}
}
catch (const jwt::error::claim_not_present_exception &)
{
/// issuer or x5c was not specified, simply do not verify against them
}
catch (const std::bad_cast &)
{
throw Exception(ErrorCodes::JWT_ERROR, "Invalid claim value type: must be string");
}
if (public_key.empty())
{
LOG_TRACE(getLogger("JWTAuthentication"), "{}: `issuer` or `x5c` not present, verifying {} with RSA components", name, subject);
const auto modulus = jwk.get_jwk_claim("n").as_string();
const auto exponent = jwk.get_jwk_claim("e").as_string();
public_key = jwt::helper::create_public_key_from_rsa_components(modulus, exponent);
}
if (algo == "rs256")
verifier = verifier.allow_algorithm(jwt::algorithm::rs256(public_key, "", "", ""));
else if (algo == "rs384")
verifier = verifier.allow_algorithm(jwt::algorithm::rs384(public_key, "", "", ""));
else if (algo == "rs512")
verifier = verifier.allow_algorithm(jwt::algorithm::rs512(public_key, "", "", ""));
else
throw Exception(ErrorCodes::JWT_ERROR, "Unknown algorithm {}", algo);
verifier = verifier.leeway(60UL); // value in seconds, add some to compensate timeout
verifier.verify(token);
return true;
}
JWKSClient::JWKSClient(const JWKSAuthClientParams & params_)
: HTTPAuthClient<JWKSResponseParser>(params_)
, m_refresh_ms(params_.refresh_ms)
{
}
JWKSClient::~JWKSClient() = default;
jwt::jwks<jwt::traits::kazuho_picojson> JWKSClient::getJWKS()
{
{
std::shared_lock lock(m_update_mutex);
auto now = std::chrono::high_resolution_clock::now();
auto diff = std::chrono::duration<double, std::milli>(now - m_last_request_send).count();
if (diff < m_refresh_ms)
{
jwt::jwks<jwt::traits::kazuho_picojson> result(m_jwks);
return result;
}
}
std::unique_lock lock(m_update_mutex);
auto now = std::chrono::high_resolution_clock::now();
auto diff = std::chrono::duration<double, std::milli>(now - m_last_request_send).count();
if (diff < m_refresh_ms)
{
jwt::jwks<jwt::traits::kazuho_picojson> result(m_jwks);
return result;
}
Poco::Net::HTTPRequest request{Poco::Net::HTTPRequest::HTTP_GET, this->getURI().getPathAndQuery()};
auto result = authenticateRequest(request);
m_jwks = std::move(result.keys);
if (result.is_ok)
{
m_last_request_send = std::chrono::high_resolution_clock::now();
}
jwt::jwks<jwt::traits::kazuho_picojson> results(m_jwks);
return results;
}
JWKSResponseParser::Result
JWKSResponseParser::parse(const Poco::Net::HTTPResponse & response, std::istream * body_stream) const
{
Result result;
if (response.getStatus() != Poco::Net::HTTPResponse::HTTPStatus::HTTP_OK)
return result;
result.is_ok = true;
if (!body_stream)
return result;
try
{
String response_data;
Poco::StreamCopier::copyToString(*body_stream, response_data);
auto keys = jwt::parse_jwks(response_data);
result.keys = std::move(keys);
}
catch (...)
{
LOG_INFO(getLogger("JWKSAuthentication"), "Failed to parse jwks from authentication response. Skip it.");
}
return result;
}
void StaticJWKSParams::validate() const
{
if (static_jwks.empty() && static_jwks_file.empty())
throw Exception(ErrorCodes::JWT_ERROR, "`static_jwks` or `static_jwks_file` keys must be present in configuration");
if (!static_jwks.empty() && !static_jwks_file.empty())
throw Exception(ErrorCodes::JWT_ERROR, "`static_jwks` and `static_jwks_file` keys cannot both be present in configuration");
}
void StaticJWKS::init(const StaticJWKSParams& params)
{
params.validate();
String content = String(params.static_jwks);
if (!params.static_jwks_file.empty())
{
std::ifstream ifs(params.static_jwks_file);
content = String((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
}
auto keys = jwt::parse_jwks(content);
jwks = std::move(keys);
}
}

137
src/Access/JWTVerifier.h Normal file
View File

@ -0,0 +1,137 @@
#pragma once
#include <base/types.h>
#include <chrono>
#include <memory>
#include <shared_mutex>
#include <jwt-cpp/jwt.h>
#include <jwt-cpp/traits/kazuho-picojson/traits.h>
#include "Access/HTTPAuthClient.h"
namespace DB
{
class SettingsChanges;
struct JWTVerifierParams
{
String settings_key;
};
class IJWTVerifier
{
public:
explicit IJWTVerifier(const String & _name)
: name(_name)
{}
void init(const JWTVerifierParams &_params);
bool verify(const String &claims, const String &token, SettingsChanges & settings) const;
virtual ~IJWTVerifier() = default;
protected:
virtual bool verify_impl(const jwt::decoded_jwt<jwt::traits::kazuho_picojson> &token) const = 0;
JWTVerifierParams params;
const String name;
};
struct SimpleJWTVerifierParams:
public JWTVerifierParams
{
String algo;
String single_key;
bool single_key_in_base64;
String public_key;
String private_key;
String public_key_password;
String private_key_password;
void validate() const;
};
class SimpleJWTVerifier: public IJWTVerifier
{
public:
explicit SimpleJWTVerifier(const String & _name);
void init(const SimpleJWTVerifierParams & _params);
private:
bool verify_impl(const jwt::decoded_jwt<jwt::traits::kazuho_picojson> &token) const override;
jwt::verifier<jwt::default_clock, jwt::traits::kazuho_picojson> verifier;
};
class IJWKSProvider
{
public:
virtual ~IJWKSProvider() = default;
virtual jwt::jwks<jwt::traits::kazuho_picojson> getJWKS() = 0;
};
class JWKSVerifier: public IJWTVerifier
{
public:
explicit JWKSVerifier(const String & _name, std::shared_ptr<IJWKSProvider> _provider);
private:
bool verify_impl(const jwt::decoded_jwt<jwt::traits::kazuho_picojson> &token) const override;
std::shared_ptr<IJWKSProvider> provider;
};
struct JWKSAuthClientParams: public HTTPAuthClientParams
{
size_t refresh_ms;
};
class JWKSResponseParser
{
static constexpr auto settings_key = "settings";
public:
struct Result
{
bool is_ok = false;
jwt::jwks<jwt::traits::kazuho_picojson> keys;
};
Result parse(const Poco::Net::HTTPResponse & response, std::istream * body_stream) const;
};
class JWKSClient: public IJWKSProvider,
private HTTPAuthClient<JWKSResponseParser>
{
public:
explicit JWKSClient(const JWKSAuthClientParams & params_);
~JWKSClient() override;
JWKSClient(const JWKSClient &) = delete;
JWKSClient(JWKSClient &&) = delete;
JWKSClient & operator= (const JWKSClient &) = delete;
JWKSClient & operator= (JWKSClient &&) = delete;
private:
jwt::jwks<jwt::traits::kazuho_picojson> getJWKS() override;
size_t m_refresh_ms;
std::shared_mutex m_update_mutex;
jwt::jwks<jwt::traits::kazuho_picojson> m_jwks;
std::chrono::time_point<std::chrono::high_resolution_clock> m_last_request_send;
};
struct StaticJWKSParams
{
String static_jwks;
String static_jwks_file;
void validate() const;
};
class StaticJWKS: public IJWKSProvider
{
public:
void init(const StaticJWKSParams& params);
private:
jwt::jwks<jwt::traits::kazuho_picojson> getJWKS() override
{
return jwks;
}
jwt::jwks<jwt::traits::kazuho_picojson> jwks;
};
}

View File

@ -456,7 +456,8 @@ std::optional<AuthResult> LDAPAccessStorage::authenticateImpl(
const ExternalAuthenticators & external_authenticators,
bool throw_if_user_not_exists,
bool /* allow_no_password */,
bool /* allow_plaintext_password */) const
bool /* allow_plaintext_password */,
bool /* allow_jwt */) const
{
std::lock_guard lock(mutex);
auto id = memory_storage.find<User>(credentials.getUserName());

View File

@ -48,7 +48,7 @@ private: // IAccessStorage implementations.
std::vector<UUID> findAllImpl(AccessEntityType type) const override;
AccessEntityPtr readImpl(const UUID & id, bool throw_if_not_exists) const override;
std::optional<std::pair<String, AccessEntityType>> readNameWithTypeImpl(const UUID & id, bool throw_if_not_exists) const override;
std::optional<AuthResult> authenticateImpl(const Credentials & credentials, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators, bool throw_if_user_not_exists, bool allow_no_password, bool allow_plaintext_password) const override;
std::optional<AuthResult> authenticateImpl(const Credentials & credentials, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators, bool throw_if_user_not_exists, bool allow_no_password, bool allow_plaintext_password, bool allow_jwt) const override;
void setConfiguration(const Poco::Util::AbstractConfiguration & config, const String & prefix);
void processRoleChange(const UUID & id, const AccessEntityPtr & entity);

View File

@ -441,7 +441,7 @@ std::optional<AuthResult>
MultipleAccessStorage::authenticateImpl(const Credentials & credentials, const Poco::Net::IPAddress & address,
const ExternalAuthenticators & external_authenticators,
bool throw_if_user_not_exists,
bool allow_no_password, bool allow_plaintext_password) const
bool allow_no_password, bool allow_plaintext_password, bool allow_jwt) const
{
auto storages = getStoragesInternal();
for (size_t i = 0; i != storages->size(); ++i)
@ -450,7 +450,7 @@ MultipleAccessStorage::authenticateImpl(const Credentials & credentials, const P
bool is_last_storage = (i == storages->size() - 1);
auto auth_result = storage->authenticate(credentials, address, external_authenticators,
(throw_if_user_not_exists && is_last_storage),
allow_no_password, allow_plaintext_password);
allow_no_password, allow_plaintext_password, allow_jwt);
if (auth_result)
{
std::lock_guard lock{mutex};

View File

@ -70,7 +70,7 @@ protected:
bool insertImpl(const UUID & id, const AccessEntityPtr & entity, bool replace_if_exists, bool throw_if_exists) override;
bool removeImpl(const UUID & id, bool throw_if_not_exists) override;
bool updateImpl(const UUID & id, const UpdateFunc & update_func, bool throw_if_not_exists) override;
std::optional<AuthResult> authenticateImpl(const Credentials & credentials, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators, bool throw_if_user_not_exists, bool allow_no_password, bool allow_plaintext_password) const override;
std::optional<AuthResult> authenticateImpl(const Credentials & credentials, const Poco::Net::IPAddress & address, const ExternalAuthenticators & external_authenticators, bool throw_if_user_not_exists, bool allow_no_password, bool allow_plaintext_password, bool allow_jwt) const override;
private:
using Storages = std::vector<StoragePtr>;

View File

@ -14,6 +14,7 @@
#include <Common/StringUtils.h>
#include <Common/quoteString.h>
#include <Common/transformEndianness.h>
#include "Access/Credentials.h"
#include <Core/Settings.h>
#include <Interpreters/executeQuery.h>
#include <Parsers/Access/ASTGrantQuery.h>
@ -118,7 +119,8 @@ namespace
const std::unordered_set<UUID> & allowed_profile_ids,
const std::unordered_set<UUID> & allowed_role_ids,
bool allow_no_password,
bool allow_plaintext_password)
bool allow_plaintext_password,
bool allow_jwt)
{
auto user = std::make_shared<User>();
user->setName(user_name);
@ -129,6 +131,7 @@ namespace
bool has_password_double_sha1_hex = config.has(user_config + ".password_double_sha1_hex");
bool has_ldap = config.has(user_config + ".ldap");
bool has_kerberos = config.has(user_config + ".kerberos");
bool has_jwt = config.has(user_config + ".jwt");
const auto certificates_config = user_config + ".ssl_certificates";
bool has_certificates = config.has(certificates_config);
@ -140,18 +143,18 @@ namespace
bool has_http_auth = config.has(http_auth_config);
size_t num_password_fields = has_no_password + has_password_plaintext + has_password_sha256_hex + has_password_double_sha1_hex
+ has_ldap + has_kerberos + has_certificates + has_ssh_keys + has_http_auth;
+ has_ldap + has_kerberos + has_certificates + has_ssh_keys + has_http_auth + has_jwt;
if (num_password_fields > 1)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "More than one field of 'password', 'password_sha256_hex', "
"'password_double_sha1_hex', 'no_password', 'ldap', 'kerberos', 'ssl_certificates', 'ssh_keys', "
"'http_authentication' are used to specify authentication info for user {}. "
"'http_authentication', 'jwt' are used to specify authentication info for user {}. "
"Must be only one of them.", user_name);
if (num_password_fields < 1)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "Either 'password' or 'password_sha256_hex' "
"or 'password_double_sha1_hex' or 'no_password' or 'ldap' or 'kerberos "
"or 'ssl_certificates' or 'ssh_keys' or 'http_authentication' must be specified for user {}.", user_name);
"or 'ssl_certificates' or 'ssh_keys' or 'http_authentication' or 'jwt' must be specified for user {}.", user_name);
if (has_password_plaintext)
{
@ -265,6 +268,12 @@ namespace
auto scheme = config.getString(http_auth_config + ".scheme");
user->auth_data.setHTTPAuthenticationScheme(parseHTTPAuthenticationScheme(scheme));
}
else if (has_jwt)
{
if (!allow_jwt)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "JWT auth not allowed because JWT not configured");
user->auth_data = AuthenticationData{AuthenticationType::JWT};
}
auto auth_type = user->auth_data.getType();
if (((auth_type == AuthenticationType::NO_PASSWORD) && !allow_no_password) ||
@ -419,7 +428,8 @@ namespace
const std::unordered_set<UUID> & allowed_profile_ids,
const std::unordered_set<UUID> & allowed_role_ids,
bool allow_no_password,
bool allow_plaintext_password)
bool allow_plaintext_password,
bool allow_jwt)
{
Poco::Util::AbstractConfiguration::Keys user_names;
config.keys("users", user_names);
@ -430,7 +440,7 @@ namespace
{
try
{
users.push_back(parseUser(config, user_name, allowed_profile_ids, allowed_role_ids, allow_no_password, allow_plaintext_password));
users.push_back(parseUser(config, user_name, allowed_profile_ids, allowed_role_ids, allow_no_password, allow_plaintext_password, allow_jwt));
}
catch (Exception & e)
{
@ -845,9 +855,10 @@ void UsersConfigAccessStorage::parseFromConfig(const Poco::Util::AbstractConfigu
auto allowed_role_ids = getAllowedIDs(config, "roles", AccessEntityType::ROLE);
bool no_password_allowed = access_control.isNoPasswordAllowed();
bool plaintext_password_allowed = access_control.isPlaintextPasswordAllowed();
bool allow_jwt = access_control.isJWTAllowed();
std::vector<std::pair<UUID, AccessEntityPtr>> all_entities;
for (const auto & entity : parseUsers(config, allowed_profile_ids, allowed_role_ids, no_password_allowed, plaintext_password_allowed))
for (const auto & entity : parseUsers(config, allowed_profile_ids, allowed_role_ids, no_password_allowed, plaintext_password_allowed, allow_jwt))
all_entities.emplace_back(generateID(*entity), entity);
for (const auto & entity : parseQuotas(config))
all_entities.emplace_back(generateID(*entity), entity);

View File

@ -352,6 +352,7 @@ target_link_libraries(clickhouse_common_io
ch_contrib::zlib
pcg_random
Poco::Foundation
ch_contrib::jwt-cpp
)
if (TARGET ch_contrib::libfiu)

View File

@ -609,6 +609,7 @@
M(728, UNEXPECTED_DATA_TYPE) \
M(729, ILLEGAL_TIME_SERIES_TAGS) \
M(730, REFRESH_FAILED) \
M(731, JWT_ERROR) \
\
M(900, DISTRIBUTED_CACHE_ERROR) \
M(901, CANNOT_USE_DISTRIBUTED_CACHE) \

View File

@ -86,6 +86,8 @@ static constexpr auto DBMS_MIN_REVISION_WITH_ROWS_BEFORE_AGGREGATION = 54469;
/// Packets size header
static constexpr auto DBMS_MIN_PROTOCOL_VERSION_WITH_CHUNKED_PACKETS = 54470;
static constexpr auto DBMS_MIN_REVISION_WITH_JWT_AUTHENTICATION = 54471;
/// Version of ClickHouse TCP protocol.
///
/// Should be incremented manually on protocol changes.
@ -93,6 +95,6 @@ static constexpr auto DBMS_MIN_PROTOCOL_VERSION_WITH_CHUNKED_PACKETS = 54470;
/// NOTE: DBMS_TCP_PROTOCOL_VERSION has nothing common with VERSION_REVISION,
/// later is just a number for server version (one number instead of commit SHA)
/// for simplicity (sometimes it may be more convenient in some use cases).
static constexpr auto DBMS_TCP_PROTOCOL_VERSION = 54470;
static constexpr auto DBMS_TCP_PROTOCOL_VERSION = 54471;
}

View File

@ -91,8 +91,11 @@ void ASTAuthenticationData::formatImpl(const FormatSettings & settings, FormatSt
}
case AuthenticationType::JWT:
{
prefix = "CLAIMS";
parameter = true;
if (!children.empty())
{
prefix = "CLAIMS";
parameter = true;
}
break;
}
case AuthenticationType::LDAP:

View File

@ -17,7 +17,7 @@ class ASTAuthenticationData;
/** CREATE USER [IF NOT EXISTS | OR REPLACE] name
* [NOT IDENTIFIED | IDENTIFIED {[WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash}] BY {'password'|'hash'}}|{WITH ldap SERVER 'server_name'}|{WITH kerberos [REALM 'realm']}]
* [NOT IDENTIFIED | IDENTIFIED {[WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash}] BY {'password'|'hash'}}|{WITH ldap SERVER 'server_name'}|{WITH kerberos [REALM 'realm']|{WITH jwt [CLAIMS 'json_object']}}]
* [HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
* [DEFAULT ROLE role [,...]]
* [DEFAULT DATABASE database | NONE]
@ -26,7 +26,7 @@ class ASTAuthenticationData;
*
* ALTER USER [IF EXISTS] name
* [RENAME TO new_name]
* [NOT IDENTIFIED | IDENTIFIED {[WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash}] BY {'password'|'hash'}}|{WITH ldap SERVER 'server_name'}|{WITH kerberos [REALM 'realm']}]
* [NOT IDENTIFIED | IDENTIFIED {[WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash}] BY {'password'|'hash'}}|{WITH ldap SERVER 'server_name'}|{WITH kerberos [REALM 'realm']|{WITH jwt [CLAIMS 'json_object']}}]
* [[ADD|DROP] HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
* [DEFAULT ROLE role [,...] | ALL | ALL EXCEPT role [,...] ]
* [DEFAULT DATABASE database | NONE]

View File

@ -67,6 +67,7 @@ namespace
bool expect_ssl_cert_subjects = false;
bool expect_public_ssh_key = false;
bool expect_http_auth_server = false;
bool expect_claims = false;
if (ParserKeyword{Keyword::WITH}.ignore(pos, expected))
{
@ -88,7 +89,8 @@ namespace
expect_http_auth_server = true;
else if (check_type != AuthenticationType::NO_PASSWORD)
expect_password = true;
else if (check_type != AuthenticationType::JWT)
expect_claims = true;
break;
}
}
@ -125,6 +127,7 @@ namespace
ASTPtr http_auth_scheme;
ASTPtr ssl_cert_subjects;
std::optional<String> ssl_cert_subject_type;
ASTPtr jwt_claims;
if (expect_password || expect_hash)
{
@ -189,6 +192,14 @@ namespace
return false;
}
}
else if (expect_claims)
{
if (ParserKeyword{Keyword::CLAIMS}.ignore(pos, expected))
{
if (!ParserStringAndSubstitution{}.parse(pos, jwt_claims, expected))
return false;
}
}
auth_data = std::make_shared<ASTAuthenticationData>();
@ -214,6 +225,9 @@ namespace
if (http_auth_scheme)
auth_data->children.push_back(std::move(http_auth_scheme));
if (jwt_claims)
auth_data->children.push_back(std::move(jwt_claims));
return true;
});
}

View File

@ -7,7 +7,7 @@ namespace DB
{
/** Parses queries like
* CREATE USER [IF NOT EXISTS | OR REPLACE] name
* [NOT IDENTIFIED | IDENTIFIED {[WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash}] BY {'password'|'hash'}}|{WITH ldap SERVER 'server_name'}|{WITH kerberos [REALM 'realm']}]
* [NOT IDENTIFIED | IDENTIFIED {[WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash}] BY {'password'|'hash'}}|{WITH ldap SERVER 'server_name'}|{WITH kerberos [REALM 'realm']}|{WITH jwt}]
* [HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
* [DEFAULT ROLE role [,...]]
* [SETTINGS variable [= value] [MIN [=] min_value] [MAX [=] max_value] [CONST|READONLY|WRITABLE|CHANGEABLE_IN_READONLY] | PROFILE 'profile_name'] [,...]
@ -15,7 +15,7 @@ namespace DB
*
* ALTER USER [IF EXISTS] name
* [RENAME TO new_name]
* [NOT IDENTIFIED | IDENTIFIED {[WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash}] BY {'password'|'hash'}}|{WITH ldap SERVER 'server_name'}|{WITH kerberos [REALM 'realm']}]
* [NOT IDENTIFIED | IDENTIFIED {[WITH {no_password|plaintext_password|sha256_password|sha256_hash|double_sha1_password|double_sha1_hash}] BY {'password'|'hash'}}|{WITH ldap SERVER 'server_name'}|{WITH kerberos [REALM 'realm']}|{WITH jwt}]
* [[ADD|DROP] HOST {LOCAL | NAME 'name' | REGEXP 'name_regexp' | IP 'address' | LIKE 'pattern'} [,...] | ANY | NONE]
* [DEFAULT ROLE role [,...] | ALL | ALL EXCEPT role [,...] ]
* [SETTINGS variable [= value] [MIN [=] min_value] [MAX [=] max_value] [CONST|READONLY|WRITABLE|CHANGEABLE_IN_READONLY] | PROFILE 'profile_name'] [,...]

View File

@ -84,6 +84,7 @@ namespace DB
MR_MACROS(CLEAR_INDEX, "CLEAR INDEX") \
MR_MACROS(CLEAR_PROJECTION, "CLEAR PROJECTION") \
MR_MACROS(CLEAR_STATISTICS, "CLEAR STATISTICS") \
MR_MACROS(CLAIMS, "CLAIMS") \
MR_MACROS(CLUSTER, "CLUSTER") \
MR_MACROS(CLUSTERS, "CLUSTERS") \
MR_MACROS(CN, "CN") \

View File

@ -17,7 +17,7 @@
#include <Poco/Net/X509Certificate.h>
#endif
const String BEARER_PREFIX = "bearer ";
namespace DB
{
@ -75,6 +75,8 @@ bool authenticateUserByHTTP(
bool has_http_credentials = request.hasCredentials();
bool has_credentials_in_query_params = params.has("user") || params.has("password");
std::string jwt_token = request.get("X-ClickHouse-JWT-Token", request.get("Authorization", (params.has("token") ? BEARER_PREFIX + params.get("token") : "")));
std::string spnego_challenge;
SSLCertificateSubjects certificate_subjects;
@ -137,7 +139,7 @@ bool authenticateUserByHTTP(
if (spnego_challenge.empty())
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Invalid authentication: SPNEGO challenge is empty");
}
else
else if (Poco::icompare(scheme, "Bearer") < 0)
{
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Invalid authentication: '{}' HTTP Authorization scheme is not supported", scheme);
}
@ -189,6 +191,10 @@ bool authenticateUserByHTTP(
return false;
}
}
else if (!jwt_token.empty() && Poco::toLower(jwt_token).starts_with(BEARER_PREFIX))
{
current_credentials = std::make_unique<JWTCredentials>(jwt_token.substr(BEARER_PREFIX.length()));
}
else // I.e., now using user name and password strings ("Basic").
{
if (!current_credentials)

View File

@ -1560,6 +1560,10 @@ void TCPHandler::receiveHello()
if (is_ssh_based_auth)
user.erase(0, std::string_view(EncodedUserInfo::SSH_KEY_AUTHENTICAION_MARKER).size());
is_jwt_based_auth = user.starts_with(EncodedUserInfo::JWT_AUTHENTICAION_MARKER);
if (is_jwt_based_auth)
user.erase(0, std::string_view(EncodedUserInfo::JWT_AUTHENTICAION_MARKER).size());
session = makeSession();
const auto & client_info = session->getClientInfo();
@ -1636,6 +1640,16 @@ void TCPHandler::receiveHello()
}
#endif
if (is_jwt_based_auth)
{
if (client_tcp_protocol_version < DBMS_MIN_REVISION_WITH_JWT_AUTHENTICATION)
throw Exception(ErrorCodes::UNSUPPORTED_METHOD, "Cannot authenticate user with JWT key, because client version is too old");
auto cred = JWTCredentials(password);
session->authenticate(cred, getClientAddress(client_info));
return;
}
session->authenticate(user, password, getClientAddress(client_info));
}

View File

@ -221,6 +221,7 @@ private:
String default_database;
bool is_ssh_based_auth = false; /// authentication is via SSH pub-key challenge
bool is_jwt_based_auth = false; /// authentication is via JWT challenge
/// For inter-server secret (remote_server.*.secret)
bool is_interserver_mode = false;
bool is_interserver_authenticated = false;

View File

@ -158,6 +158,7 @@ function clone_submodules
contrib/libfiu
contrib/incbin
contrib/yaml-cpp
contrib/jwt-cpp
)
git submodule sync

View File

@ -0,0 +1,15 @@
<clickhouse>
<profiles>
<default>
</default>
</profiles>
<users>
<jwt_user>
<jwt>
<claims>{"resource_access": "view-profile"}</claims>
</jwt>
<profile>default</profile>
<quota>default</quota>
</jwt_user>
</users>
</clickhouse>

View File

@ -0,0 +1,12 @@
<clickhouse>
<jwt_verifiers>
<single_key_validator>
<algo>hs256</algo>
<single_key>my_secret</single_key>
<single_key_in_base64>false</single_key_in_base64>
</single_key_validator>
<!-- <static_jwks_validator>-->
<!-- <static_jwks>{"keys": [{"kty": "RSA", "alg": "RS256", "kid": "mykid", "n": "lICGC8S5pObyASih5qfmwuclG0oKsbzY2z9vgwqyhTYQOWcqYcTjVV4aQ30qb6E0-5W6rJ-jx9zx6GuAEGMiG_aWJEdbUAMGp-L1kz4lrw5U6GlwoZIvk4wqoRwsiyc-mnDMQAmiZLBNyt3wU6YnKgYmb4O1cSzcZ5HMbImJpj4tpYjqnIazvYMn_9Pxjkl0ezLCr52av0UkWHro1H4QMVfuEoNmHuWPww9jgHn-I-La0xdOhRpAa0XnJi65dXZd4330uWjeJwt413yz881uS4n1OLOGKG8ImDcNlwU_guyvk0n0aqT0zkOAPp9_yYo13MPWmiRCfOX8ozdN7VDIJw", "e": "AQAB"}]}</static_jwks>-->
<!-- </static_jwks_validator>-->
</jwt_verifiers>
</clickhouse>

View File

@ -0,0 +1,48 @@
import os
import pytest
from helpers.cluster import ClickHouseCluster
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
cluster = ClickHouseCluster(__file__)
instance = cluster.add_instance(
"instance",
main_configs=["configs/verify_static_key.xml"],
user_configs=["configs/users.xml"],
)
client = cluster.add_instance(
"client",
main_configs=["configs/verify_static_key.xml"],
user_configs=["configs/users.xml"],
)
@pytest.fixture(scope="module")
def started_cluster():
try:
cluster.start()
yield cluster
finally:
cluster.shutdown()
def test_static_key(started_cluster):
res = client.exec_in_container(
[
"bash",
"-c",
f'curl -H "X-ClickHouse-JWT-Token: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqd3RfdXNlciIsInJlc291cmNlX2FjY2VzcyI6InZpZXctcHJvZmlsZSJ9.TVnAmEMZeUqG-BD2K4f3Hk6LRvCiTr28W9dbjSGzi0Q" "http://{cluster.get_instance_ip(instance.name)}:8123/?query=SELECT%20currentUser()"',
]
)
assert res == "jwt_user\n"
def test_static_jwks(started_cluster):
res = client.exec_in_container(
[
"bash",
"-c",
f'curl -H "X-ClickHouse-JWT-Token: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Im15a2lkIiwidHlwIjoiSldUIn0.eyJzdWIiOiJqd3RfdXNlciIsInJlc291cmNlX2FjY2VzcyI6InZpZXctcHJvZmlsZSIsImlzcyI6InRlc3RfaXNzIn0.RnSXgwJBQGf9ph-HR0-tgN2ky87vvLtPZzOwSRxGwAQjLBIBSJ9t4JXB_DVK9MpVq7fV7c7_MtohX_fwqT9KhkmrRluyRvPBfImFkxmRJZMdNwQpr3RLm2Lj7NlINFIoAUnkMiNBN7SK0JA0F3ppi4CVZLahDsvWm4ddghoiAQwpqpjQIsDK3O2EWUxWRwhp28mY4auPcbutTD82Kzv1x_Zjygo1yEMqAid_4bfawkeAAERFUel3GCLrAqTNkFbXX_uWkd0bSOvPR9zzyrou0w2O0dborEHPwIMdxtJzfpymLrR_nvzH9xnt8ohhVvNojAekTnOTQrg3xQHUXAJJQA" "http://{cluster.get_instance_ip(instance.name)}:8123/?query=SELECT%20currentUser()"',
]
)
assert res == "jwt_user\n"

View File

@ -246,6 +246,8 @@ DockerHub
DoubleDelta
Doxygen
Durre
ECDSA
EdDSA
ECMA
ETag
Ecto
@ -346,6 +348,7 @@ Heredoc
HexAreaKm
HexAreaM
HexRing
HMSC
Holistics
Homebrew
Homebrew's
@ -455,6 +458,8 @@ Jitter
Joda
JumpConsistentHash
Jupyter
jwks
JWKS
KDevelop
KafkaAssignedPartitions
KafkaBackgroundReads
@ -2939,6 +2944,7 @@ utils
uuid
uuidv
vCPU
validators
varPop
varPopStable
varSamp
@ -2956,6 +2962,8 @@ vectorscan
vendoring
verificationDepth
verificationMode
verifier
verifiers
versionedcollapsingmergetree
vhost
virtualized