diff --git a/.gitmodules b/.gitmodules
index a618104f3642..d3270d85155c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -369,3 +369,6 @@
[submodule "contrib/idna"]
path = contrib/idna
url = https://github.com/ada-url/idna.git
+[submodule "contrib/jwt-cpp"]
+ path = contrib/jwt-cpp
+ url = https://github.com/Thalhammer/jwt-cpp.git
diff --git a/contrib/CMakeLists.txt b/contrib/CMakeLists.txt
index c6d1dcb41e61..bc3cdc1a87f3 100644
--- a/contrib/CMakeLists.txt
+++ b/contrib/CMakeLists.txt
@@ -85,7 +85,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)
diff --git a/contrib/jwt-cpp b/contrib/jwt-cpp
new file mode 160000
index 000000000000..a6927cb81408
--- /dev/null
+++ b/contrib/jwt-cpp
@@ -0,0 +1 @@
+Subproject commit a6927cb8140858c34e05d1a954626b9849fbcdfc
diff --git a/contrib/jwt-cpp-cmake/CMakeLists.txt b/contrib/jwt-cpp-cmake/CMakeLists.txt
new file mode 100644
index 000000000000..2ee0281348f2
--- /dev/null
+++ b/contrib/jwt-cpp-cmake/CMakeLists.txt
@@ -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)
diff --git a/docker/test/fasttest/run.sh b/docker/test/fasttest/run.sh
index b7c98730253c..bf52b2922eb1 100755
--- a/docker/test/fasttest/run.sh
+++ b/docker/test/fasttest/run.sh
@@ -155,6 +155,7 @@ function clone_submodules
contrib/libfiu
contrib/incbin
contrib/yaml-cpp
+ contrib/jwt-cpp
)
git submodule sync
diff --git a/docs/en/interfaces/cli.md b/docs/en/interfaces/cli.md
index 1eb426af617e..e18ff6f1a3fc 100644
--- a/docs/en/interfaces/cli.md
+++ b/docs/en/interfaces/cli.md
@@ -193,6 +193,7 @@ You can pass parameters to `clickhouse-client` (all parameters have a default va
- `--hardware-utilization` — Print hardware utilization information in progress bar.
- `--print-profile-events` – Print `ProfileEvents` packets.
- `--profile-events-delay-ms` – Delay between printing `ProfileEvents` packets (-1 - print only totals, 0 - print every single packet).
+- `--jwt` – If specified, enables authorization via JSON Web Token. Server JWT authorization is available only in ClickHouse Cloud.
Instead of `--host`, `--port`, `--user` and `--password` options, ClickHouse client also supports connection strings (see next section).
diff --git a/docs/en/operations/external-authenticators/index.md b/docs/en/operations/external-authenticators/index.md
index f644613641cc..2730389e1177 100644
--- a/docs/en/operations/external-authenticators/index.md
+++ b/docs/en/operations/external-authenticators/index.md
@@ -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)
\ No newline at end of file
+- HTTP [Authenticator](./http.md)
+- JWT [Authenticator](./jwt.md)
diff --git a/docs/en/operations/external-authenticators/jwt.md b/docs/en/operations/external-authenticators/jwt.md
new file mode 100644
index 000000000000..0ce4493d16d1
--- /dev/null
+++ b/docs/en/operations/external-authenticators/jwt.md
@@ -0,0 +1,204 @@
+---
+slug: /en/operations/external-authenticators/jwt
+---
+# JWT
+import SelfManaged from '@site/docs/en/_snippets/_self_managed_only_no_roadmap.md';
+
+
+
+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.
+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
+
+It is mandatory for a JWT to indicate the name of the ClickHouse user under `"sub"` claim, otherwise it will not be accepted.
+
+A JWT may additionally be verified by checking the JWT payload.
+In this case, the occurrence of specified claims from the user settings in the JWT payload is checked.
+See [Enabling JWT authentication in `users.xml`](#enabling-jwt-auth-in-users-xml)
+
+To use JWT authentication, JWT validators must be configured in ClickHouse config.
+
+
+## Enabling JWT validators in ClickHouse {#enabling-jwt-validators-in-clickhouse}
+
+To enable JWT validators, add `jwt_validators` 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
+
+
+
+
+ HS256
+ my_static_secret
+
+
+
+```
+
+#### Parameters:
+
+- `algo` - Algorithm for validate signature. Supported:
+
+ | HMAC | RSA | ECDSA | PSS | EdDSA |
+ |-------| ----- | ------ | ----- | ------- |
+ | HS256 | RS256 | ES256 | PS256 | Ed25519 |
+ | HS384 | RS384 | ES384 | PS384 | Ed448 |
+ | HS512 | RS512 | ES512 | PS512 | |
+ | | | ES256K | | |
+ Also support None.
+- `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.
+
+### Verifying JWT signature using static JWKS {$verifying-jwt-signature-using-static-jwks}
+
+:::note
+Only RS* family algorithms are supported!
+:::
+
+**Example**
+```xml
+
+
+
+
+ {"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 file with JWKS
+
+:::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
+
+
+
+
+ http://localhost:8000/.well-known/jwks.json
+ 1000
+ 1000
+ 1000
+ 3
+ 50
+ 1000
+ 300000
+
+
+
+```
+
+#### Parameters:
+
+- `uri` - JWKS endpoint. Mandatory.
+- `refresh_ms` - Period for resend request for refreshing JWKS. Optional, default: 300000.
+
+Timeouts in milliseconds on the socket used for communicating with the server (optional):
+- `connection_timeout_ms` - Default: 1000.
+- `receive_timeout_ms` - Default: 1000.
+- `send_timeout_ms` - Default: 1000.
+
+Retry parameters (optional):
+- `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.
+- `retry_max_backoff_ms` - The maximum backoff interval. Default: 1000.
+
+### Enabling JWT authentication in `users.xml` {#enabling-jwt-auth-in-users-xml}
+
+In order to enable JWT 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.
+
+```
+{
+...
+ "resource_access": {
+ "account": {
+ "roles": ["view-profile"]
+ }
+ },
+...
+}
+```
+
+:::note
+JWT authentication cannot be used together with any other authentication method. The presence of any other sections like `password` alongside `jwt` will force ClickHouse to shut down.
+:::
+
+### 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 authentication examples {#jwt-authentication-examples}
+
+#### Console client
+
+```
+clickhouse-client -jwt
+```
+
+#### HTTP requests
+
+```
+curl 'http://localhost:8080/?' \
+ -H 'Authorization: Bearer ' \
+ -H 'Content type: text/plain;charset=UTF-8' \
+ --data-raw 'SELECT current_user()'
+```
+:::note
+ClickHouse will look for a JWT token in (by priority):
+1. `X-ClickHouse-JWT-Token` header.
+2. `Authorization` header.
+3. `token` request parameter. In this case, the "Bearer" prefix should not exist.
+:::
+
+### Passing session settings {#passing-session-settings}
+
+If `settings_key` exists in the `jwt_validators` 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_validators` section. If `settings_key` in the verifier section does not exist, the `settings_key` from the `jwt_validators` section will be used.
diff --git a/docs/ru/interfaces/cli.md b/docs/ru/interfaces/cli.md
index 4d19cf50ae12..86eeaac2da74 100644
--- a/docs/ru/interfaces/cli.md
+++ b/docs/ru/interfaces/cli.md
@@ -141,6 +141,7 @@ $ clickhouse-client --param_tbl="numbers" --param_db="system" --param_col="numbe
- `--secure` — если указано, будет использован безопасный канал.
- `--history_file` - путь к файлу с историей команд.
- `--param_` — значение параметра для [запроса с параметрами](#cli-queries-with-parameters).
+- `--jwt` – авторизация с использованием JSON Web Token. Доступно только в ClickHouse Cloud.
Вместо параметров `--host`, `--port`, `--user` и `--password` клиент ClickHouse также поддерживает строки подключения (смотри следующий раздел).
diff --git a/docs/ru/operations/external-authenticators/jwt.md b/docs/ru/operations/external-authenticators/jwt.md
new file mode 100644
index 000000000000..66372c6a9a31
--- /dev/null
+++ b/docs/ru/operations/external-authenticators/jwt.md
@@ -0,0 +1,206 @@
+---
+slug: /ru/operations/external-authenticators/jwt
+---
+# JWT
+import SelfManaged from '@site/docs/en/_snippets/_self_managed_only_no_roadmap.md';
+
+
+
+Существующие и корректно настроенные пользователи ClickHouse могут быть аутентифицированы с помощью JWT.
+
+Сейчас JWT работает только как внешний аутентификатор для уже существующих пользователей.
+Имя пользователя будет извлечено из JWT после проверки срока действия токена и подписи.
+Подпись может быть проверена с помощью:
+- статического (указанного в конфигурации) открытого ключа,
+- статического (указанного в конфигурации) JWKS или файла, содержащего JWKS,
+- полученного от JWKS-сервера.
+
+Имя пользователя ClickHouse должно быть обязательно указано в поле (claim) `"sub"`, в противном случае токен не будет принят.
+
+Можно также дополнительно проверять JWT на наличие определённого содержимого (payload).
+В этом случае проверяется наличие указанных полей (claims) из настроек пользователя в содержимом JWT.
+Смотри [Настройка JWT аутентификации пользователя через `users.xml`](#enabling-jwt-auth-in-users-xml) и [Настройка JWT аутентификации пользователя через SQL](#enabling-jwt-auth-using-sql)
+
+
+## Настройка JWT валидаторов {#enabling-jwt-validators}
+
+Для аутентификации с помощью JWT сконфигурировать как минимум один валидатор.
+Это делается в секции `jwt_validators` в `config.xml`. Эта секция может содержать несколько JWT-верификаторов.
+
+### Проверка JWT с помощью статического ключа {$verifying-jwt-signature-using-static-key}
+
+**Пример**
+```xml
+
+
+
+
+ HS256
+ my_static_secret
+
+
+
+```
+
+#### Параметры:
+
+- `algo` - Алгоритм для проверки подписи. Поддерживаемые алгоритмы:
+
+ | HMAC | RSA | ECDSA | PSS | EdDSA |
+ |-------| ----- | ------ | ----- | ------- |
+ | HS256 | RS256 | ES256 | PS256 | Ed25519 |
+ | HS384 | RS384 | ES384 | PS384 | Ed448 |
+ | HS512 | RS512 | ES512 | PS512 | |
+ | | | ES256K | | |
+ Можно не проверять подпись, указав `None` для этого параметра.
+- `static_key` - ключ симметричного алгоритма. Обязателен для алгоритмов семейства `HS*`.
+- `static_key_in_base64` - указывает, закодирован ли `static_key` в формате base64. Необязательный параметр, по умолчанию: `False`.
+- `public_key` - открытый ключ для асимметричных алгоритмов. Обязателен для всех алгоритмов, кроме семейства `HS*` и `None`.
+- `private_key` - закрытый ключ для асимметричных алгоритмов. Необязательный параметр.
+- `public_key_password` - пароль открытого ключа, необязательный параметр.
+- `private_key_password` - пароль закрытого ключа, необязательный параметр.
+
+### Проверка JWT с помощью статического JWKS {$verifying-jwt-signature-using-static-jwks}
+
+:::note
+Проверка с помощью JWKS невозможна для алгоритмов семейства `HS*`.
+:::
+
+**Пример**
+```xml
+
+
+
+
+ {"keys": [{"kty": "RSA", "alg": "RS256", "kid": "mykid", "n": "_public_key_mod_", "e": "AQAB"}]}
+
+
+
+```
+
+#### Параметры:
+- `static_jwks` - содержимое JWKS в виде JSON.
+- `static_jwks_file` - путь к файлу, содержащему JWKS.
+
+:::note
+Должен быть указан один и только один из этих двух параметров.
+:::
+
+### Проверка JWT с помощью JWKS сервера {$verifying-jwt-signature-using-static-jwks}
+
+:::note
+Проверка с помощью JWKS невозможна для алгоритмов семейства `HS*`.
+:::
+
+**Пример**
+```xml
+
+
+
+
+ http://localhost:8000/.well-known/jwks.json
+ 1000
+ 1000
+ 1000
+ 3
+ 50
+ 1000
+ 300000
+
+
+
+```
+
+#### Параметры:
+
+- `uri` - адрес, по которому доступен JWKS. Обязательный параметр.
+- `refresh_ms` - Период обновления JWKS. Необязательный параметр, по умолчанию: 300000.
+
+Таймауты в миллисекундах для сокета, используемого для связи с сервером (необязательные параметры):
+- `connection_timeout_ms` - По умолчанию: 1000.
+- `receive_timeout_ms` - По умолчанию: 1000.
+- `send_timeout_ms` - По умолчанию: 1000.
+
+Настройка повторных попыток (необязательные параметры):
+- `max_tries` - Максимальное количество попыток аутентификации. По умолчанию: 3.
+- `retry_initial_backoff_ms` - Стартовый интервал между повторными попытками (backoff). По умолчанию: 50.
+- `retry_max_backoff_ms` - Максимальный интервал между повторными попытками (backoff). По умолчанию: 1000.
+
+### Настройка JWT аутентификации пользователя в `users.xml` {#enabling-jwt-auth-in-users-xml}
+
+Чтобы включить аутентификацию с помощью JWT для пользователя, укажите секцию `jwt` вместо секции `password` и аналогичных секций.
+
+**Пример (`users.xml`)**
+```xml
+
+
+
+
+
+ {"resource_access":{"account": {"roles": ["view-profile"]}}}
+
+
+
+```
+
+#### Параметры
+- `claims` - строка, содержащая JSON, который должен присутствовать в содержимом токена.
+
+В данном случае содержимое JWT должно содержать значение ["view-profile"] по пути `resource_access.account.roles`,
+в противном случае аутентификация не будет успешной, даже если в остальном JWT верный.
+
+**Пример payload**
+```json
+{
+ "sub": "my_user",
+ "resource_access": {
+ "account": {
+ "roles": ["view-profile"]
+ }
+ },
+}
+```
+
+:::note
+Аутентификация JWT не может использоваться вместе с другими методами аутентификации. Наличие любых других секций, таких как `password`, наряду с секцией `jwt` приведет к аварийному завершению работы.
+:::
+
+### Настройка JWT аутентификации пользователя через SQL {#enabling-jwt-auth-using-sql}
+
+В случае если в ClickHouse включено управление доступом через SQL ([SQL-driven Access Control and Account Management](/docs/en/guides/sre/user-management/index.md#access-control)),
+можно создать пользователя с аутентификацией через JWT с помощью SQL-запросов.
+
+**Без проверки содержимого JWT**
+```sql
+CREATE USER my_user IDENTIFIED WITH jwt
+```
+
+**С проверкой содержимого JWT**
+```sql
+CREATE USER my_user IDENTIFIED WITH jwt CLAIMS '{"resource_access":{"account": {"roles": ["view-profile"]}}}'
+```
+
+## Примеры аутентификации {#jwt-authentication-examples}
+
+#### `clickhouse-client`
+
+```
+clickhouse-client -jwt
+```
+
+#### HTTP
+
+```
+curl 'http://localhost:8080/?' \
+ -H 'Authorization: Bearer ' \
+ -H 'Content type: text/plain;charset=UTF-8' \
+ --data-raw 'SELECT current_user()'
+```
+:::note
+ClickHouse ищет токен в следующих местах (по порядку):
+1. Заголовок `X-ClickHouse-JWT-Token`.
+2. Стандартный заголовок `Authorization`.
+3. Параметр `token`. В этом случае параметр не должен содержать префикс `Bearer`.
+:::
+
+### Передача параметров сессии {#passing-session-settings}
diff --git a/programs/client/Client.cpp b/programs/client/Client.cpp
index d4bf2f686c88..7537ade653e0 100644
--- a/programs/client/Client.cpp
+++ b/programs/client/Client.cpp
@@ -73,6 +73,7 @@ void Client::processError(const String & query) const
fmt::print(stderr, "Received exception from server (version {}):\n{}\n",
server_version,
getExceptionMessage(*server_exception, print_stack_trace, true));
+
if (is_interactive)
{
fmt::print(stderr, "\n");
@@ -936,6 +937,7 @@ void Client::addOptions(OptionsDescription & options_description)
("ssh-key-file", po::value(), "File containing ssh private key needed for authentication. If not set does password authentication.")
("ssh-key-passphrase", po::value(), "Passphrase for imported ssh key.")
("quota_key", po::value(), "A string to differentiate quotas when the user have keyed quotas configured on server")
+ ("jwt", po::value(), "Use JWT for authentication")
("max_client_network_bandwidth", po::value(), "the maximum speed of data exchange over the network for the client in bytes per second.")
("compression", po::value(), "enable or disable compression (enabled by default for remote communication and disabled for localhost communication).")
@@ -1093,6 +1095,12 @@ void Client::processOptions(const OptionsDescription & options_description,
config().setBool("no-warnings", true);
if (options.count("fake-drop"))
fake_drop = true;
+ if (options.count("jwt"))
+ {
+ if (!options["user"].defaulted())
+ throw Exception(ErrorCodes::BAD_ARGUMENTS, "User and JWT flags can't be specified together");
+ config().setString("jwt", options["jwt"].as());
+ }
if (options.count("accept-invalid-certificate"))
{
config().setString("openSSL.client.invalidCertificateHandler.name", "AcceptCertificateHandler");
diff --git a/src/Access/AccessControl.cpp b/src/Access/AccessControl.cpp
index 368d8b881fef..53e89d7d10cb 100644
--- a/src/Access/AccessControl.cpp
+++ b/src/Access/AccessControl.cpp
@@ -688,6 +688,11 @@ bool AccessControl::isNoPasswordAllowed() const
return allow_no_password;
}
+bool AccessControl::isJWTEnabled() const
+{
+ return external_authenticators->isJWTAllowed();
+}
+
void AccessControl::setPlaintextPasswordAllowed(bool allow_plaintext_password_)
{
allow_plaintext_password = allow_plaintext_password_;
diff --git a/src/Access/AccessControl.h b/src/Access/AccessControl.h
index fae60efe9836..a49252ffa332 100644
--- a/src/Access/AccessControl.h
+++ b/src/Access/AccessControl.h
@@ -148,6 +148,8 @@ class AccessControl : public MultipleAccessStorage
void setNoPasswordAllowed(bool allow_no_password_);
bool isNoPasswordAllowed() const;
+ bool isJWTEnabled() const;
+
/// Allows users with plaintext password (by default it's allowed).
void setPlaintextPasswordAllowed(bool allow_plaintext_password_);
bool isPlaintextPasswordAllowed() const;
diff --git a/src/Access/Authentication.cpp b/src/Access/Authentication.cpp
index 47187d831548..701d0e5cdf6c 100644
--- a/src/Access/Authentication.cpp
+++ b/src/Access/Authentication.cpp
@@ -107,6 +107,9 @@ bool Authentication::areCredentialsValid(
case AuthenticationType::HTTP:
throw Authentication::Require("ClickHouse Basic Authentication");
+ case AuthenticationType::JWT:
+ throw Authentication::Require("ClickHouse JWT Authentication");
+
case AuthenticationType::KERBEROS:
return external_authenticators.checkKerberosCredentials(auth_data.getKerberosRealm(), *gss_acceptor_context);
@@ -144,6 +147,9 @@ bool Authentication::areCredentialsValid(
case AuthenticationType::SSL_CERTIFICATE:
throw Authentication::Require("ClickHouse X.509 Authentication");
+ case AuthenticationType::JWT:
+ throw Authentication::Require("ClickHouse JWT Authentication");
+
case AuthenticationType::SSH_KEY:
throw Authentication::Require("Ssh Keys Authentication");
@@ -180,6 +186,9 @@ bool Authentication::areCredentialsValid(
case AuthenticationType::SSH_KEY:
throw Authentication::Require("Ssh Keys Authentication");
+ case AuthenticationType::JWT:
+ throw Authentication::Require("ClickHouse JWT Authentication");
+
case AuthenticationType::BCRYPT_PASSWORD:
return checkPasswordBcrypt(basic_credentials->getPassword(), auth_data.getPasswordHashBinary());
@@ -209,6 +218,9 @@ bool Authentication::areCredentialsValid(
case AuthenticationType::HTTP:
throw Authentication::Require("ClickHouse Basic Authentication");
+ case AuthenticationType::JWT:
+ throw Authentication::Require("ClickHouse JWT Authentication");
+
case AuthenticationType::KERBEROS:
throw Authentication::Require(auth_data.getKerberosRealm());
@@ -236,6 +248,9 @@ bool Authentication::areCredentialsValid(
case AuthenticationType::HTTP:
throw Authentication::Require("ClickHouse Basic Authentication");
+ case AuthenticationType::JWT:
+ throw Authentication::Require("ClickHouse JWT Authentication");
+
case AuthenticationType::KERBEROS:
throw Authentication::Require(auth_data.getKerberosRealm());
@@ -253,6 +268,39 @@ bool Authentication::areCredentialsValid(
}
}
+
+ if (const auto * jwt_credentials = typeid_cast(&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("ClickHouse Basic Authentication");
+
+ case AuthenticationType::JWT:
+ return external_authenticators.checkJWTCredentials(auth_data.getJWTClaims(), *jwt_credentials, settings);
+
+ case AuthenticationType::SSL_CERTIFICATE:
+ throw Authentication::Require("ClickHouse X.509 Authentication");
+
+ case AuthenticationType::SSH_KEY:
+#if USE_SSH
+ throw Authentication::Require("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(&credentials))
return true;
diff --git a/src/Access/AuthenticationData.cpp b/src/Access/AuthenticationData.cpp
index da90a0f5842c..9d014d840a87 100644
--- a/src/Access/AuthenticationData.cpp
+++ b/src/Access/AuthenticationData.cpp
@@ -1,5 +1,6 @@
#include
#include
+#include
#include
#include
#include
@@ -14,6 +15,7 @@
#include
#include
#include
+#include
#include "config.h"
@@ -126,6 +128,7 @@ void AuthenticationData::setPassword(const String & password_)
case AuthenticationType::BCRYPT_PASSWORD:
case AuthenticationType::NO_PASSWORD:
case AuthenticationType::LDAP:
+ case AuthenticationType::JWT:
case AuthenticationType::KERBEROS:
case AuthenticationType::SSL_CERTIFICATE:
case AuthenticationType::SSH_KEY:
@@ -231,6 +234,7 @@ void AuthenticationData::setPasswordHashBinary(const Digest & hash)
case AuthenticationType::NO_PASSWORD:
case AuthenticationType::LDAP:
+ case AuthenticationType::JWT:
case AuthenticationType::KERBEROS:
case AuthenticationType::SSL_CERTIFICATE:
case AuthenticationType::SSH_KEY:
@@ -302,6 +306,13 @@ std::shared_ptr AuthenticationData::toAST() const
node->children.push_back(std::make_shared(getLDAPServerName()));
break;
}
+ case AuthenticationType::JWT:
+ {
+ const auto & claims = getJWTClaims();
+ if (!claims.empty())
+ node->children.push_back(std::make_shared(claims));
+ break;
+ }
case AuthenticationType::KERBEROS:
{
const auto & realm = getKerberosRealm();
@@ -504,6 +515,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(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);
+ }
+ }
else
{
throw Exception(ErrorCodes::LOGICAL_ERROR, "Unexpected ASTAuthenticationData structure");
diff --git a/src/Access/AuthenticationData.h b/src/Access/AuthenticationData.h
index feef4d71d668..e52853a8c99d 100644
--- a/src/Access/AuthenticationData.h
+++ b/src/Access/AuthenticationData.h
@@ -68,6 +68,9 @@ class AuthenticationData
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); }
@@ -98,6 +101,7 @@ class AuthenticationData
/// HTTP authentication properties
String http_auth_server_name;
HTTPAuthenticationScheme http_auth_scheme = HTTPAuthenticationScheme::BASIC;
+ String jwt_claims;
};
}
diff --git a/src/Access/Common/AuthenticationType.cpp b/src/Access/Common/AuthenticationType.cpp
index 2cc126ad9b7a..427765b8a791 100644
--- a/src/Access/Common/AuthenticationType.cpp
+++ b/src/Access/Common/AuthenticationType.cpp
@@ -72,6 +72,11 @@ const AuthenticationTypeInfo & AuthenticationTypeInfo::get(AuthenticationType ty
static const auto info = make_info(Keyword::HTTP);
return info;
}
+ case AuthenticationType::JWT:
+ {
+ static const auto info = make_info(Keyword::JWT);
+ return info;
+ }
case AuthenticationType::MAX:
break;
}
diff --git a/src/Access/Common/AuthenticationType.h b/src/Access/Common/AuthenticationType.h
index 48ace3ca00a9..24feb6a43fd3 100644
--- a/src/Access/Common/AuthenticationType.h
+++ b/src/Access/Common/AuthenticationType.h
@@ -41,6 +41,9 @@ enum class AuthenticationType
/// Authentication through HTTP protocol
HTTP,
+ /// JSON Web Token
+ JWT,
+
MAX,
};
diff --git a/src/Access/Credentials.cpp b/src/Access/Credentials.cpp
index f9886c0182be..aed77ddfc49e 100644
--- a/src/Access/Credentials.cpp
+++ b/src/Access/Credentials.cpp
@@ -1,5 +1,8 @@
#include
#include
+#include
+
+#include
namespace DB
@@ -8,6 +11,7 @@ namespace DB
namespace ErrorCodes
{
extern const int LOGICAL_ERROR;
+ extern const int JWT_ERROR;
}
Credentials::Credentials(const String & user_name_)
@@ -97,4 +101,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::JWT_ERROR, "Failed to validate jwt");
+ }
+}
+}
+
+JWTCredentials::JWTCredentials(const String & token_)
+ : Credentials(extractSubjectFromToken(token_))
+ , token(token_)
+ {
+ is_ready = !user_name.empty();
+ }
}
diff --git a/src/Access/Credentials.h b/src/Access/Credentials.h
index 77b90eaaebce..696ee817f0cd 100644
--- a/src/Access/Credentials.h
+++ b/src/Access/Credentials.h
@@ -118,4 +118,20 @@ class SshCredentials : public Credentials
String original;
};
+class JWTCredentials: public Credentials
+{
+public:
+ explicit JWTCredentials(const String & token_);
+ const String & getToken() const
+ {
+ if (!isReady())
+ {
+ throwNotReady();
+ }
+ return token;
+ }
+private:
+ String token;
+};
+
}
diff --git a/src/Access/ExternalAuthenticators.cpp b/src/Access/ExternalAuthenticators.cpp
index 77812ac5eb5d..2ad4b7002ad2 100644
--- a/src/Access/ExternalAuthenticators.cpp
+++ b/src/Access/ExternalAuthenticators.cpp
@@ -2,14 +2,21 @@
#include
#include
#include
+#include "Common/Logger.h"
+#include "Common/logger_useful.h"
#include
#include
#include
#include
+#include "Access/AccessControl.h"
+#include "Access/Credentials.h"
+#include "Access/JWTValidator.h"
#include
#include
+#include