Skip to content

Commit cf800a8

Browse files
committed
some changes after review
1 parent 9a284f0 commit cf800a8

File tree

4 files changed

+38
-30
lines changed

4 files changed

+38
-30
lines changed

docs/en/operations/external-authenticators/tokens.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,30 @@ import SelfManaged from '@site/docs/en/_snippets/_self_managed_only_no_roadmap.m
88

99
ClickHouse users can be authenticated using tokens. This works in two ways:
1010

11-
- Existing users (defined in `users.xml` or in local access control paths) can be authenticated with a token if this user can be `IDENTIFIED WITH jwt`.
11+
- An existing user (defined in `users.xml` or in local access control paths) can be authenticated with a token if this user can be `IDENTIFIED WITH jwt`.
1212
- Use the information from the token or from an external Identity Provider (IdP) as a source of user definitions and allow locally undefined users to be authenticated with a valid token.
1313

1414
Although not all tokens are JWTs, under the hood both ways are treated as the same authentication method to maintain better compatibility.
1515

1616
# Token Processors
1717

1818
## Configuration
19-
To use token-based authentication, add `token_processors` section to `config.xml` and define at least one token processor in it.
19+
To use token-based authentication, add `token_processors` section to `config.xml` and define at least one token processor in it.
2020
Its contents are different for different token processor types.
2121

2222
**Common parameters**
2323
- `type` -- type of token processor. Supported values: "JWT", "Azure", "OpenID". Mandatory. Case-insensitive.
2424
- `token_cache_lifetime` -- maximum lifetime of cached token (in seconds). Optional, default: 3600.
2525
- `username_claim` -- name of claim (field) that will be treated as ClickHouse username. Optional, default: "sub".
26-
- `groups_claim` -- Name of claim (field) that contains list of groups user belongs to. This claim will be looked up in the token itself (in case token is a valid JWT, e.g. in Keycloak) or in response from `/userinfo`. Optional, default: "groups".
26+
- `groups_claim` -- name of claim (field) that contains list of groups user belongs to. This claim will be looked up in the token itself (in case token is a valid JWT, e.g. in Keycloak) or in response from `/userinfo`. Optional, default: "groups".
2727

28-
For each type, there are additional specific parameters.
28+
For each type, there are additional specific parameters.
2929
If some parameters that are not required for current processor type are specified, they are ignored.
3030
If there are conflicting parameters (e.g `algo` is specified together with `jwks_uri`), an exception will be thrown.
3131

3232
## JWT (JSON Web Token)
3333

34-
JWT itself is a source of information about user.
34+
JWT itself is a source of information about user.
3535
It is decoded locally and its integrity is verified using either static key or JWKS (JSON Web Key Set), either local or remote.
3636

3737
`algo`, `static_jwks`/`static_jwks_file` and `jwks_uri` are defining different JWT processing workflows, and they cannot be specified together.
@@ -48,7 +48,7 @@ It is decoded locally and its integrity is verified using either static key or J
4848
</clickhouse>
4949
```
5050
**Parameters:**
51-
- `algo` - Algorithm for validate signature. Mandatory. Supported values:
51+
- `algo` - Algorithm for signature validation. Mandatory. Supported values:
5252

5353
| HMAC | RSA | ECDSA | PSS | EdDSA |
5454
|-------| ----- | ------ | ----- | ------- |
@@ -168,7 +168,7 @@ Either `configuration_endpoint` or both `userinfo_endpoint` and `token_introspec
168168
Sometimes a token is a valid JWT. In that case token will be decoded and validated locally if configuration endpoint returns JWKS URI (or `jwks_uri` is specified alongside `userinfo_endpoint` and `token_introspection_endpoint`).
169169

170170
### Tokens cache
171-
To reduce number of requests to IdP, tokens are cached internally for no longer then `token_cache_lifetime` seconds.
171+
To reduce number of requests to IdP, tokens are cached internally for a maximum period of `token_cache_lifetime` seconds.
172172
If token expires sooner than `token_cache_lifetime`, then cache entry for this token will only be valid while token is valid.
173173
If token lifetime is longer than `token_cache_lifetime`, cache entry for this token will be valid for `token_cache_lifetime`.
174174

@@ -190,7 +190,7 @@ Example (goes into `users.xml`):
190190
</clickhouse>
191191
```
192192

193-
Here, the JWT payload must contain `["view-profile"]` on path `resource_access.account.roles`, otherwise authentication will not succeed even with a valid JWT.
193+
Here, the JWT payload must contain `["view-profile"]` on path `resource_access.account.roles`, otherwise authentication will not succeed even with a valid JWT.
194194

195195
:::note
196196
If `claims` is defined, this user will not be able to authenticate using opaque tokens, so, only JWT-based authentication will be available.
@@ -209,7 +209,7 @@ If `claims` is defined, this user will not be able to authenticate using opaque
209209
```
210210

211211
:::note
212-
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.
212+
A user cannot have JWT authentication together with any other authentication method. The presence of any other sections like `password` alongside `jwt` will force ClickHouse to shut down.
213213
:::
214214

215215
## Enabling token authentication using SQL {#enabling-jwt-auth-using-sql}
@@ -219,10 +219,10 @@ Users with "JWT" authentication type cannot be created using SQL now.
219219
## Identity Provider as an External User Directory {#idp-external-user-directory}
220220

221221
If there is no suitable user pre-defined in ClickHouse, authentication is still possible: Identity Provider can be used as source of user information.
222-
To allow this, add `token` section to the `users_directories` section of the `config.xml` file.
222+
To allow this, add `token` section to the `users_directories` section of the `config.xml` file.
223223

224224
At each login attempt, ClickHouse tries to find the user definition locally and authenticate it as usual.
225-
If the user is not defined, ClickHouse will treat the user as externally defined and will try to validate the token and get user information from the specified processor.
225+
If a token is provided but the user is not defined, ClickHouse will treat the user as externally defined and will try to validate the token and get user information from the specified processor.
226226
If validated successfully, the user will be considered existing and authenticated. The user will be assigned roles from the list specified in the `roles` section.
227227
All this implies that the SQL-driven [Access Control and Account Management](/docs/en/guides/sre/user-management/index.md#access-control) is enabled and roles are created using the [CREATE ROLE](/docs/en/sql-reference/statements/create/role.md#create-role-statement) statement.
228228

@@ -232,7 +232,7 @@ All this implies that the SQL-driven [Access Control and Account Management](/do
232232
<clickhouse>
233233
<user_directories>
234234
<token>
235-
<processor>processor_name</processor>
235+
<processor>token_processor_name</processor>
236236
<common_roles>
237237
<token_test_role_1 />
238238
</common_roles>

src/Access/Common/JWKSProvider.cpp

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,28 @@ jwt::jwks<jwt::traits::kazuho_picojson> JWKSClient::getJWKS()
3131
}
3232

3333
Poco::Net::HTTPResponse response;
34-
std::ostringstream responseString;
34+
std::string response_string;
3535

3636
Poco::Net::HTTPRequest request{Poco::Net::HTTPRequest::HTTP_GET, jwks_uri.getPathAndQuery()};
3737

3838
if (jwks_uri.getScheme() == "https")
3939
{
4040
Poco::Net::HTTPSClientSession session = Poco::Net::HTTPSClientSession(jwks_uri.getHost(), jwks_uri.getPort());
4141
session.sendRequest(request);
42-
std::istream & responseStream = session.receiveResponse(response);
43-
if (response.getStatus() != Poco::Net::HTTPResponse::HTTP_OK || !responseStream)
42+
std::istream & response_stream = session.receiveResponse(response);
43+
if (response.getStatus() != Poco::Net::HTTPResponse::HTTP_OK || !response_stream)
4444
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to get user info by access token, code: {}, reason: {}",
4545
response.getStatus(), response.getReason());
46-
Poco::StreamCopier::copyStream(responseStream, responseString);
46+
Poco::StreamCopier::copyToString(response_stream, response_string);
4747
}
4848
else
4949
{
5050
Poco::Net::HTTPClientSession session = Poco::Net::HTTPClientSession(jwks_uri.getHost(), jwks_uri.getPort());
5151
session.sendRequest(request);
52-
std::istream & responseStream = session.receiveResponse(response);
53-
if (response.getStatus() != Poco::Net::HTTPResponse::HTTP_OK || !responseStream)
52+
std::istream & response_stream = session.receiveResponse(response);
53+
if (response.getStatus() != Poco::Net::HTTPResponse::HTTP_OK || !response_stream)
5454
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to get user info by access token, code: {}, reason: {}", response.getStatus(), response.getReason());
55-
Poco::StreamCopier::copyStream(responseStream, responseString);
55+
Poco::StreamCopier::copyToString(response_stream, response_string);
5656
}
5757

5858
last_request_send = std::chrono::high_resolution_clock::now();
@@ -61,9 +61,9 @@ jwt::jwks<jwt::traits::kazuho_picojson> JWKSClient::getJWKS()
6161

6262
try
6363
{
64-
parsed_jwks = jwt::parse_jwks(responseString.str());
64+
parsed_jwks = jwt::parse_jwks(response_string);
6565
}
66-
catch (const Exception & e)
66+
catch (const std::exception & e)
6767
{
6868
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to parse JWKS: {}", e.what());
6969
}
@@ -72,7 +72,7 @@ jwt::jwks<jwt::traits::kazuho_picojson> JWKSClient::getJWKS()
7272
return cached_jwks;
7373
}
7474

75-
StaticJWKSParams::StaticJWKSParams(const std::string &static_jwks_, const std::string &static_jwks_file_)
75+
StaticJWKSParams::StaticJWKSParams(const std::string & static_jwks_, const std::string & static_jwks_file_)
7676
{
7777
if (static_jwks_.empty() && static_jwks_file_.empty())
7878
throw Exception(ErrorCodes::INVALID_CONFIG_PARAMETER,
@@ -85,16 +85,23 @@ StaticJWKSParams::StaticJWKSParams(const std::string &static_jwks_, const std::s
8585
static_jwks_file = static_jwks_file_;
8686
}
8787

88-
StaticJWKS::StaticJWKS(const StaticJWKSParams &params)
88+
StaticJWKS::StaticJWKS(const StaticJWKSParams & params)
8989
{
9090
String content = String(params.static_jwks);
9191
if (!params.static_jwks_file.empty())
9292
{
9393
std::ifstream ifs(params.static_jwks_file);
94-
content = String((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
94+
Poco::StreamCopier::copyToString(ifs, content);
95+
}
96+
try
97+
{
98+
auto keys = jwt::parse_jwks(content);
99+
jwks = std::move(keys);
100+
}
101+
catch (const std::exception & e)
102+
{
103+
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "Failed to parse JWKS: {}", e.what());
95104
}
96-
auto keys = jwt::parse_jwks(content);
97-
jwks = std::move(keys);
98105
}
99106

100107
}

src/Access/ExternalAuthenticators.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,9 @@ bool ExternalAuthenticators::checkCredentialsAgainstProcessor(const ITokenProces
616616
{
617617
if (credentials.getExpiresAt().value() < default_expiration_ts)
618618
cache_entry.expires_at = credentials.getExpiresAt().value();
619+
else
620+
LOG_TRACE(getLogger("AccessTokenAuthentication"), "Attempt to authenticate user {} with expired access token by {}", credentials.getUserName(), processor.getProcessorName());
621+
619622
}
620623
else
621624
{

src/Access/TokenAccessStorage.h

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ class TokenAccessStorage : public IAccessStorage
4040
bool isReadOnly() const override { return true; }
4141
bool exists(const UUID & id) const override;
4242

43-
private: // IAccessStorage implementations.
44-
43+
private:
4544
mutable std::recursive_mutex mutex; // Note: Reentrance possible by internal role lookup via access_control
4645
AccessControl & access_control;
4746
const Poco::Util::AbstractConfiguration & config;
@@ -56,10 +55,9 @@ class TokenAccessStorage : public IAccessStorage
5655
mutable std::map<String, std::set<String>> roles_per_users; // user name -> role names (...that should be granted to it; may but don't have to include common roles)
5756
mutable std::map<UUID, String> granted_role_names; // (currently granted) role id -> its name
5857
mutable std::map<String, UUID> granted_role_ids; // (currently granted) role name -> its id
59-
scope_guard role_change_subscription;
6058
mutable MemoryAccessStorage memory_storage;
59+
scope_guard role_change_subscription;
6160

62-
// void setConfiguration();
6361
void processRoleChange(const UUID & id, const AccessEntityPtr & entity);
6462

6563
bool areTokenCredentialsValidNoLock(const User & user, const Credentials & credentials, const ExternalAuthenticators & external_authenticators) const;

0 commit comments

Comments
 (0)