From 4c0e620b928e53e3cd859c94a0e7cc21b05f4206 Mon Sep 17 00:00:00 2001 From: vdimir Date: Wed, 30 Oct 2024 16:35:01 +0000 Subject: [PATCH 1/9] Add TOTP for authentification Co-authored-by: Denis Kamenskii --- .gitmodules | 3 + contrib/CMakeLists.txt | 2 + contrib/libcotp | 1 + contrib/libcotp-cmake/CMakeLists.txt | 22 +++ .../external-authenticators/index.md | 3 +- .../external-authenticators/totp.md | 7 + programs/server/users.xml | 18 ++ src/Access/AccessControl.cpp | 2 +- src/Access/Authentication.cpp | 8 + src/Access/AuthenticationData.cpp | 30 ++- src/Access/AuthenticationData.h | 5 + src/Access/Common/AuthenticationType.cpp | 5 + src/Access/Common/AuthenticationType.h | 4 + src/Access/Common/CMakeLists.txt | 4 + src/Access/Common/OneTimePassword.cpp | 179 ++++++++++++++++++ src/Access/Common/OneTimePassword.h | 37 ++++ src/Access/UsersConfigAccessStorage.cpp | 19 +- src/Interpreters/SessionLog.cpp | 3 +- src/Parsers/Access/ASTAuthenticationData.cpp | 6 + src/Parsers/CommonParsers.h | 1 + src/Storages/System/StorageSystemUsers.cpp | 10 + tests/fuzz/dictionaries/keywords.dict | 5 +- tests/integration/test_totp_auth/__init__.py | 0 .../test_totp_auth/config/users.xml | 23 +++ tests/integration/test_totp_auth/test_totp.py | 112 +++++++++++ 25 files changed, 499 insertions(+), 10 deletions(-) create mode 160000 contrib/libcotp create mode 100644 contrib/libcotp-cmake/CMakeLists.txt create mode 100644 docs/en/operations/external-authenticators/totp.md create mode 100644 src/Access/Common/OneTimePassword.cpp create mode 100644 src/Access/Common/OneTimePassword.h create mode 100644 tests/integration/test_totp_auth/__init__.py create mode 100644 tests/integration/test_totp_auth/config/users.xml create mode 100644 tests/integration/test_totp_auth/test_totp.py diff --git a/.gitmodules b/.gitmodules index bbc8fc7d06c..3c870061290 100644 --- a/.gitmodules +++ b/.gitmodules @@ -369,3 +369,6 @@ [submodule "contrib/postgres"] path = contrib/postgres url = https://github.com/ClickHouse/postgres.git +[submodule "contrib/libcotp"] + path = contrib/libcotp + url = git@github.com:paolostivanin/libcotp.git diff --git a/contrib/CMakeLists.txt b/contrib/CMakeLists.txt index fa0f95245f2..c0685c960e3 100644 --- a/contrib/CMakeLists.txt +++ b/contrib/CMakeLists.txt @@ -219,6 +219,8 @@ add_contrib (prometheus-protobufs-cmake prometheus-protobufs prometheus-protobuf add_contrib(numactl-cmake numactl) +add_contrib (libcotp-cmake libcotp) + # Put all targets defined here and in subdirectories under "contrib/" folders in GUI-based IDEs. # Some of third-party projects may override CMAKE_FOLDER or FOLDER property of their targets, so they would not appear # in "contrib/..." as originally planned, so we workaround this by fixing FOLDER properties of all targets manually, diff --git a/contrib/libcotp b/contrib/libcotp new file mode 160000 index 00000000000..78a3783ac19 --- /dev/null +++ b/contrib/libcotp @@ -0,0 +1 @@ +Subproject commit 78a3783ac19604e9e3ad7053f1c43c761066bfb4 diff --git a/contrib/libcotp-cmake/CMakeLists.txt b/contrib/libcotp-cmake/CMakeLists.txt new file mode 100644 index 00000000000..1c90c9eaea3 --- /dev/null +++ b/contrib/libcotp-cmake/CMakeLists.txt @@ -0,0 +1,22 @@ +if(NOT ENABLE_SSL) + message(STATUS "libcotp: DISABLED because ENABLE_SSL=OFF") + return() +endif() + +set (LIBCOTP_SOURCE_DIR "${ClickHouse_SOURCE_DIR}/contrib/libcotp") +set (LIBCOTP_BINARY_DIR "${ClickHouse_BINARY_DIR}/contrib/libcotp") + +set(SRCS + "${LIBCOTP_SOURCE_DIR}/src/otp.c" + "${LIBCOTP_SOURCE_DIR}/src/utils/base32.c" + "${LIBCOTP_SOURCE_DIR}/src/utils/whmac_openssl.c" +) + +add_library (_libcotp ${SRCS}) + +file(MAKE_DIRECTORY "${LIBCOTP_BINARY_DIR}/include") +file(COPY "${LIBCOTP_SOURCE_DIR}/src/" DESTINATION "${LIBCOTP_BINARY_DIR}/include" FILES_MATCHING PATTERN "*.h") +target_include_directories(_libcotp SYSTEM BEFORE PUBLIC "${LIBCOTP_BINARY_DIR}/include") +target_link_libraries(_libcotp PRIVATE OpenSSL::Crypto) + +add_library(ch_contrib::libcotp ALIAS _libcotp) diff --git a/docs/en/operations/external-authenticators/index.md b/docs/en/operations/external-authenticators/index.md index f644613641c..f87db8834e8 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) +- [Time-based one-time password](./totp.md) diff --git a/docs/en/operations/external-authenticators/totp.md b/docs/en/operations/external-authenticators/totp.md new file mode 100644 index 00000000000..3036f415688 --- /dev/null +++ b/docs/en/operations/external-authenticators/totp.md @@ -0,0 +1,7 @@ +--- +slug: /en/operations/external-authenticators/ +title: "HTTP" +--- +import SelfManaged from '@site/docs/en/_snippets/_self_managed_only_no_roadmap.md'; + + diff --git a/programs/server/users.xml b/programs/server/users.xml index 57bc6309a54..1b148af7cf6 100644 --- a/programs/server/users.xml +++ b/programs/server/users.xml @@ -32,6 +32,24 @@ If you want to specify double SHA1, place it in 'password_double_sha1_hex' element. Example: e395796d6546b1b65db9d665cd43f0e858dd4303 + If you want to specify secret for one time passwords, place it in 'time_based_one_time_password' element. + Example: + + ALLT7V6M3NNSQXMEMPEAUSX77OOUTBSO + 30 + 6 + SHA1 + + + secret - base32 encoded secret key + period - password validity duration in seconds, default is 30 + digits - number of digits in password, default is 6 + algorithm - hash algorithm: SHA1/SHA256/SHA512, default is SHA1 + + How to generate secret: + Execute: echo `base32 < /dev/urandom | head -c32` + More info: https://datatracker.ietf.org/doc/html/rfc6238 + If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication, place its name in 'server' element inside 'ldap' element. Example: my_ldap_server diff --git a/src/Access/AccessControl.cpp b/src/Access/AccessControl.cpp index e8ee363be1a..e205d14b387 100644 --- a/src/Access/AccessControl.cpp +++ b/src/Access/AccessControl.cpp @@ -611,7 +611,7 @@ AuthResult AccessControl::authenticate(const Credentials & credentials, const Po tryLogCurrentException(getLogger(), "from: " + address.toString() + ", user: " + credentials.getUserName() + ": Authentication failed"); WriteBufferFromOwnString message; - message << credentials.getUserName() << ": Authentication failed: password is incorrect, or there is no user with such name."; + message << credentials.getUserName() << ": Authentication failed: password is incorrect, or there is no user with such name"; /// Better exception message for usability. /// It is typical when users install ClickHouse, type some password and instantly forget it. diff --git a/src/Access/Authentication.cpp b/src/Access/Authentication.cpp index 1d69a659cd6..7f129adbd5f 100644 --- a/src/Access/Authentication.cpp +++ b/src/Access/Authentication.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "config.h" @@ -136,6 +137,13 @@ namespace { return checkPasswordDoubleSHA1(basic_credentials->getPassword(), authentication_method.getPasswordHashBinary()); } + case AuthenticationType::ONE_TIME_PASSWORD: + { + return checkOneTimePassword( + /* password */ basic_credentials->getPassword(), + /* secret */ authentication_method.getPassword(), + /* config */ authentication_method.getOneTimePasswordConfig()); + } case AuthenticationType::LDAP: { return external_authenticators.checkLDAPCredentials(authentication_method.getLDAPServerName(), *basic_credentials); diff --git a/src/Access/AuthenticationData.cpp b/src/Access/AuthenticationData.cpp index 57a1cd756ff..d83b86bade9 100644 --- a/src/Access/AuthenticationData.cpp +++ b/src/Access/AuthenticationData.cpp @@ -133,6 +133,10 @@ void AuthenticationData::setPassword(const String & password_, bool validate) setPasswordHashBinary(Util::encodeDoubleSHA1(password_), validate); return; + case AuthenticationType::ONE_TIME_PASSWORD: + setPasswordHashBinary(Util::stringToDigest(normalizeOneTimePasswordSecret(password_)), validate); + return; + case AuthenticationType::BCRYPT_PASSWORD: case AuthenticationType::NO_PASSWORD: case AuthenticationType::LDAP: @@ -149,6 +153,15 @@ void AuthenticationData::setPassword(const String & password_, bool validate) throw Exception(ErrorCodes::NOT_IMPLEMENTED, "setPassword(): authentication type {} not supported", toString(type)); } +void AuthenticationData::setOneTimePassword(const String & password_, OneTimePasswordConfig config, bool validate) +{ + if (type != AuthenticationType::ONE_TIME_PASSWORD) + throw Exception(ErrorCodes::LOGICAL_ERROR, "Cannot specify one-time password for authentication type {}", toString(type)); + + otp_config = config; + setPasswordHashBinary(Util::stringToDigest(normalizeOneTimePasswordSecret(password_)), validate); +} + void AuthenticationData::setPasswordBcrypt(const String & password_, int workfactor_, bool validate) { if (type != AuthenticationType::BCRYPT_PASSWORD) @@ -159,8 +172,9 @@ void AuthenticationData::setPasswordBcrypt(const String & password_, int workfac String AuthenticationData::getPassword() const { - if (type != AuthenticationType::PLAINTEXT_PASSWORD) - throw Exception(ErrorCodes::LOGICAL_ERROR, "Cannot decode the password"); + if (type != AuthenticationType::PLAINTEXT_PASSWORD + && type != AuthenticationType::ONE_TIME_PASSWORD) + throw Exception(ErrorCodes::LOGICAL_ERROR, "Cannot decode the password for authentication type {}", type); return String(password_hash.data(), password_hash.data() + password_hash.size()); } @@ -205,6 +219,12 @@ void AuthenticationData::setPasswordHashBinary(const Digest & hash, bool validat return; } + case AuthenticationType::ONE_TIME_PASSWORD: + { + password_hash = hash; + return; + } + case AuthenticationType::SHA256_PASSWORD: { if (hash.size() != 32) @@ -307,6 +327,12 @@ std::shared_ptr AuthenticationData::toAST() const node->children.push_back(std::make_shared(getPassword())); break; } + case AuthenticationType::ONE_TIME_PASSWORD: + { + node->contains_password = true; + node->children.push_back(std::make_shared(getPassword())); + break; + } case AuthenticationType::SHA256_PASSWORD: { node->contains_hash = true; diff --git a/src/Access/AuthenticationData.h b/src/Access/AuthenticationData.h index a0c100264f8..bb0ccc71fd2 100644 --- a/src/Access/AuthenticationData.h +++ b/src/Access/AuthenticationData.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,9 @@ public: /// Returns the password. Allowed to use only for Type::PLAINTEXT_PASSWORD. String getPassword() const; + void setOneTimePassword(const String & password_, OneTimePasswordConfig config, bool validate); + const OneTimePasswordConfig & getOneTimePasswordConfig() const { return otp_config; } + /// Sets the password as a string of hexadecimal digits. void setPasswordHashHex(const String & hash, bool validate); String getPasswordHashHex() const; @@ -96,6 +100,7 @@ public: private: AuthenticationType type = AuthenticationType::NO_PASSWORD; Digest password_hash; + OneTimePasswordConfig otp_config; String ldap_server_name; String kerberos_realm; SSLCertificateSubjects ssl_certificate_subjects; diff --git a/src/Access/Common/AuthenticationType.cpp b/src/Access/Common/AuthenticationType.cpp index 427765b8a79..fe1a7382c2e 100644 --- a/src/Access/Common/AuthenticationType.cpp +++ b/src/Access/Common/AuthenticationType.cpp @@ -42,6 +42,11 @@ const AuthenticationTypeInfo & AuthenticationTypeInfo::get(AuthenticationType ty static const auto info = make_info(Keyword::DOUBLE_SHA1_PASSWORD, true); return info; } + case AuthenticationType::ONE_TIME_PASSWORD: + { + static const auto info = make_info(Keyword::ONE_TIME_PASSWORD, true); + return info; + } case AuthenticationType::LDAP: { static const auto info = make_info(Keyword::LDAP); diff --git a/src/Access/Common/AuthenticationType.h b/src/Access/Common/AuthenticationType.h index 16f4388bbff..00178d56d54 100644 --- a/src/Access/Common/AuthenticationType.h +++ b/src/Access/Common/AuthenticationType.h @@ -21,6 +21,10 @@ enum class AuthenticationType : uint8_t /// This kind of hash is used by the `mysql_native_password` authentication plugin. DOUBLE_SHA1_PASSWORD, + /// A secret is stored instead of a password + /// Users are authenticated using time-based one-time passwords + ONE_TIME_PASSWORD, + /// Password is checked by a [remote] LDAP server. Connection will be made at each authentication attempt. LDAP, diff --git a/src/Access/Common/CMakeLists.txt b/src/Access/Common/CMakeLists.txt index 6a7682ec4bd..a748af04277 100644 --- a/src/Access/Common/CMakeLists.txt +++ b/src/Access/Common/CMakeLists.txt @@ -3,3 +3,7 @@ include("${ClickHouse_SOURCE_DIR}/cmake/dbms_glob_sources.cmake") add_headers_and_sources(clickhouse_common_access .) add_library(clickhouse_common_access ${clickhouse_common_access_headers} ${clickhouse_common_access_sources}) target_link_libraries(clickhouse_common_access PUBLIC clickhouse_common_io) + +if (TARGET ch_contrib::libcotp) + target_link_libraries (clickhouse_common_access PRIVATE ch_contrib::libcotp) +endif() diff --git a/src/Access/Common/OneTimePassword.cpp b/src/Access/Common/OneTimePassword.cpp new file mode 100644 index 00000000000..21ce20085cc --- /dev/null +++ b/src/Access/Common/OneTimePassword.cpp @@ -0,0 +1,179 @@ +#include +#include +#include +#include +#include +#include + +#include "config.h" + + +namespace DB +{ + +namespace ErrorCodes +{ + extern const int BAD_ARGUMENTS; + extern const int LOGICAL_ERROR; +} + +static const UInt8 b32_alphabet[] = u8"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + +String normalizeOneTimePasswordSecret(const String & secret) +{ + static const UInt8 b32_lower_alphabet[] = u8"abcdefghijklmnopqrstuvwxyz"; + + constexpr static UInt8 CHAR_IS_VALID = 1; + constexpr static UInt8 CHAR_IS_LOWER = 2; + std::array table = {}; + for (const auto * p = b32_alphabet; *p; p++) + table[*p] = CHAR_IS_VALID; + for (const auto * p = b32_lower_alphabet; *p; p++) + table[*p] = CHAR_IS_LOWER; + + String result = secret; + size_t i = 0; + size_t n = 0; + for (; i < secret.size(); ++i) + { + if (secret[i] == ' ' || secret[i] == '=') + continue; + size_t idx = static_cast(secret[i]); + if (idx >= table.size() || table[idx] == 0) + throw Exception(ErrorCodes::BAD_ARGUMENTS, "Invalid character in base32 secret: '{}'", secret[i]); + if (table[idx] == CHAR_IS_VALID) + result[n] = secret[i]; + if (table[idx] == CHAR_IS_LOWER) + result[n] = std::toupper(secret[i]); + ++n; + } + result.resize(n); + + if (result.empty()) + throw Exception(ErrorCodes::BAD_ARGUMENTS, "Empty secret for one-time password"); + return result; +} + +static bool validateBase32Secret(const String & secret) +{ + if (secret.empty()) + return false; + + std::array table = {}; + for (const auto * p = b32_alphabet; *p; p++) + table[*p] = 1; + for (const auto c : secret) + { + size_t idx = static_cast(c); + if (idx >= table.size() || table[idx] == 0) + return false; + } + return true; +} + +static std::string_view toString(OneTimePasswordConfig::Algorithm algorithm) +{ + switch (algorithm) + { + case OneTimePasswordConfig::Algorithm::SHA1: return "SHA1"; + case OneTimePasswordConfig::Algorithm::SHA256: return "SHA256"; + case OneTimePasswordConfig::Algorithm::SHA512: return "SHA512"; + } + throw Exception(ErrorCodes::LOGICAL_ERROR, "Unknown algorithm for one-time password: {}", static_cast(algorithm)); +} + +static OneTimePasswordConfig::Algorithm hashingAlgorithmFromString(const String & algorithm_name) +{ + for (auto alg : {OneTimePasswordConfig::Algorithm::SHA1, OneTimePasswordConfig::Algorithm::SHA256, OneTimePasswordConfig::Algorithm::SHA512}) + { + if (Poco::toUpper(algorithm_name) == toString(alg)) + return alg; + } + throw Exception(ErrorCodes::BAD_ARGUMENTS, "Unknown algorithm for one-time password: '{}'", algorithm_name); +} + +OneTimePasswordConfig::OneTimePasswordConfig(Int32 num_digits_, Int32 period_, const String & algorithm_name_) +{ + if (num_digits_) + num_digits = num_digits_; + if (period_) + period = period_; + if (!algorithm_name_.empty()) + algorithm = hashingAlgorithmFromString(algorithm_name_); + + if (num_digits < 4 || 10 < num_digits) + throw Exception(ErrorCodes::BAD_ARGUMENTS, "Invalid number of digits for one-time password: {}", num_digits); + if (period <= 0 || 120 < period) + throw Exception(ErrorCodes::BAD_ARGUMENTS, "Invalid period for one-time password: {}", period); +} + +std::string_view OneTimePasswordConfig::getAlgorithmName() const { return toString(algorithm); } + +String getOneTimePasswordLink(const String & secret, const OneTimePasswordConfig & config) +{ + validateBase32Secret(secret); + + if (config == OneTimePasswordConfig{}) + return fmt::format("otpauth://totp/ClickHouse?issuer=ClickHouse&secret={}", secret); + + return fmt::format("otpauth://totp/ClickHouse?issuer=ClickHouse&secret={}&digits={}&period={}&algorithm={}", + secret, config.num_digits, config.period, toString(config.algorithm)); +} + +bool checkOneTimePassword(const String & password, const String & secret, const OneTimePasswordConfig & config) +{ + return password == getOneTimePassword(secret, config); +} + +} + +#if USE_SSL + +#include + +constexpr int TOTP_SHA512 = SHA512; +constexpr int TOTP_SHA256 = SHA256; +constexpr int TOTP_SHA1 = SHA1; +#undef SHA512 +#undef SHA256 +#undef SHA1 + +namespace DB +{ + +String getOneTimePassword(const String & secret, const OneTimePasswordConfig & config) +{ + validateBase32Secret(secret); + + cotp_error_t error; + int sha_algo = config.algorithm == OneTimePasswordConfig::Algorithm::SHA512 ? TOTP_SHA512 + : config.algorithm == OneTimePasswordConfig::Algorithm::SHA256 ? TOTP_SHA256 + : TOTP_SHA1; + + auto result = std::unique_ptr(get_totp(secret.c_str(), config.num_digits, config.period, sha_algo, &error)); + if (result == nullptr || (error != NO_ERROR && error != VALID)) + throw Exception(ErrorCodes::BAD_ARGUMENTS, "Error while retrieving one-time password, code: {}", + static_cast>(error)); + return String(result.get(), strlen(result.get())); +} + +} + +#else + +namespace DB +{ + +namespace ErrorCodes +{ + extern const int SUPPORT_IS_DISABLED; +} + +String getOneTimePassword(const String & secret) +{ + throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "One-time password support is disabled, because ClickHouse was built without openssl library"); +} + +} + +#endif diff --git a/src/Access/Common/OneTimePassword.h b/src/Access/Common/OneTimePassword.h new file mode 100644 index 00000000000..f8aafe8a775 --- /dev/null +++ b/src/Access/Common/OneTimePassword.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace DB +{ + +struct OneTimePasswordConfig +{ + Int32 num_digits = 6; + Int32 period = 30; + + enum class Algorithm : UInt8 + { + SHA1, + SHA256, + SHA512, + } algorithm = Algorithm::SHA1; + + explicit OneTimePasswordConfig(Int32 num_digits_ = 0, Int32 period_ = 0, const String & algorithm_name_ = ""); + + bool operator==(const OneTimePasswordConfig &) const = default; + + std::string_view getAlgorithmName() const; +}; + + +String getOneTimePasswordLink(const String & secret, const OneTimePasswordConfig & config); + +String getOneTimePassword(const String & secret, const OneTimePasswordConfig & config); +bool checkOneTimePassword(const String & password, const String & secret, const OneTimePasswordConfig & config); + +/// Checks if the secret contains only valid base32 characters. +/// The secret may contain spaces, which are ignored and lower-case characters, which are converted to upper-case. +String normalizeOneTimePasswordSecret(const String & secret); + +} diff --git a/src/Access/UsersConfigAccessStorage.cpp b/src/Access/UsersConfigAccessStorage.cpp index eddc7ca1e0e..12ea39bf71c 100644 --- a/src/Access/UsersConfigAccessStorage.cpp +++ b/src/Access/UsersConfigAccessStorage.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -127,6 +129,7 @@ namespace String user_config = "users." + user_name; bool has_no_password = config.has(user_config + ".no_password"); bool has_password_plaintext = config.has(user_config + ".password"); + bool has_otp_secret = config.has(user_config + ".time_based_one_time_password"); bool has_password_sha256_hex = config.has(user_config + ".password_sha256_hex"); bool has_password_double_sha1_hex = config.has(user_config + ".password_double_sha1_hex"); bool has_ldap = config.has(user_config + ".ldap"); @@ -142,11 +145,11 @@ 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_otp_secret; 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', " + "'password_double_sha1_hex', 'no_password', 'time_based_one_time_password', 'ldap', 'kerberos', 'ssl_certificates', 'ssh_keys', " "'http_authentication' are used to specify authentication info for user {}. " "Must be only one of them.", user_name); @@ -170,6 +173,16 @@ namespace user->authentication_methods.emplace_back(AuthenticationType::DOUBLE_SHA1_PASSWORD); user->authentication_methods.back().setPasswordHashHex(config.getString(user_config + ".password_double_sha1_hex"), validate); } + else if (has_otp_secret) + { + String secret = config.getString(user_config + ".time_based_one_time_password.secret", ""); + OneTimePasswordConfig otp_config( + config.getInt(user_config + ".time_based_one_time_password.digits", {}), + config.getInt(user_config + ".time_based_one_time_password.period", {}), + config.getString(user_config + ".time_based_one_time_password.algorithm", "") + ); + user->authentication_methods.emplace_back(AuthenticationType::ONE_TIME_PASSWORD).setOneTimePassword(secret, otp_config, validate); + } else if (has_ldap) { bool has_ldap_server = config.has(user_config + ".ldap.server"); @@ -269,7 +282,7 @@ namespace } else { - user->authentication_methods.emplace_back(); + user->authentication_methods.emplace_back(AuthenticationType::NO_PASSWORD); } for (const auto & authentication_method : user->authentication_methods) diff --git a/src/Interpreters/SessionLog.cpp b/src/Interpreters/SessionLog.cpp index afbd4ed45a1..88d86b41b3b 100644 --- a/src/Interpreters/SessionLog.cpp +++ b/src/Interpreters/SessionLog.cpp @@ -85,6 +85,7 @@ ColumnsDescription SessionLogElement::getColumnsDescription() AUTH_TYPE_NAME_AND_VALUE(AuthType::PLAINTEXT_PASSWORD), AUTH_TYPE_NAME_AND_VALUE(AuthType::SHA256_PASSWORD), AUTH_TYPE_NAME_AND_VALUE(AuthType::DOUBLE_SHA1_PASSWORD), + AUTH_TYPE_NAME_AND_VALUE(AuthType::ONE_TIME_PASSWORD), AUTH_TYPE_NAME_AND_VALUE(AuthType::LDAP), AUTH_TYPE_NAME_AND_VALUE(AuthType::JWT), AUTH_TYPE_NAME_AND_VALUE(AuthType::KERBEROS), @@ -94,7 +95,7 @@ ColumnsDescription SessionLogElement::getColumnsDescription() AUTH_TYPE_NAME_AND_VALUE(AuthType::HTTP), }); #undef AUTH_TYPE_NAME_AND_VALUE - static_assert(static_cast(AuthenticationType::MAX) == 11); + static_assert(static_cast(AuthenticationType::MAX) == 12); auto interface_type_column = std::make_shared( DataTypeEnum8::Values diff --git a/src/Parsers/Access/ASTAuthenticationData.cpp b/src/Parsers/Access/ASTAuthenticationData.cpp index 7a1091d8a1a..95bf268a4bb 100644 --- a/src/Parsers/Access/ASTAuthenticationData.cpp +++ b/src/Parsers/Access/ASTAuthenticationData.cpp @@ -89,6 +89,12 @@ void ASTAuthenticationData::formatImpl(const FormatSettings & settings, FormatSt password = true; break; } + case AuthenticationType::ONE_TIME_PASSWORD: + { + prefix = "BY"; + password = true; + break; + } case AuthenticationType::JWT: { prefix = "CLAIMS"; diff --git a/src/Parsers/CommonParsers.h b/src/Parsers/CommonParsers.h index dd0ba91d428..6c190a332b3 100644 --- a/src/Parsers/CommonParsers.h +++ b/src/Parsers/CommonParsers.h @@ -561,6 +561,7 @@ namespace DB MR_MACROS(DOUBLE_SHA1_PASSWORD, "DOUBLE_SHA1_PASSWORD") \ MR_MACROS(IS_OBJECT_ID, "IS_OBJECT_ID") \ MR_MACROS(NO_PASSWORD, "NO_PASSWORD") \ + MR_MACROS(ONE_TIME_PASSWORD, "ONE_TIME_PASSWORD") \ MR_MACROS(PART_MOVE_TO_SHARD, "PART_MOVE_TO_SHARD") \ MR_MACROS(PLAINTEXT_PASSWORD, "PLAINTEXT_PASSWORD") \ MR_MACROS(SHA256_HASH, "SHA256_HASH") \ diff --git a/src/Storages/System/StorageSystemUsers.cpp b/src/Storages/System/StorageSystemUsers.cpp index 7589afdeb3e..325931d3bf0 100644 --- a/src/Storages/System/StorageSystemUsers.cpp +++ b/src/Storages/System/StorageSystemUsers.cpp @@ -149,6 +149,16 @@ void StorageSystemUsers::fillData(MutableColumns & res_columns, ContextPtr conte { auth_params_json.set("realm", auth_data.getKerberosRealm()); } + else if (auth_data.getType() == AuthenticationType::ONE_TIME_PASSWORD) + { + const auto & config = auth_data.getOneTimePasswordConfig(); + if (config != OneTimePasswordConfig{}) + { + auth_params_json.set("algorithm", toString(config.algorithm)); + auth_params_json.set("num_digits", toString(config.num_digits)); + auth_params_json.set("period", toString(config.period)); + } + } else if (auth_data.getType() == AuthenticationType::SSL_CERTIFICATE) { Poco::JSON::Array::Ptr common_names = new Poco::JSON::Array(); diff --git a/tests/fuzz/dictionaries/keywords.dict b/tests/fuzz/dictionaries/keywords.dict index abaaf9e53b5..6226725b3d3 100644 --- a/tests/fuzz/dictionaries/keywords.dict +++ b/tests/fuzz/dictionaries/keywords.dict @@ -341,14 +341,15 @@ "NULL" "NULLS" "OFFSET" -"ON" "ON DELETE" "ON UPDATE" "ON VOLUME" +"ON" +"ONE_TIME_PASSWORD" "ONLY" "OPTIMIZE TABLE" -"OR" "OR REPLACE" +"OR" "ORDER BY" "OUTER" "OVER" diff --git a/tests/integration/test_totp_auth/__init__.py b/tests/integration/test_totp_auth/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/integration/test_totp_auth/config/users.xml b/tests/integration/test_totp_auth/config/users.xml new file mode 100644 index 00000000000..ec0bf08c9a2 --- /dev/null +++ b/tests/integration/test_totp_auth/config/users.xml @@ -0,0 +1,23 @@ + + + + + + + + + inwg sy3l jbxx k43f biaa ==== + 10 + 9 + SHA1 + + + 1 + + ::/0 + + default + default + + + diff --git a/tests/integration/test_totp_auth/test_totp.py b/tests/integration/test_totp_auth/test_totp.py new file mode 100644 index 00000000000..c82ea7b426d --- /dev/null +++ b/tests/integration/test_totp_auth/test_totp.py @@ -0,0 +1,112 @@ +import pytest + + +import base64 +import hmac +import struct +import time +import hashlib +from fnmatch import fnmatch + + +from helpers.cluster import ClickHouseCluster + +cluster = ClickHouseCluster(__file__) +node = cluster.add_instance( + "node", + user_configs=["config/users.xml"], +) + + +@pytest.fixture(scope="module") +def started_cluster(): + try: + cluster.start() + yield cluster + finally: + cluster.shutdown() + + +def generate_totp(secret, interval=30, digits=6): + key = base64.b32decode(secret, casefold=True) + time_step = int(time.time() / interval) + msg = struct.pack(">Q", time_step) + hmac_hash = hmac.new(key, msg, hashlib.sha1).digest() + offset = hmac_hash[-1] & 0x0F + binary_code = struct.unpack(">I", hmac_hash[offset : offset + 4])[0] & 0x7FFFFFFF + otp = binary_code % (10**digits) + return f"{otp:0{digits}d}" + + +def test_one_time_password(started_cluster): + query_text = "SELECT currentUser() || toString(42)" + + totuser_secret = {"secret": "INWGSY3LJBXXK43FBIAA====", "interval": 10, "digits": 9} + old_password = generate_totp(**totuser_secret) + old_password_created = time.time() + assert "totuser42\n" == node.query( + query_text, user="totuser", password=old_password + ) + + assert "totuser42\n" == node.query( + query_text, user="totuser", password=generate_totp(**totuser_secret) + ) + + assert "CREATE USER totuser IDENTIFIED WITH one_time_password" in node.query( + "SHOW CREATE USER totuser", + user="totuser", + password=generate_totp(**totuser_secret), + ) + + for bad_secret, error_message in [ + ("i11egalbase32", "Invalid character in*secret"), + (" ", "Empty secret"), + (" =", "Empty secret"), + ("", "Empty secret"), + ]: + err_resp = node.query_and_get_error( + f"CREATE USER user2 IDENTIFIED WITH one_time_password BY '{bad_secret}'", + user="totuser", + password=generate_totp(**totuser_secret), + ) + assert fnmatch(err_resp, f"*{error_message}*BAD_ARGUMENTS*"), err_resp + + node.query( + "CREATE USER user2 IDENTIFIED WITH one_time_password BY 'WLFVSILVT3PKZPKONCMGAHN7KBPTUX2J'", + user="totuser", + password=generate_totp(**totuser_secret), + ) + + assert "user242\n" == node.query( + query_text, + user="user2", + password=generate_totp("WLFVSILVT3PKZPKONCMGAHN7KBPTUX2J"), + ) + + resp = node.query( + """ + SELECT + name, + auth_type[1], + JSONExtractString(auth_params[1], 'algorithm'), + JSONExtractString(auth_params[1], 'num_digits'), + JSONExtractString(auth_params[1], 'period') + FROM system.users WHERE name IN ('totuser', 'user2') + ORDER BY 1 + """, + user="totuser", + password=generate_totp(**totuser_secret), + ).splitlines() + assert resp[0].startswith("totuser\tone_time_password\tSHA1\t9\t10"), resp + assert resp[1].startswith("user2\tone_time_password"), resp + + # check that old password invalidated + elapsed = int(time.time() - old_password_created) + for _ in range(20 - elapsed): + time.sleep(1) + print(".", end="", flush=True) + print() + + assert "AUTHENTICATION_FAILED" in node.query_and_get_error( + query_text, user="totuser", password=old_password + ) From 2b1c21afa914fe541133783836468407a1049280 Mon Sep 17 00:00:00 2001 From: vdimir Date: Thu, 31 Oct 2024 11:44:04 +0000 Subject: [PATCH 2/9] better usability, add doc --- .../external-authenticators/totp.md | 58 ++++++++++++- src/Access/Common/OneTimePassword.cpp | 84 +++++++++---------- src/Access/Common/OneTimePassword.h | 5 +- tests/integration/test_totp_auth/test_totp.py | 29 +++---- 4 files changed, 108 insertions(+), 68 deletions(-) diff --git a/docs/en/operations/external-authenticators/totp.md b/docs/en/operations/external-authenticators/totp.md index 3036f415688..51bae0b9359 100644 --- a/docs/en/operations/external-authenticators/totp.md +++ b/docs/en/operations/external-authenticators/totp.md @@ -1,7 +1,61 @@ --- -slug: /en/operations/external-authenticators/ -title: "HTTP" +slug: /en/operations/external-authenticators/totp +title: "TOTP" --- import SelfManaged from '@site/docs/en/_snippets/_self_managed_only_no_roadmap.md'; + +Time-Based One-Time Password (TOTP) can be used to authenticate ClickHouse users by generating temporary access codes that are valid for a limited time. +In current implementation it is a standalone authentication method, rather than a second factor for password-based authentication. +This TOTP authentication method aligns with [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238) standards, making it compatible with popular TOTP applications like Google Authenticator, 1Password and similar tools. + +## TOTP Authentication Configuration {#totp-auth-configuration} + +To enable TOTP authentication for a user, configure the `time_based_one_time_password` section in `users.xml`. This section defines the TOTP settings, such as secret, validity period, number of digits, and hash algorithm. + +**Example** +```xml + + + + + + + JBSWY3DPEHPK3PXP + 30 + 6 + SHA1 + + + + + +Parameters: + +- secret - (Required) The base32-encoded secret key used to generate TOTP codes. +- period - Optional. Sets the validity period of each OTP in seconds. Must be a positive number not exceeding 120. Default is 30. +- digits - Optional. Specifies the number of digits in each OTP. Must be between 4 and 10. Default is 6. +- algorithm - Optional. Defines the hash algorithm for generating OTPs. Supported values are SHA1, SHA256, and SHA512. Default is SHA1. + +Generating a TOTP Secret + +To generate a TOTP-compatible secret for use with ClickHouse, run the following command in the terminal: + +```bash +$ base32 -w32 < /dev/urandom | head -1 +``` + +This command will produce a base32-encoded secret that can be added to the secret field in users.xml. + +To enable TOTP for a specific user, replace any existing password-based fields (like `password` or `password_sha256_hex`) with the `time_based_one_time_password` section. Only one authentication method is allowed per user, so TOTP cannot be combined with other methods such as password or LDAP. + +## Enabling TOTP Authentication using SQL {#enabling-totp-auth-using-sql} + +When SQL-driven Access Control and Account Management is enabled, TOTP authentication can be set for users via SQL: + +```SQL +CREATE USER my_user IDENTIFIED WITH one_time_password BY 'JBSWY3DPEHPK3PXP'; +``` + +Values for `period`, `digits`, and `algorithm` will be set to their default values. diff --git a/src/Access/Common/OneTimePassword.cpp b/src/Access/Common/OneTimePassword.cpp index 21ce20085cc..8ebbea35fe8 100644 --- a/src/Access/Common/OneTimePassword.cpp +++ b/src/Access/Common/OneTimePassword.cpp @@ -7,6 +7,18 @@ #include "config.h" +#if USE_SSL + +#include + +constexpr int TOTP_SHA512 = SHA512; +constexpr int TOTP_SHA256 = SHA256; +constexpr int TOTP_SHA1 = SHA1; +#undef SHA512 +#undef SHA256 +#undef SHA1 + +#endif namespace DB { @@ -15,6 +27,7 @@ namespace ErrorCodes { extern const int BAD_ARGUMENTS; extern const int LOGICAL_ERROR; + extern const int SUPPORT_IS_DISABLED; } static const UInt8 b32_alphabet[] = u8"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; @@ -109,7 +122,7 @@ OneTimePasswordConfig::OneTimePasswordConfig(Int32 num_digits_, Int32 period_, c std::string_view OneTimePasswordConfig::getAlgorithmName() const { return toString(algorithm); } -String getOneTimePasswordLink(const String & secret, const OneTimePasswordConfig & config) +String getOneTimePasswordSecretLink(const String & secret, const OneTimePasswordConfig & config) { validateBase32Secret(secret); @@ -120,60 +133,41 @@ String getOneTimePasswordLink(const String & secret, const OneTimePasswordConfig secret, config.num_digits, config.period, toString(config.algorithm)); } -bool checkOneTimePassword(const String & password, const String & secret, const OneTimePasswordConfig & config) +String getOneTimePassword(const String & secret [[ maybe_unused ]], const OneTimePasswordConfig & config [[ maybe_unused ]], UInt64 current_time [[ maybe_unused ]]) { - return password == getOneTimePassword(secret, config); -} - -} - #if USE_SSL - -#include - -constexpr int TOTP_SHA512 = SHA512; -constexpr int TOTP_SHA256 = SHA256; -constexpr int TOTP_SHA1 = SHA1; -#undef SHA512 -#undef SHA256 -#undef SHA1 - -namespace DB -{ - -String getOneTimePassword(const String & secret, const OneTimePasswordConfig & config) -{ - validateBase32Secret(secret); - - cotp_error_t error; int sha_algo = config.algorithm == OneTimePasswordConfig::Algorithm::SHA512 ? TOTP_SHA512 : config.algorithm == OneTimePasswordConfig::Algorithm::SHA256 ? TOTP_SHA256 : TOTP_SHA1; - auto result = std::unique_ptr(get_totp(secret.c_str(), config.num_digits, config.period, sha_algo, &error)); + cotp_error_t error; + auto result = std::unique_ptr(get_totp_at(secret.c_str(), current_time, config.num_digits, config.period, sha_algo, &error)); + if (result == nullptr || (error != NO_ERROR && error != VALID)) throw Exception(ErrorCodes::BAD_ARGUMENTS, "Error while retrieving one-time password, code: {}", static_cast>(error)); return String(result.get(), strlen(result.get())); -} - -} - #else - -namespace DB -{ - -namespace ErrorCodes -{ - extern const int SUPPORT_IS_DISABLED; -} - -String getOneTimePassword(const String & secret) -{ throw Exception(ErrorCodes::SUPPORT_IS_DISABLED, "One-time password support is disabled, because ClickHouse was built without openssl library"); -} - -} - #endif +} + + +bool checkOneTimePassword(const String & password, const String & secret, const OneTimePasswordConfig & config) +{ + if (password.size() != static_cast(config.num_digits) + || !std::all_of(password.begin(), password.end(), isdigit)) + return false; + + validateBase32Secret(secret); + + auto current_time = static_cast(std::time(nullptr)); + for (int delta : {0, -1, 1}) + { + if (password == getOneTimePassword(secret, config, current_time + delta * config.period)) + return true; + } + return false; +} + +} diff --git a/src/Access/Common/OneTimePassword.h b/src/Access/Common/OneTimePassword.h index f8aafe8a775..ee17ef2d5a8 100644 --- a/src/Access/Common/OneTimePassword.h +++ b/src/Access/Common/OneTimePassword.h @@ -24,10 +24,7 @@ struct OneTimePasswordConfig std::string_view getAlgorithmName() const; }; - -String getOneTimePasswordLink(const String & secret, const OneTimePasswordConfig & config); - -String getOneTimePassword(const String & secret, const OneTimePasswordConfig & config); +String getOneTimePasswordSecretLink(const String & secret, const OneTimePasswordConfig & config); bool checkOneTimePassword(const String & password, const String & secret, const OneTimePasswordConfig & config); /// Checks if the secret contains only valid base32 characters. diff --git a/tests/integration/test_totp_auth/test_totp.py b/tests/integration/test_totp_auth/test_totp.py index c82ea7b426d..bb28fad144c 100644 --- a/tests/integration/test_totp_auth/test_totp.py +++ b/tests/integration/test_totp_auth/test_totp.py @@ -27,9 +27,9 @@ def started_cluster(): cluster.shutdown() -def generate_totp(secret, interval=30, digits=6): +def generate_totp(secret, interval=30, digits=6, timepoint=None): key = base64.b32decode(secret, casefold=True) - time_step = int(time.time() / interval) + time_step = int(timepoint or time.time() / interval) msg = struct.pack(">Q", time_step) hmac_hash = hmac.new(key, msg, hashlib.sha1).digest() offset = hmac_hash[-1] & 0x0F @@ -40,11 +40,12 @@ def generate_totp(secret, interval=30, digits=6): def test_one_time_password(started_cluster): query_text = "SELECT currentUser() || toString(42)" - totuser_secret = {"secret": "INWGSY3LJBXXK43FBIAA====", "interval": 10, "digits": 9} - old_password = generate_totp(**totuser_secret) - old_password_created = time.time() - assert "totuser42\n" == node.query( + + old_password = generate_totp( + **totuser_secret, timepoint=time.time() - 3 * totuser_secret["interval"] + ) + assert "AUTHENTICATION_FAILED" in node.query_and_get_error( query_text, user="totuser", password=old_password ) @@ -52,6 +53,10 @@ def test_one_time_password(started_cluster): query_text, user="totuser", password=generate_totp(**totuser_secret) ) + assert "totuser42\n" == node.query( + query_text, user="totuser", password=generate_totp(**totuser_secret) + ) + assert "CREATE USER totuser IDENTIFIED WITH one_time_password" in node.query( "SHOW CREATE USER totuser", user="totuser", @@ -60,6 +65,7 @@ def test_one_time_password(started_cluster): for bad_secret, error_message in [ ("i11egalbase32", "Invalid character in*secret"), + ("abc$d", "Invalid character in*secret"), (" ", "Empty secret"), (" =", "Empty secret"), ("", "Empty secret"), @@ -99,14 +105,3 @@ def test_one_time_password(started_cluster): ).splitlines() assert resp[0].startswith("totuser\tone_time_password\tSHA1\t9\t10"), resp assert resp[1].startswith("user2\tone_time_password"), resp - - # check that old password invalidated - elapsed = int(time.time() - old_password_created) - for _ in range(20 - elapsed): - time.sleep(1) - print(".", end="", flush=True) - print() - - assert "AUTHENTICATION_FAILED" in node.query_and_get_error( - query_text, user="totuser", password=old_password - ) From 6fc8a8891370add656dfb534e73649841aaf7ca0 Mon Sep 17 00:00:00 2001 From: vdimir Date: Thu, 31 Oct 2024 11:46:39 +0000 Subject: [PATCH 3/9] fix stylecheck --- tests/integration/test_totp_auth/test_totp.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_totp_auth/test_totp.py b/tests/integration/test_totp_auth/test_totp.py index bb28fad144c..9dc8a1380cd 100644 --- a/tests/integration/test_totp_auth/test_totp.py +++ b/tests/integration/test_totp_auth/test_totp.py @@ -1,13 +1,11 @@ -import pytest - - import base64 +import hashlib import hmac import struct import time -import hashlib from fnmatch import fnmatch +import pytest from helpers.cluster import ClickHouseCluster From ade30bd6d2c835e9e400aef9c96b72b1ef7219a1 Mon Sep 17 00:00:00 2001 From: vdimir Date: Thu, 31 Oct 2024 11:46:55 +0000 Subject: [PATCH 4/9] upd contrib/libcotp link --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 3c870061290..b1b0f226b69 100644 --- a/.gitmodules +++ b/.gitmodules @@ -371,4 +371,4 @@ url = https://github.com/ClickHouse/postgres.git [submodule "contrib/libcotp"] path = contrib/libcotp - url = git@github.com:paolostivanin/libcotp.git + url = https://github.com/paolostivanin/libcotp.git From 4bdcd7a72f91b2bc8eb324a12e57ab467e31b0de Mon Sep 17 00:00:00 2001 From: vdimir Date: Fri, 1 Nov 2024 09:09:36 +0000 Subject: [PATCH 5/9] better --- .../external-authenticators/totp.md | 8 +++ src/Access/Authentication.cpp | 3 +- src/Access/AuthenticationData.cpp | 19 ++--- src/Access/AuthenticationData.h | 6 +- src/Access/Common/OneTimePassword.cpp | 72 ++++++++----------- src/Access/Common/OneTimePassword.h | 22 +++--- src/Access/UsersConfigAccessStorage.cpp | 5 +- src/Storages/System/StorageSystemUsers.cpp | 10 +-- .../test_totp_auth/config/users.xml | 23 ------ tests/integration/test_totp_auth/test_totp.py | 57 +++++++++++++-- 10 files changed, 124 insertions(+), 101 deletions(-) delete mode 100644 tests/integration/test_totp_auth/config/users.xml diff --git a/docs/en/operations/external-authenticators/totp.md b/docs/en/operations/external-authenticators/totp.md index 51bae0b9359..d75f9599caa 100644 --- a/docs/en/operations/external-authenticators/totp.md +++ b/docs/en/operations/external-authenticators/totp.md @@ -50,6 +50,14 @@ This command will produce a base32-encoded secret that can be added to the secre To enable TOTP for a specific user, replace any existing password-based fields (like `password` or `password_sha256_hex`) with the `time_based_one_time_password` section. Only one authentication method is allowed per user, so TOTP cannot be combined with other methods such as password or LDAP. +The [qrencode](https://linux.die.net/man/1/qrencode) tool can be used to generate a QR code for the TOTP secret. + +```bash +$ qrencode -t ansiutf8 'otpauth://totp/ClickHouse?issuer=ClickHouse&secret=JBSWY3DPEHPK3PXP' +``` + +After configuring TOTP for a user, one-time password can be used as a password for a user. + ## Enabling TOTP Authentication using SQL {#enabling-totp-auth-using-sql} When SQL-driven Access Control and Account Management is enabled, TOTP authentication can be set for users via SQL: diff --git a/src/Access/Authentication.cpp b/src/Access/Authentication.cpp index 7f129adbd5f..7a31ee31c86 100644 --- a/src/Access/Authentication.cpp +++ b/src/Access/Authentication.cpp @@ -141,8 +141,7 @@ namespace { return checkOneTimePassword( /* password */ basic_credentials->getPassword(), - /* secret */ authentication_method.getPassword(), - /* config */ authentication_method.getOneTimePasswordConfig()); + /* secret */ authentication_method.getOneTimePassword()); } case AuthenticationType::LDAP: { diff --git a/src/Access/AuthenticationData.cpp b/src/Access/AuthenticationData.cpp index d83b86bade9..fc4a76b1c5e 100644 --- a/src/Access/AuthenticationData.cpp +++ b/src/Access/AuthenticationData.cpp @@ -134,7 +134,7 @@ void AuthenticationData::setPassword(const String & password_, bool validate) return; case AuthenticationType::ONE_TIME_PASSWORD: - setPasswordHashBinary(Util::stringToDigest(normalizeOneTimePasswordSecret(password_)), validate); + setOneTimePassword(password_, OneTimePasswordParams{}, validate); return; case AuthenticationType::BCRYPT_PASSWORD: @@ -153,13 +153,13 @@ void AuthenticationData::setPassword(const String & password_, bool validate) throw Exception(ErrorCodes::NOT_IMPLEMENTED, "setPassword(): authentication type {} not supported", toString(type)); } -void AuthenticationData::setOneTimePassword(const String & password_, OneTimePasswordConfig config, bool validate) +void AuthenticationData::setOneTimePassword(const String & password_, OneTimePasswordParams config, bool validate) { if (type != AuthenticationType::ONE_TIME_PASSWORD) throw Exception(ErrorCodes::LOGICAL_ERROR, "Cannot specify one-time password for authentication type {}", toString(type)); - otp_config = config; - setPasswordHashBinary(Util::stringToDigest(normalizeOneTimePasswordSecret(password_)), validate); + otp_secret.emplace(password_, config); + setPasswordHashBinary(Util::stringToDigest(otp_secret->key), validate); } void AuthenticationData::setPasswordBcrypt(const String & password_, int workfactor_, bool validate) @@ -172,10 +172,13 @@ void AuthenticationData::setPasswordBcrypt(const String & password_, int workfac String AuthenticationData::getPassword() const { - if (type != AuthenticationType::PLAINTEXT_PASSWORD - && type != AuthenticationType::ONE_TIME_PASSWORD) - throw Exception(ErrorCodes::LOGICAL_ERROR, "Cannot decode the password for authentication type {}", type); - return String(password_hash.data(), password_hash.data() + password_hash.size()); + if (type == AuthenticationType::PLAINTEXT_PASSWORD) + return String(password_hash.data(), password_hash.data() + password_hash.size()); + + if (type == AuthenticationType::ONE_TIME_PASSWORD && otp_secret) + return otp_secret->key; + + throw Exception(ErrorCodes::LOGICAL_ERROR, "Cannot decode the password for authentication type {}", type); } diff --git a/src/Access/AuthenticationData.h b/src/Access/AuthenticationData.h index bb0ccc71fd2..0b8489195ac 100644 --- a/src/Access/AuthenticationData.h +++ b/src/Access/AuthenticationData.h @@ -37,8 +37,8 @@ public: /// Returns the password. Allowed to use only for Type::PLAINTEXT_PASSWORD. String getPassword() const; - void setOneTimePassword(const String & password_, OneTimePasswordConfig config, bool validate); - const OneTimePasswordConfig & getOneTimePasswordConfig() const { return otp_config; } + void setOneTimePassword(const String & password_, OneTimePasswordParams config, bool validate); + const OneTimePasswordSecret & getOneTimePassword() const { return otp_secret.value(); } /// Sets the password as a string of hexadecimal digits. void setPasswordHashHex(const String & hash, bool validate); @@ -100,7 +100,7 @@ public: private: AuthenticationType type = AuthenticationType::NO_PASSWORD; Digest password_hash; - OneTimePasswordConfig otp_config; + std::optional otp_secret; String ldap_server_name; String kerberos_realm; SSLCertificateSubjects ssl_certificate_subjects; diff --git a/src/Access/Common/OneTimePassword.cpp b/src/Access/Common/OneTimePassword.cpp index 8ebbea35fe8..9338fd32bb9 100644 --- a/src/Access/Common/OneTimePassword.cpp +++ b/src/Access/Common/OneTimePassword.cpp @@ -30,10 +30,11 @@ namespace ErrorCodes extern const int SUPPORT_IS_DISABLED; } -static const UInt8 b32_alphabet[] = u8"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - +/// Checks if the secret contains only valid base32 characters. +/// The secret may contain spaces, which are ignored and lower-case characters, which are converted to upper-case. String normalizeOneTimePasswordSecret(const String & secret) { + static const UInt8 b32_alphabet[] = u8"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; static const UInt8 b32_lower_alphabet[] = u8"abcdefghijklmnopqrstuvwxyz"; constexpr static UInt8 CHAR_IS_VALID = 1; @@ -67,37 +68,20 @@ String normalizeOneTimePasswordSecret(const String & secret) return result; } -static bool validateBase32Secret(const String & secret) -{ - if (secret.empty()) - return false; - - std::array table = {}; - for (const auto * p = b32_alphabet; *p; p++) - table[*p] = 1; - for (const auto c : secret) - { - size_t idx = static_cast(c); - if (idx >= table.size() || table[idx] == 0) - return false; - } - return true; -} - -static std::string_view toString(OneTimePasswordConfig::Algorithm algorithm) +static std::string_view toString(OneTimePasswordParams::Algorithm algorithm) { switch (algorithm) { - case OneTimePasswordConfig::Algorithm::SHA1: return "SHA1"; - case OneTimePasswordConfig::Algorithm::SHA256: return "SHA256"; - case OneTimePasswordConfig::Algorithm::SHA512: return "SHA512"; + case OneTimePasswordParams::Algorithm::SHA1: return "SHA1"; + case OneTimePasswordParams::Algorithm::SHA256: return "SHA256"; + case OneTimePasswordParams::Algorithm::SHA512: return "SHA512"; } throw Exception(ErrorCodes::LOGICAL_ERROR, "Unknown algorithm for one-time password: {}", static_cast(algorithm)); } -static OneTimePasswordConfig::Algorithm hashingAlgorithmFromString(const String & algorithm_name) +static OneTimePasswordParams::Algorithm hashingAlgorithmFromString(const String & algorithm_name) { - for (auto alg : {OneTimePasswordConfig::Algorithm::SHA1, OneTimePasswordConfig::Algorithm::SHA256, OneTimePasswordConfig::Algorithm::SHA512}) + for (auto alg : {OneTimePasswordParams::Algorithm::SHA1, OneTimePasswordParams::Algorithm::SHA256, OneTimePasswordParams::Algorithm::SHA512}) { if (Poco::toUpper(algorithm_name) == toString(alg)) return alg; @@ -105,7 +89,7 @@ static OneTimePasswordConfig::Algorithm hashingAlgorithmFromString(const String throw Exception(ErrorCodes::BAD_ARGUMENTS, "Unknown algorithm for one-time password: '{}'", algorithm_name); } -OneTimePasswordConfig::OneTimePasswordConfig(Int32 num_digits_, Int32 period_, const String & algorithm_name_) +OneTimePasswordParams::OneTimePasswordParams(Int32 num_digits_, Int32 period_, const String & algorithm_name_) { if (num_digits_) num_digits = num_digits_; @@ -120,24 +104,28 @@ OneTimePasswordConfig::OneTimePasswordConfig(Int32 num_digits_, Int32 period_, c throw Exception(ErrorCodes::BAD_ARGUMENTS, "Invalid period for one-time password: {}", period); } -std::string_view OneTimePasswordConfig::getAlgorithmName() const { return toString(algorithm); } +std::string_view OneTimePasswordParams::getAlgorithmName() const { return toString(algorithm); } -String getOneTimePasswordSecretLink(const String & secret, const OneTimePasswordConfig & config) + +OneTimePasswordSecret::OneTimePasswordSecret(const String & key_, OneTimePasswordParams params_) + : key(normalizeOneTimePasswordSecret(key_)), params(params_) { - validateBase32Secret(secret); - - if (config == OneTimePasswordConfig{}) - return fmt::format("otpauth://totp/ClickHouse?issuer=ClickHouse&secret={}", secret); - - return fmt::format("otpauth://totp/ClickHouse?issuer=ClickHouse&secret={}&digits={}&period={}&algorithm={}", - secret, config.num_digits, config.period, toString(config.algorithm)); } -String getOneTimePassword(const String & secret [[ maybe_unused ]], const OneTimePasswordConfig & config [[ maybe_unused ]], UInt64 current_time [[ maybe_unused ]]) +String getOneTimePasswordSecretLink(const OneTimePasswordSecret & secret) +{ + if (secret.params == OneTimePasswordParams{}) + return fmt::format("otpauth://totp/ClickHouse?issuer=ClickHouse&secret={}", secret.key); + + return fmt::format("otpauth://totp/ClickHouse?issuer=ClickHouse&secret={}&digits={}&period={}&algorithm={}", + secret.key, secret.params.num_digits, secret.params.period, toString(secret.params.algorithm)); +} + +String getOneTimePassword(const String & secret [[ maybe_unused ]], const OneTimePasswordParams & config [[ maybe_unused ]], UInt64 current_time [[ maybe_unused ]]) { #if USE_SSL - int sha_algo = config.algorithm == OneTimePasswordConfig::Algorithm::SHA512 ? TOTP_SHA512 - : config.algorithm == OneTimePasswordConfig::Algorithm::SHA256 ? TOTP_SHA256 + int sha_algo = config.algorithm == OneTimePasswordParams::Algorithm::SHA512 ? TOTP_SHA512 + : config.algorithm == OneTimePasswordParams::Algorithm::SHA256 ? TOTP_SHA256 : TOTP_SHA1; cotp_error_t error; @@ -153,18 +141,16 @@ String getOneTimePassword(const String & secret [[ maybe_unused ]], const OneTim } -bool checkOneTimePassword(const String & password, const String & secret, const OneTimePasswordConfig & config) +bool checkOneTimePassword(const String & password, const OneTimePasswordSecret & secret) { - if (password.size() != static_cast(config.num_digits) + if (password.size() != static_cast(secret.params.num_digits) || !std::all_of(password.begin(), password.end(), isdigit)) return false; - validateBase32Secret(secret); - auto current_time = static_cast(std::time(nullptr)); for (int delta : {0, -1, 1}) { - if (password == getOneTimePassword(secret, config, current_time + delta * config.period)) + if (password == getOneTimePassword(secret.key, secret.params, current_time + delta * secret.params.period)) return true; } return false; diff --git a/src/Access/Common/OneTimePassword.h b/src/Access/Common/OneTimePassword.h index ee17ef2d5a8..9bd49792d21 100644 --- a/src/Access/Common/OneTimePassword.h +++ b/src/Access/Common/OneTimePassword.h @@ -5,7 +5,7 @@ namespace DB { -struct OneTimePasswordConfig +struct OneTimePasswordParams { Int32 num_digits = 6; Int32 period = 30; @@ -17,18 +17,24 @@ struct OneTimePasswordConfig SHA512, } algorithm = Algorithm::SHA1; - explicit OneTimePasswordConfig(Int32 num_digits_ = 0, Int32 period_ = 0, const String & algorithm_name_ = ""); + explicit OneTimePasswordParams(Int32 num_digits_ = {}, Int32 period_ = {}, const String & algorithm_name_ = {}); - bool operator==(const OneTimePasswordConfig &) const = default; + bool operator==(const OneTimePasswordParams &) const = default; std::string_view getAlgorithmName() const; }; -String getOneTimePasswordSecretLink(const String & secret, const OneTimePasswordConfig & config); -bool checkOneTimePassword(const String & password, const String & secret, const OneTimePasswordConfig & config); +struct OneTimePasswordSecret +{ + String key; + OneTimePasswordParams params; -/// Checks if the secret contains only valid base32 characters. -/// The secret may contain spaces, which are ignored and lower-case characters, which are converted to upper-case. -String normalizeOneTimePasswordSecret(const String & secret); + explicit OneTimePasswordSecret( + const String & key_, + OneTimePasswordParams params_ = OneTimePasswordParams{}); +}; + +String getOneTimePasswordSecretLink(const OneTimePasswordSecret & secret); +bool checkOneTimePassword(const String & password, const OneTimePasswordSecret & secret); } diff --git a/src/Access/UsersConfigAccessStorage.cpp b/src/Access/UsersConfigAccessStorage.cpp index 12ea39bf71c..c2283db08d1 100644 --- a/src/Access/UsersConfigAccessStorage.cpp +++ b/src/Access/UsersConfigAccessStorage.cpp @@ -176,11 +176,10 @@ namespace else if (has_otp_secret) { String secret = config.getString(user_config + ".time_based_one_time_password.secret", ""); - OneTimePasswordConfig otp_config( + OneTimePasswordParams otp_config( config.getInt(user_config + ".time_based_one_time_password.digits", {}), config.getInt(user_config + ".time_based_one_time_password.period", {}), - config.getString(user_config + ".time_based_one_time_password.algorithm", "") - ); + config.getString(user_config + ".time_based_one_time_password.algorithm", {})); user->authentication_methods.emplace_back(AuthenticationType::ONE_TIME_PASSWORD).setOneTimePassword(secret, otp_config, validate); } else if (has_ldap) diff --git a/src/Storages/System/StorageSystemUsers.cpp b/src/Storages/System/StorageSystemUsers.cpp index 325931d3bf0..8c40efc0897 100644 --- a/src/Storages/System/StorageSystemUsers.cpp +++ b/src/Storages/System/StorageSystemUsers.cpp @@ -151,12 +151,12 @@ void StorageSystemUsers::fillData(MutableColumns & res_columns, ContextPtr conte } else if (auth_data.getType() == AuthenticationType::ONE_TIME_PASSWORD) { - const auto & config = auth_data.getOneTimePasswordConfig(); - if (config != OneTimePasswordConfig{}) + const auto & secret = auth_data.getOneTimePassword(); + if (secret.params != OneTimePasswordParams{}) { - auth_params_json.set("algorithm", toString(config.algorithm)); - auth_params_json.set("num_digits", toString(config.num_digits)); - auth_params_json.set("period", toString(config.period)); + auth_params_json.set("algorithm", toString(secret.params.algorithm)); + auth_params_json.set("num_digits", toString(secret.params.num_digits)); + auth_params_json.set("period", toString(secret.params.period)); } } else if (auth_data.getType() == AuthenticationType::SSL_CERTIFICATE) diff --git a/tests/integration/test_totp_auth/config/users.xml b/tests/integration/test_totp_auth/config/users.xml deleted file mode 100644 index ec0bf08c9a2..00000000000 --- a/tests/integration/test_totp_auth/config/users.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - inwg sy3l jbxx k43f biaa ==== - 10 - 9 - SHA1 - - - 1 - - ::/0 - - default - default - - - diff --git a/tests/integration/test_totp_auth/test_totp.py b/tests/integration/test_totp_auth/test_totp.py index 9dc8a1380cd..a9399153391 100644 --- a/tests/integration/test_totp_auth/test_totp.py +++ b/tests/integration/test_totp_auth/test_totp.py @@ -1,6 +1,8 @@ import base64 import hashlib import hmac +import os +import random import struct import time from fnmatch import fnmatch @@ -9,6 +11,9 @@ import pytest from helpers.cluster import ClickHouseCluster +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +TOTP_SECRET = base64.b32encode(random.randbytes(random.randint(3, 64))).decode() + cluster = ClickHouseCluster(__file__) node = cluster.add_instance( "node", @@ -16,20 +21,54 @@ node = cluster.add_instance( ) +def create_config(totp_secret): + config = f""" + + + + + + + + + {totp_secret} + 10 + 9 + SHA256 + + + 1 + + ::/0 + + default + default + + + +""".lstrip() + + with open(os.path.join(SCRIPT_DIR, "config/users.xml"), "w") as f: + f.write(config) + + @pytest.fixture(scope="module") def started_cluster(): try: + create_config(TOTP_SECRET) cluster.start() yield cluster finally: cluster.shutdown() -def generate_totp(secret, interval=30, digits=6, timepoint=None): +def generate_totp( + secret, interval=30, digits=6, sha_version=hashlib.sha1, timepoint=None +): key = base64.b32decode(secret, casefold=True) time_step = int(timepoint or time.time() / interval) msg = struct.pack(">Q", time_step) - hmac_hash = hmac.new(key, msg, hashlib.sha1).digest() + hmac_hash = hmac.new(key, msg, sha_version).digest() offset = hmac_hash[-1] & 0x0F binary_code = struct.unpack(">I", hmac_hash[offset : offset + 4])[0] & 0x7FFFFFFF otp = binary_code % (10**digits) @@ -38,7 +77,12 @@ def generate_totp(secret, interval=30, digits=6, timepoint=None): def test_one_time_password(started_cluster): query_text = "SELECT currentUser() || toString(42)" - totuser_secret = {"secret": "INWGSY3LJBXXK43FBIAA====", "interval": 10, "digits": 9} + totuser_secret = { + "secret": TOTP_SECRET, + "interval": 10, + "digits": 9, + "sha_version": hashlib.sha256, + } old_password = generate_totp( **totuser_secret, timepoint=time.time() - 3 * totuser_secret["interval"] @@ -75,8 +119,9 @@ def test_one_time_password(started_cluster): ) assert fnmatch(err_resp, f"*{error_message}*BAD_ARGUMENTS*"), err_resp + # lowercase secret with spaces is allowed node.query( - "CREATE USER user2 IDENTIFIED WITH one_time_password BY 'WLFVSILVT3PKZPKONCMGAHN7KBPTUX2J'", + "CREATE USER user2 IDENTIFIED WITH one_time_password BY 'inwg sy3l jbxx k43f biaa'", user="totuser", password=generate_totp(**totuser_secret), ) @@ -84,7 +129,7 @@ def test_one_time_password(started_cluster): assert "user242\n" == node.query( query_text, user="user2", - password=generate_totp("WLFVSILVT3PKZPKONCMGAHN7KBPTUX2J"), + password=generate_totp("INWGSY3LJBXXK43FBIAA===="), ) resp = node.query( @@ -101,5 +146,5 @@ def test_one_time_password(started_cluster): user="totuser", password=generate_totp(**totuser_secret), ).splitlines() - assert resp[0].startswith("totuser\tone_time_password\tSHA1\t9\t10"), resp + assert resp[0].startswith("totuser\tone_time_password\tSHA256\t9\t10"), resp assert resp[1].startswith("user2\tone_time_password"), resp From f9ef3bbdde9f4f65615b83a920fcfd58736175ea Mon Sep 17 00:00:00 2001 From: vdimir Date: Fri, 1 Nov 2024 09:16:18 +0000 Subject: [PATCH 6/9] mkdir tests/integration/test_totp_auth/config --- tests/integration/test_totp_auth/config/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/integration/test_totp_auth/config/.gitkeep diff --git a/tests/integration/test_totp_auth/config/.gitkeep b/tests/integration/test_totp_auth/config/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d From 203bd663e6f44e58f0ccb2dc83c27dc75f7b87bf Mon Sep 17 00:00:00 2001 From: vdimir Date: Fri, 1 Nov 2024 09:26:59 +0000 Subject: [PATCH 7/9] fix --- src/Access/Common/OneTimePassword.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Access/Common/OneTimePassword.cpp b/src/Access/Common/OneTimePassword.cpp index 9338fd32bb9..d7d8c8d5f13 100644 --- a/src/Access/Common/OneTimePassword.cpp +++ b/src/Access/Common/OneTimePassword.cpp @@ -121,6 +121,8 @@ String getOneTimePasswordSecretLink(const OneTimePasswordSecret & secret) secret.key, secret.params.num_digits, secret.params.period, toString(secret.params.algorithm)); } +struct CStringDeleter { void operator()(char * ptr) const { std::free(ptr); } }; + String getOneTimePassword(const String & secret [[ maybe_unused ]], const OneTimePasswordParams & config [[ maybe_unused ]], UInt64 current_time [[ maybe_unused ]]) { #if USE_SSL @@ -129,7 +131,7 @@ String getOneTimePassword(const String & secret [[ maybe_unused ]], const OneTim : TOTP_SHA1; cotp_error_t error; - auto result = std::unique_ptr(get_totp_at(secret.c_str(), current_time, config.num_digits, config.period, sha_algo, &error)); + auto result = std::unique_ptr(get_totp_at(secret.c_str(), current_time, config.num_digits, config.period, sha_algo, &error)); if (result == nullptr || (error != NO_ERROR && error != VALID)) throw Exception(ErrorCodes::BAD_ARGUMENTS, "Error while retrieving one-time password, code: {}", From 46483ed0ac387194c94ddcb4a539f28fb8c03fe3 Mon Sep 17 00:00:00 2001 From: vdimir Date: Fri, 1 Nov 2024 09:29:03 +0000 Subject: [PATCH 8/9] fix stylecheck --- utils/check-style/aspell-ignore/en/aspell-dict.txt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/utils/check-style/aspell-ignore/en/aspell-dict.txt b/utils/check-style/aspell-ignore/en/aspell-dict.txt index a08143467cd..1d403f32a85 100644 --- a/utils/check-style/aspell-ignore/en/aspell-dict.txt +++ b/utils/check-style/aspell-ignore/en/aspell-dict.txt @@ -1,4 +1,4 @@ -personal_ws-1.1 en 2984 +personal_ws-1.1 en 3156 AArch ACLs ALTERs @@ -186,7 +186,6 @@ ComplexKeyCache ComplexKeyDirect ComplexKeyHashed Composable -composable ConcurrencyControlAcquired ConcurrencyControlSoftLimit Config @@ -405,12 +404,12 @@ ITION Identifiant IdentifierQuotingRule IdentifierQuotingStyle -Incrementing -IndexesAreNeighbors -InfluxDB InJodaSyntax InJodaSyntaxOrNull InJodaSyntaxOrZero +Incrementing +IndexesAreNeighbors +InfluxDB Instana IntN Integrations @@ -1013,6 +1012,7 @@ TINYINT TLSv TMAX TMIN +TOTP TPCH TSDB TSVRaw @@ -1099,7 +1099,6 @@ URLHash URLHierarchy URLPathHierarchy USearch -USearch UTCTimestamp UUIDNumToString UUIDStringToNum @@ -2458,6 +2457,7 @@ punycodeEncode pushdown pwrite py +qrencode qryn quantile quantileBFloat @@ -2966,6 +2966,7 @@ topLevelDomain topLevelDomainRFC topk topkweighted +totp tpcds tpch transactionID From 9c1fcc5adc99ffdd9810486833bde4a2b272d67b Mon Sep 17 00:00:00 2001 From: vdimir Date: Fri, 1 Nov 2024 16:34:45 +0000 Subject: [PATCH 9/9] fix fasttests --- .../0_stateless/02117_show_create_table_system.reference | 2 +- .../02550_client_connections_credentials.reference | 6 +++--- .../0_stateless/02550_client_connections_credentials.sh | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/queries/0_stateless/02117_show_create_table_system.reference b/tests/queries/0_stateless/02117_show_create_table_system.reference index b260e2dce6c..cf98d6a117c 100644 --- a/tests/queries/0_stateless/02117_show_create_table_system.reference +++ b/tests/queries/0_stateless/02117_show_create_table_system.reference @@ -1145,7 +1145,7 @@ CREATE TABLE system.users `name` String, `id` UUID, `storage` String, - `auth_type` Array(Enum8('no_password' = 0, 'plaintext_password' = 1, 'sha256_password' = 2, 'double_sha1_password' = 3, 'ldap' = 4, 'kerberos' = 5, 'ssl_certificate' = 6, 'bcrypt_password' = 7, 'ssh_key' = 8, 'http' = 9, 'jwt' = 10)), + `auth_type` Array(Enum8('no_password' = 0, 'plaintext_password' = 1, 'sha256_password' = 2, 'double_sha1_password' = 3, 'one_time_password' = 4, 'ldap' = 5, 'kerberos' = 6, 'ssl_certificate' = 7, 'bcrypt_password' = 8, 'ssh_key' = 9, 'http' = 10, 'jwt' = 11)), `auth_params` Array(String), `host_ip` Array(String), `host_names` Array(String), diff --git a/tests/queries/0_stateless/02550_client_connections_credentials.reference b/tests/queries/0_stateless/02550_client_connections_credentials.reference index 1a0988002b1..b925adbed45 100644 --- a/tests/queries/0_stateless/02550_client_connections_credentials.reference +++ b/tests/queries/0_stateless/02550_client_connections_credentials.reference @@ -16,12 +16,12 @@ user MySQL: Authentication failed default password -default: Authentication failed: password is incorrect, or there is no user with such name. +default: Authentication failed: password is incorrect, or there is no user with such name default history_file Cannot create file: /no/such/dir/.history root overrides -foo: Authentication failed: password is incorrect, or there is no user with such name. +foo: Authentication failed: password is incorrect, or there is no user with such name default default -foo: Authentication failed: password is incorrect, or there is no user with such name. +foo: Authentication failed: password is incorrect, or there is no user with such name diff --git a/tests/queries/0_stateless/02550_client_connections_credentials.sh b/tests/queries/0_stateless/02550_client_connections_credentials.sh index a88f3fc7880..70e4467250b 100755 --- a/tests/queries/0_stateless/02550_client_connections_credentials.sh +++ b/tests/queries/0_stateless/02550_client_connections_credentials.sh @@ -117,7 +117,7 @@ echo 'user' $CLICKHOUSE_CLIENT --config $CONFIG --connection test_user -q 'select currentUser()' |& grep -F -o 'MySQL: Authentication failed' $CLICKHOUSE_CLIENT --config $CONFIG --connection test_user --user default -q 'select currentUser()' echo 'password' -$CLICKHOUSE_CLIENT --config $CONFIG --connection test_password -q 'select currentUser()' |& grep -F -o 'default: Authentication failed: password is incorrect, or there is no user with such name.' +$CLICKHOUSE_CLIENT --config $CONFIG --connection test_password -q 'select currentUser()' |& grep -F -o 'default: Authentication failed: password is incorrect, or there is no user with such name' $CLICKHOUSE_CLIENT --config $CONFIG --connection test_password --password "" -q 'select currentUser()' echo 'history_file' $CLICKHOUSE_CLIENT --progress off --interactive --config $CONFIG --connection test_history_file -q 'select 1'