diff --git a/ci/jobs/scripts/check_style/aspell-ignore/en/aspell-dict.txt b/ci/jobs/scripts/check_style/aspell-ignore/en/aspell-dict.txt
index 9fd8799053eb..6fcc43b0c674 100644
--- a/ci/jobs/scripts/check_style/aspell-ignore/en/aspell-dict.txt
+++ b/ci/jobs/scripts/check_style/aspell-ignore/en/aspell-dict.txt
@@ -291,6 +291,8 @@ DoubleDelta
Doxygen
Dresseler
Durre
+ECDSA
+EdDSA
ECMA
ETag
EachRow
@@ -539,6 +541,8 @@ JoinAlgorithm
JoinStrictness
JumpConsistentHash
Jupyter
+jwks
+JWKS
KDevelop
KafkaAssignedPartitions
KafkaBackgroundReads
@@ -3379,6 +3383,7 @@ uuid
uuids
uuidv
vCPU
+validators
varPop
varPopStable
varSamp
@@ -3396,6 +3401,8 @@ vectorscan
vendoring
verificationDepth
verificationMode
+verifier
+verifiers
versionedcollapsingmergetree
vhost
virtualized
diff --git a/contrib/jwt-cpp-cmake/CMakeLists.txt b/contrib/jwt-cpp-cmake/CMakeLists.txt
index 4cb8716bc68f..606c13d29de2 100644
--- a/contrib/jwt-cpp-cmake/CMakeLists.txt
+++ b/contrib/jwt-cpp-cmake/CMakeLists.txt
@@ -1,7 +1,4 @@
-set(ENABLE_JWT_CPP_DEFAULT OFF)
-if(ENABLE_LIBRARIES AND CLICKHOUSE_CLOUD)
- set(ENABLE_JWT_CPP_DEFAULT ON)
-endif()
+set(ENABLE_JWT_CPP_DEFAULT ON)
option(ENABLE_JWT_CPP "Enable jwt-cpp library" ${ENABLE_JWT_CPP_DEFAULT})
@@ -20,4 +17,4 @@ set (JWT_CPP_INCLUDE_DIR "${ClickHouse_SOURCE_DIR}/contrib/jwt-cpp/include")
add_library (_jwt-cpp INTERFACE)
target_include_directories(_jwt-cpp SYSTEM BEFORE INTERFACE ${JWT_CPP_INCLUDE_DIR})
-add_library(ch_contrib::jwt-cpp ALIAS _jwt-cpp)
+add_library(ch_contrib::jwt-cpp ALIAS _jwt-cpp)
\ No newline at end of file
diff --git a/docs/en/operations/external-authenticators/index.md b/docs/en/operations/external-authenticators/index.md
index 5a28003ad6c5..1391b7d14ce9 100644
--- a/docs/en/operations/external-authenticators/index.md
+++ b/docs/en/operations/external-authenticators/index.md
@@ -18,4 +18,5 @@ The following external authenticators and directories are supported:
- [LDAP](/operations/external-authenticators/ldap#ldap-external-authenticator) [Authenticator](./ldap.md#ldap-external-authenticator) and [Directory](./ldap.md#ldap-external-user-directory)
- Kerberos [Authenticator](/operations/external-authenticators/kerberos#kerberos-as-an-external-authenticator-for-existing-users)
- [SSL X.509 authentication](/operations/external-authenticators/ssl-x509)
-- HTTP [Authenticator](./http.md)
\ No newline at end of file
+- HTTP [Authenticator](./http.md)
+- Token-based [Authenticator](./tokens.md)
diff --git a/docs/en/operations/external-authenticators/tokens.md b/docs/en/operations/external-authenticators/tokens.md
new file mode 100644
index 000000000000..ed9ebee2bd09
--- /dev/null
+++ b/docs/en/operations/external-authenticators/tokens.md
@@ -0,0 +1,278 @@
+---
+slug: /en/operations/external-authenticators/oauth
+title: "Token-based authentication"
+---
+import SelfManaged from '@site/docs/en/_snippets/_self_managed_only_no_roadmap.md';
+
+
+
+ClickHouse users can be authenticated using tokens. This works in two ways:
+
+- An existing user (defined in `users.xml` or in local access control paths) can be authenticated with a token if this user can be `IDENTIFIED WITH jwt`.
+- Use the information from the token or from an external Identity Provider (IdP) as a source of user definitions and allow locally undefined users to be authenticated with a valid token.
+
+Although not all tokens are JWTs, under the hood both ways are treated as the same authentication method to maintain better compatibility.
+
+# Token Processors
+
+## Configuration
+
+Token-based authentication is enabled by default. To disable it, set `enable_token_auth` to `0` in `config.xml`:
+
+```xml
+0
+```
+
+When disabled, token processors are not parsed, TokenAccessStorage is not available, and authentication via tokens (`--jwt` option or `Authorization: Bearer` header) is rejected.
+
+To use token-based authentication, add `token_processors` section to `config.xml` and define at least one token processor in it.
+Its contents are different for different token processor types.
+
+**Common parameters**
+- `type` -- type of token processor. Supported values: "jwt_static_key", "jwt_static_jwks", "jwt_dynamic_jwks", "azure", "openid". Mandatory. Case-insensitive.
+- `token_cache_lifetime` -- maximum lifetime of cached token (in seconds). Optional, default: 3600.
+- `username_claim` -- name of claim (field) that will be treated as ClickHouse username. Optional, default: "sub".
+- `groups_claim` -- name of claim (field) that contains list of groups user belongs to. This claim will be looked up in the token itself (in case token is a valid JWT, e.g. in Keycloak) or in response from `/userinfo`. Optional, default: "groups".
+
+For each type, there are additional specific parameters (some of them are mandatory).
+If some parameters that are not required for current processor type are specified, they are ignored.
+
+## JWT (JSON Web Token)
+
+JWT itself is a source of information about user.
+It is decoded locally and its integrity is verified using either a local static key or JWKS (JSON Web Key Set), local or remote.
+
+### JWT with static key:
+```xml
+
+
+
+ jwt_static_key
+ HS256
+ my_static_secret
+
+
+
+```
+**Parameters:**
+- `algo` - Algorithm for signature validation. Mandatory. Supported values:
+
+ | HMAC | RSA | ECDSA | PSS | EdDSA |
+ |-------| ----- | ------ | ----- | ------- |
+ | HS256 | RS256 | ES256 | PS256 | Ed25519 |
+ | HS384 | RS384 | ES384 | PS384 | Ed448 |
+ | HS512 | RS512 | ES512 | PS512 | |
+ | | | ES256K | | |
+ Also supports None (not recommended and must *NEVER* be used in production).
+- `claims` - A string containing a JSON object that should be contained in the token payload. If this parameter is defined, token without corresponding payload will be considered invalid. Optional.
+- `static_key` - key for symmetric algorithms. Mandatory for `HS*` family algorithms.
+- `static_key_in_base64` - indicates if the `static_key` key is base64-encoded. Optional, default: `False`.
+- `public_key` - public key for asymmetric algorithms. Mandatory except for `HS*` family algorithms and `None`.
+- `private_key` - private key for asymmetric algorithms. Optional.
+- `public_key_password` - public key password. Optional.
+- `private_key_password` - private key password. Optional.
+- `expected_issuer` - Expected value of the `iss` (issuer) claim in the JWT. If specified, tokens with a different issuer will be rejected. Optional.
+- `expected_audience` - Expected value of the `aud` (audience) claim in the JWT. If specified, tokens with a different audience will be rejected. Optional.
+- `allow_no_expiration` - If `true`, tokens without the `exp` (expiration) claim are accepted. Otherwise they are rejected. Optional, default: `false`.
+
+### JWT with static JWKS
+```xml
+
+
+
+ jwt_static_jwks
+ {"keys": [{"kty": "RSA", "alg": "RS256", "kid": "mykid", "n": "_public_key_mod_", "e": "AQAB"}]}
+
+
+
+```
+
+**Parameters:**
+
+- `static_jwks` - content of JWKS in JSON
+- `static_jwks_file` - path to a file with JWKS
+- `claims` - A string containing a JSON object that should be contained in the token payload. If this parameter is defined, token without corresponding payload will be considered invalid. Optional.
+- `verifier_leeway` - Clock skew tolerance (seconds). Useful for handling small differences in system clocks between ClickHouse and the token issuer. Optional.
+- `expected_issuer` - Expected value of the `iss` (issuer) claim in the JWT. If specified, tokens with a different issuer will be rejected. Optional.
+- `expected_audience` - Expected value of the `aud` (audience) claim in the JWT. If specified, tokens with a different audience will be rejected. Optional.
+- `allow_no_expiration` - If `true`, tokens without the `exp` (expiration) claim are accepted. Otherwise they are rejected. Optional, default: `false`.
+
+:::note
+Only one of `static_jwks` or `static_jwks_file` keys must be present in one verifier
+:::
+
+:::note
+Only RS* family algorithms are supported!
+:::
+
+### JWT with remote JWKS
+```xml
+
+
+
+ jwt_dynamic_jwks
+ http://localhost:8000/.well-known/jwks.json
+ 3600
+
+
+
+```
+
+**Parameters:**
+
+- `uri` - JWKS endpoint. Mandatory.
+- `jwks_cache_lifetime` - Period for resend request for refreshing JWKS. Optional, default: 3600.
+- `claims` - A string containing a JSON object that should be contained in the token payload. If this parameter is defined, token without corresponding payload will be considered invalid. Optional.
+- `verifier_leeway` - Clock skew tolerance (seconds). Useful for handling small differences in system clocks between ClickHouse and the token issuer. Optional.
+- `expected_issuer` - Expected value of the `iss` (issuer) claim in the JWT. If specified, tokens with a different issuer will be rejected. Optional.
+- `expected_audience` - Expected value of the `aud` (audience) claim in the JWT. If specified, tokens with a different audience will be rejected. Optional.
+- `allow_no_expiration` - If `true`, tokens without the `exp` (expiration) claim are accepted. Otherwise they are rejected. Optional, default: `false`.
+
+
+## Processors with external providers
+
+Some tokens cannot be decoded and validated locally. External service is needed in this case. "Azure" and "OpenID" (a generic type) are supported now.
+
+### Azure
+```xml
+
+
+
+ azure
+
+
+
+```
+
+No additional parameters are required.
+
+### OpenID
+```xml
+
+
+
+ openid
+ url/.well-known/openid-configuration
+ 60
+ 3600
+
+
+ openid
+ url/userinfo
+ url/tokeninfo
+ url/.well-known/jwks.json
+ 60
+ 3600
+
+
+
+```
+
+:::note
+Either `configuration_endpoint` or both `userinfo_endpoint` and `token_introspection_endpoint` (and, optionally, `jwks_uri`) shall be set. If none of them are set or all three are set, this is an invalid configuration that will not be parsed.
+:::
+
+**Parameters:**
+
+- `configuration_endpoint` - URI of OpenID configuration (often ends with `.well-known/openid-configuration`);
+- `userinfo_endpoint` - URI of endpoint that returns user information in exchange for a valid token;
+- `token_introspection_endpoint` - URI of token introspection endpoint (returns information about a valid token);
+- `jwks_uri` - URI of OpenID configuration (often ends with `.well-known/jwks.json`)
+- `jwks_cache_lifetime` - Period for resend request for refreshing JWKS. Optional, default: 3600.
+- `verifier_leeway` - Clock skew tolerance (seconds). Useful for handling small differences in system clocks between ClickHouse and the token issuer. Optional, default: 60
+- `expected_issuer` - Expected value of the `iss` (issuer) claim in the JWT. If specified, tokens with a different issuer will be rejected. Optional.
+- `expected_audience` - Expected value of the `aud` (audience) claim in the JWT. If specified, tokens with a different audience will be rejected. Optional.
+- `allow_no_expiration` - If `true`, tokens without the `exp` (expiration) claim are accepted. Otherwise they are rejected. Optional, default: `false`.
+
+Sometimes a token is a valid JWT. In that case token will be decoded and validated locally if configuration endpoint returns JWKS URI (or `jwks_uri` is specified alongside `userinfo_endpoint` and `token_introspection_endpoint`).
+
+### Tokens cache
+To reduce number of requests to IdP, tokens are cached internally for a maximum period of `token_cache_lifetime` seconds.
+If token expires sooner than `token_cache_lifetime`, then cache entry for this token will only be valid while token is valid.
+If token lifetime is longer than `token_cache_lifetime`, cache entry for this token will be valid for `token_cache_lifetime`.
+
+## Enabling token authentication for a user in `users.xml` {#enabling-jwt-auth-in-users-xml}
+
+In order to enable token-based authentication for the user, specify `jwt` section instead of `password` or other 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
+
+
+
+ {"resource_access":{"account": {"roles": ["view-profile"]}}}
+
+
+
+```
+
+Here, the JWT payload must contain `["view-profile"]` on path `resource_access.account.roles`, otherwise authentication will not succeed even with a valid JWT.
+
+:::note
+If `claims` is defined, this user will not be able to authenticate using opaque tokens, so, only JWT-based authentication will be available.
+:::
+
+```
+{
+...
+ "resource_access": {
+ "account": {
+ "roles": ["view-profile"]
+ }
+ },
+...
+}
+```
+
+:::note
+A user cannot have JWT authentication together with any other authentication method. The presence of any other sections like `password` alongside `jwt` will force ClickHouse to shut down.
+:::
+
+## Enabling token authentication using SQL {#enabling-jwt-auth-using-sql}
+
+Users with "JWT" authentication type cannot be created using SQL now.
+
+## Identity Provider as an External User Directory {#idp-external-user-directory}
+
+If there is no suitable user pre-defined in ClickHouse, authentication is still possible: Identity Provider can be used as source of user information.
+To allow this, add `token` section to the `users_directories` section of the `config.xml` file.
+
+At each login attempt, ClickHouse tries to find the user definition locally and authenticate it as usual.
+If a token is provided but the user is not defined, ClickHouse will treat the user as externally defined and will try to validate the token and get user information from the specified processor.
+If validated successfully, the user will be considered existing and authenticated. The user will be assigned roles from the list specified in the `roles` section.
+All this implies that the SQL-driven [Access Control and Account Management](/docs/en/guides/sre/user-management/index.md#access-control) is enabled and roles are created using the [CREATE ROLE](/docs/en/sql-reference/statements/create/role.md#create-role-statement) statement.
+
+**Example**
+
+```xml
+
+
+
+ token_processor_name
+
+
+
+ my_profile
+
+ \bclickhouse-[a-zA-Z0-9]+\b
+
+ s/-/_/g
+
+
+
+```
+
+:::note
+For now, no more than one `token` section can be defined inside `user_directories`. This _may_ change in future.
+:::
+
+**Parameters**
+
+- `processor` — Name of one of processors defined in `token_processors` config section described above. This parameter is mandatory and cannot be empty.
+- `common_roles` — Section with a list of locally defined roles that will be assigned to each user retrieved from the IdP. Optional.
+- `default_profile` — Name of a locally defined settings profile that will be assigned to each user retrieved from the IdP. If the profile does not exist, a warning will be logged and the user will be created without a profile. Optional.
+- `roles_filter` — Regex string for groups filtering. Only groups matching this regex will be mapped to roles. Optional.
+- `roles_transform` — Sed-style transform pattern to apply to group names before mapping to roles. Format: `s/pattern/replacement/flags`. The `g` flag applies the replacement globally (all occurrences). Example: `s/-/_/g` converts `clickhouse-grp-dba` to `clickhouse_grp_dba`. Optional.
diff --git a/src/Access/AccessControl.cpp b/src/Access/AccessControl.cpp
index 70818a25af16..ac1b0f5499e5 100644
--- a/src/Access/AccessControl.cpp
+++ b/src/Access/AccessControl.cpp
@@ -5,6 +5,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -41,6 +42,7 @@ namespace ErrorCodes
extern const int REQUIRED_PASSWORD;
extern const int CANNOT_COMPILE_REGEXP;
extern const int BAD_ARGUMENTS;
+ extern const int INVALID_CONFIG_PARAMETER;
}
namespace
@@ -295,6 +297,8 @@ void AccessControl::setupFromMainConfig(const Poco::Util::AbstractConfiguration
setDefaultPasswordTypeFromConfig(config_.getString("default_password_type", "sha256_password"));
setPasswordComplexityRulesFromConfig(config_);
+ setTokenAuthEnabled(config_.getBool("enable_token_auth", true));
+
setBcryptWorkfactor(config_.getInt("bcrypt_workfactor", 12));
/// Optional improvements in access control system.
@@ -422,6 +426,12 @@ void AccessControl::addLDAPStorage(const String & storage_name_, const Poco::Uti
LOG_DEBUG(getLogger(), "Added {} access storage '{}', LDAP server name: {}", String(new_storage->getStorageType()), new_storage->getStorageName(), new_storage->getLDAPServerName());
}
+void AccessControl::addTokenStorage(const String & storage_name_, const Poco::Util::AbstractConfiguration & config_, const String & prefix_)
+{
+ auto new_storage = std::make_shared(storage_name_, *this, config_, prefix_);
+ addStorage(new_storage);
+ LOG_DEBUG(getLogger(), "Added {} access storage '{}'", String(new_storage->getStorageType()), new_storage->getStorageName());
+}
void AccessControl::addStoragesFromUserDirectoriesConfig(
const Poco::Util::AbstractConfiguration & config,
@@ -434,6 +444,8 @@ void AccessControl::addStoragesFromUserDirectoriesConfig(
Strings keys_in_user_directories;
config.keys(key, keys_in_user_directories);
+ bool has_token_storage = false;
+
for (const String & key_in_user_directories : keys_in_user_directories)
{
String prefix = key + "." + key_in_user_directories;
@@ -447,6 +459,8 @@ void AccessControl::addStoragesFromUserDirectoriesConfig(
type = DiskAccessStorage::STORAGE_TYPE;
else if (type == "ldap")
type = LDAPAccessStorage::STORAGE_TYPE;
+ else if (type == "token")
+ type = TokenAccessStorage::STORAGE_TYPE;
String name = config.getString(prefix + ".name", type);
@@ -480,6 +494,20 @@ void AccessControl::addStoragesFromUserDirectoriesConfig(
bool allow_backup = config.getBool(prefix + ".allow_backup", true);
addReplicatedStorage(name, zookeeper_path, get_zookeeper_function, allow_backup);
}
+ else if (type == TokenAccessStorage::STORAGE_TYPE)
+ {
+ if (!isTokenAuthEnabled())
+ {
+ LOG_INFO(getLogger(), "Token authentication is disabled, skipping token user directory '{}'", name);
+ continue;
+ }
+
+ if (has_token_storage)
+ throw Exception(ErrorCodes::INVALID_CONFIG_PARAMETER, "Only one `token` section can be defined.");
+
+ addTokenStorage(name, config, prefix);
+ has_token_storage = true;
+ }
else
throw Exception(ErrorCodes::UNKNOWN_ELEMENT_IN_CONFIG, "Unknown storage type '{}' at {} in config", type, prefix);
}
@@ -657,7 +685,7 @@ void AccessControl::restoreFromBackup(RestorerFromBackup & restorer, const Strin
void AccessControl::setExternalAuthenticatorsConfig(const Poco::Util::AbstractConfiguration & config)
{
- external_authenticators->setConfiguration(config, getLogger());
+ external_authenticators->setConfiguration(config, getLogger(), isTokenAuthEnabled());
}
@@ -936,4 +964,14 @@ bool AccessControl::getAllowBetaTierSettings() const
{
return allow_beta_tier_settings;
}
+
+void AccessControl::setTokenAuthEnabled(bool enable)
+{
+ enable_token_auth = enable;
+}
+
+bool AccessControl::isTokenAuthEnabled() const
+{
+ return enable_token_auth;
+}
}
diff --git a/src/Access/AccessControl.h b/src/Access/AccessControl.h
index c1e32fc7c467..a246795d005f 100644
--- a/src/Access/AccessControl.h
+++ b/src/Access/AccessControl.h
@@ -93,6 +93,8 @@ class AccessControl : public MultipleAccessStorage
/// Adds LDAPAccessStorage which allows querying remote LDAP server for user info.
void addLDAPStorage(const String & storage_name_, const Poco::Util::AbstractConfiguration & config_, const String & prefix_);
+ void addTokenStorage(const String & storage_name_, const Poco::Util::AbstractConfiguration & config_, const String & prefix_);
+
void addReplicatedStorage(const String & storage_name,
const String & zookeeper_path,
const zkutil::GetZooKeeper & get_zookeeper_function,
@@ -259,6 +261,10 @@ class AccessControl : public MultipleAccessStorage
bool getAllowExperimentalTierSettings() const;
bool getAllowBetaTierSettings() const;
+ /// Controls whether token-based auth is enabled.
+ void setTokenAuthEnabled(bool enable);
+ bool isTokenAuthEnabled() const;
+
private:
class ContextAccessCache;
class CustomSettingsPrefixes;
@@ -292,6 +298,7 @@ class AccessControl : public MultipleAccessStorage
std::atomic_bool allow_beta_tier_settings = true;
std::atomic_bool enable_user_name_access_type = true;
std::atomic_bool enable_read_write_grants = false;
+ std::atomic_bool enable_token_auth = true;
};
}
diff --git a/src/Access/Authentication.cpp b/src/Access/Authentication.cpp
index 03e47ed3b6d6..66cad391932c 100644
--- a/src/Access/Authentication.cpp
+++ b/src/Access/Authentication.cpp
@@ -298,7 +298,10 @@ bool Authentication::areCredentialsValid(
const ClientInfo & client_info,
SettingsChanges & settings)
{
- if (!credentials.isReady())
+ /// It is OK for TokenCredentials to be not ready:
+ /// When auth request happens, we do not even know the username.
+ /// Token is resolved a bit later and the user information will be put in credentials
+ if (!typeid_cast(&credentials) && !credentials.isReady())
return false;
if (const auto * gss_acceptor_context = typeid_cast(&credentials))
@@ -340,6 +343,14 @@ bool Authentication::areCredentialsValid(
}
#endif
+ if (const auto * token_credentials = typeid_cast(&credentials))
+ {
+ if (authentication_method.getType() != AuthenticationType::JWT)
+ return false;
+
+ return external_authenticators.checkTokenCredentials(*token_credentials);
+ }
+
if ([[maybe_unused]] const auto * always_allow_credentials = typeid_cast(&credentials))
return true;
diff --git a/src/Access/AuthenticationData.cpp b/src/Access/AuthenticationData.cpp
index c6760c4a0928..3e8e2e42a8d0 100644
--- a/src/Access/AuthenticationData.cpp
+++ b/src/Access/AuthenticationData.cpp
@@ -27,6 +27,10 @@
# include
#endif
+#if USE_JWT_CPP
+#include
+#endif
+
namespace DB
{
@@ -375,7 +379,10 @@ std::shared_ptr 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(claims));
+ break;
}
case AuthenticationType::KERBEROS:
{
@@ -653,6 +660,22 @@ AuthenticationData AuthenticationData::fromAST(const ASTAuthenticationData & que
auth_data.setHTTPAuthenticationServerName(server);
auth_data.setHTTPAuthenticationScheme(scheme);
}
+#if USE_JWT_CPP
+ else if (query.type == AuthenticationType::JWT)
+ {
+ if (!args.empty())
+ {
+ String value = checkAndGetLiteralArgument(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())
+ throw Exception(ErrorCodes::BAD_ARGUMENTS, "Bad JWT claims: is not an object");
+ auth_data.setJWTClaims(value);
+ }
+ }
+#endif
else
{
throw Exception(ErrorCodes::LOGICAL_ERROR, "Unexpected ASTAuthenticationData structure");
diff --git a/src/Access/AuthenticationData.h b/src/Access/AuthenticationData.h
index 187a17dca948..bcd7bd61270f 100644
--- a/src/Access/AuthenticationData.h
+++ b/src/Access/AuthenticationData.h
@@ -79,6 +79,12 @@ class AuthenticationData
time_t getValidUntil() const { return valid_until; }
void setValidUntil(time_t valid_until_) { valid_until = valid_until_; }
+ const String & getJWTClaims() const { return jwt_claims; }
+ void setJWTClaims(const String & jwt_claims_) { jwt_claims = jwt_claims_; }
+
+ const String & getTokenProcessorName() const { return token_processor_name; }
+ void setTokenProcessorName(const String & token_processor_name_) { token_processor_name = token_processor_name_; }
+
friend bool operator ==(const AuthenticationData & lhs, const AuthenticationData & rhs);
friend bool operator !=(const AuthenticationData & lhs, const AuthenticationData & rhs) { return !(lhs == rhs); }
@@ -117,6 +123,8 @@ class AuthenticationData
String http_auth_server_name;
HTTPAuthenticationScheme http_auth_scheme = HTTPAuthenticationScheme::BASIC;
time_t valid_until = 0;
+ String jwt_claims;
+ String token_processor_name;
};
}
diff --git a/src/Access/Common/JWKSProvider.cpp b/src/Access/Common/JWKSProvider.cpp
new file mode 100644
index 000000000000..40814ea5eb86
--- /dev/null
+++ b/src/Access/Common/JWKSProvider.cpp
@@ -0,0 +1,106 @@
+#include
+
+#if USE_JWT_CPP
+#include
+#include
+#include
+#include
+#include
+#include
+
+
+namespace DB
+{
+
+namespace ErrorCodes
+{
+ extern const int AUTHENTICATION_FAILED;
+ extern const int INVALID_CONFIG_PARAMETER;
+}
+
+JWKSType JWKSClient::getJWKS()
+{
+ std::shared_lock lock(mutex);
+
+ auto now = std::chrono::high_resolution_clock::now();
+ auto diff = std::chrono::duration(now - last_request_send).count();
+
+ if (diff < refresh_timeout && cached_jwks.has_value())
+ return cached_jwks.value();
+
+ Poco::Net::HTTPResponse response;
+ std::string response_string;
+
+ Poco::Net::HTTPRequest request{Poco::Net::HTTPRequest::HTTP_GET, jwks_uri.getPathAndQuery()};
+
+ if (jwks_uri.getScheme() == "https")
+ {
+ Poco::Net::HTTPSClientSession session = Poco::Net::HTTPSClientSession(jwks_uri.getHost(), jwks_uri.getPort());
+ session.sendRequest(request);
+ std::istream & response_stream = session.receiveResponse(response);
+ if (response.getStatus() != Poco::Net::HTTPResponse::HTTP_OK || !response_stream)
+ throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to get user info by access token, code: {}, reason: {}",
+ response.getStatus(), response.getReason());
+ Poco::StreamCopier::copyToString(response_stream, response_string);
+ }
+ else
+ {
+ Poco::Net::HTTPClientSession session = Poco::Net::HTTPClientSession(jwks_uri.getHost(), jwks_uri.getPort());
+ session.sendRequest(request);
+ std::istream & response_stream = session.receiveResponse(response);
+ if (response.getStatus() != Poco::Net::HTTPResponse::HTTP_OK || !response_stream)
+ throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to get user info by access token, code: {}, reason: {}", response.getStatus(), response.getReason());
+ Poco::StreamCopier::copyToString(response_stream, response_string);
+ }
+
+ last_request_send = std::chrono::high_resolution_clock::now();
+
+ JWKSType parsed_jwks;
+
+ try
+ {
+ parsed_jwks = jwt::parse_jwks(response_string);
+ }
+ catch (const std::exception & e)
+ {
+ throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to parse JWKS: {}", e.what());
+ }
+
+ cached_jwks = std::move(parsed_jwks);
+ return cached_jwks.value();
+}
+
+StaticJWKSParams::StaticJWKSParams(const std::string & static_jwks_, const std::string & static_jwks_file_)
+{
+ if (static_jwks_.empty() && static_jwks_file_.empty())
+ throw Exception(ErrorCodes::INVALID_CONFIG_PARAMETER,
+ "JWT validator misconfigured: `static_jwks` or `static_jwks_file` keys must be present in static JWKS validator configuration");
+ if (!static_jwks_.empty() && !static_jwks_file_.empty())
+ throw Exception(ErrorCodes::INVALID_CONFIG_PARAMETER,
+ "JWT validator misconfigured: `static_jwks` and `static_jwks_file` keys cannot both be present in static JWKS validator configuration");
+
+ static_jwks = static_jwks_;
+ static_jwks_file = static_jwks_file_;
+}
+
+StaticJWKS::StaticJWKS(const StaticJWKSParams & params)
+{
+ String content = String(params.static_jwks);
+ if (!params.static_jwks_file.empty())
+ {
+ std::ifstream ifs(params.static_jwks_file);
+ Poco::StreamCopier::copyToString(ifs, content);
+ }
+ try
+ {
+ auto keys = jwt::parse_jwks(content);
+ jwks = std::move(keys);
+ }
+ catch (const std::exception & e)
+ {
+ throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to parse JWKS: {}", e.what());
+ }
+}
+
+}
+#endif
diff --git a/src/Access/Common/JWKSProvider.h b/src/Access/Common/JWKSProvider.h
new file mode 100644
index 000000000000..566effd6e21e
--- /dev/null
+++ b/src/Access/Common/JWKSProvider.h
@@ -0,0 +1,73 @@
+#include
+
+#if USE_JWT_CPP
+#include
+#include
+#include
+#include
+
+#include
+
+namespace DB
+{
+
+using JWKSType = jwt::jwks;
+
+/// JWKS (JSON Web Key Set) is a kind of a set of public keys that are used to validate JWT authenticity locally.
+/// They are usually exposed by identity providers (e.g. Keycloak) via a well-known URI (usually /.well-known/jwks.json)
+/// This interface is responsible for managing JWKS. Retrieving, caching and refreshing of JWKS happens here.
+/// JWKS can either be static (e.g. provided in config) or dynamic (fetched from a remote URI and).
+class IJWKSProvider
+{
+public:
+ virtual ~IJWKSProvider() = default;
+
+ virtual JWKSType getJWKS() = 0;
+};
+
+class JWKSClient : public IJWKSProvider
+{
+public:
+ explicit JWKSClient(const String & uri, const size_t refresh_ms_): refresh_timeout(refresh_ms_), jwks_uri(uri) {}
+
+ ~JWKSClient() override = default;
+ JWKSClient(const JWKSClient &) = delete;
+ JWKSClient(JWKSClient &&) = delete;
+ JWKSClient &operator=(const JWKSClient &) = delete;
+ JWKSClient &operator=(JWKSClient &&) = delete;
+
+ JWKSType getJWKS() override;
+
+private:
+ size_t refresh_timeout;
+ Poco::URI jwks_uri;
+
+ std::shared_mutex mutex;
+ std::optional cached_jwks;
+ std::chrono::time_point last_request_send;
+};
+
+struct StaticJWKSParams
+{
+ StaticJWKSParams(const std::string &static_jwks_, const std::string &static_jwks_file_);
+
+ String static_jwks;
+ String static_jwks_file;
+};
+
+class StaticJWKS : public IJWKSProvider
+{
+public:
+ explicit StaticJWKS(const StaticJWKSParams ¶ms);
+
+private:
+ JWKSType getJWKS() override
+ {
+ return jwks;
+ }
+
+ JWKSType jwks;
+};
+
+}
+#endif
diff --git a/src/Access/Credentials.cpp b/src/Access/Credentials.cpp
index 4887d0545656..c60fb3cfea67 100644
--- a/src/Access/Credentials.cpp
+++ b/src/Access/Credentials.cpp
@@ -2,6 +2,7 @@
#include
#include
#include
+#include
namespace DB
{
@@ -9,6 +10,7 @@ namespace DB
namespace ErrorCodes
{
extern const int LOGICAL_ERROR;
+ extern const int AUTHENTICATION_FAILED;
}
Credentials::Credentials(const String & user_name_)
@@ -100,4 +102,7 @@ const String & BasicCredentials::getPassword() const
return password;
}
+/// Unless the token is validated, we will not use any data from it, including username.
+TokenCredentials::TokenCredentials(const String & token_) : Credentials(""), token(token_), expires_at(std::chrono::system_clock::now() + std::chrono::hours(1)) {}
+
}
diff --git a/src/Access/Credentials.h b/src/Access/Credentials.h
index f98eb31ff0a2..bb81ef93a15c 100644
--- a/src/Access/Credentials.h
+++ b/src/Access/Credentials.h
@@ -16,6 +16,8 @@ namespace Poco::Net
class SocketAddress;
}
+#include
+
namespace DB
{
@@ -195,4 +197,47 @@ class SSHPTYCredentials : public Credentials
#endif
+class TokenCredentials : public Credentials
+{
+public:
+ explicit TokenCredentials(const String & token_);
+
+ const String & getToken() const
+ {
+ if (token.empty())
+ {
+ throwNotReady();
+ }
+ return token;
+ }
+ void setUserName(const String & user_name_)
+ {
+ user_name = user_name_;
+ if (!user_name.empty())
+ {
+ is_ready = true;
+ }
+ }
+ std::set getGroups() const
+ {
+ return groups;
+ }
+ void setGroups(const std::set & groups_)
+ {
+ groups = groups_;
+ }
+ std::optional getExpiresAt() const
+ {
+ return expires_at;
+ }
+ void setExpiresAt(std::chrono::system_clock::time_point expires_at_)
+ {
+ expires_at = expires_at_;
+ }
+private:
+ String token;
+ std::set groups;
+ std::optional expires_at;
+};
+
}
diff --git a/src/Access/ExternalAuthenticators.cpp b/src/Access/ExternalAuthenticators.cpp
index ca61b55dc5dc..99e99c20f738 100644
--- a/src/Access/ExternalAuthenticators.cpp
+++ b/src/Access/ExternalAuthenticators.cpp
@@ -1,7 +1,10 @@
+#include
#include
#include
#include
#include
+#include "Common/Logger.h"
+#include "Common/logger_useful.h"
#include
#include
#include
@@ -12,6 +15,8 @@
#include
#include
+#include