Skip to content

Commit 2513bc1

Browse files
committed
Squashed all JWT auth
Add aspell Enable jwt-cpp in fasttest Add test + some minor improvements reduce unneeded possible clash points fix parsing create user identified with jwt refactor + fix not lowercase update test fix typo in docs fix logical_error some refactor fix alg in jwks fix jwks fix user auth method not being checked update docs better exception on no sub claim throw exception if algo not specified in jwk Support access token authorization of existing users Also possible to filter users by e-mail using regex fix token accessstorage Add Azure token processor, move JWKS logic to separate file remove docs that will be obsolete in future remove redundant resolve tokenCredentials on creation add basic docs for oauth add caching, cleanup bs fix credentials cast + some better code fix include fix invalid token handling add basic openid auth add keykloak support major refactor
1 parent 615e0c0 commit 2513bc1

43 files changed

Lines changed: 2730 additions & 21 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ci/jobs/scripts/check_style/aspell-ignore/en/aspell-dict.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ DoubleDelta
286286
Doxygen
287287
Dresseler
288288
Durre
289+
ECDSA
290+
EdDSA
289291
ECMA
290292
ETag
291293
EachRow
@@ -519,6 +521,8 @@ JoinAlgorithm
519521
JoinStrictness
520522
JumpConsistentHash
521523
Jupyter
524+
jwks
525+
JWKS
522526
KDevelop
523527
KafkaAssignedPartitions
524528
KafkaBackgroundReads
@@ -3223,6 +3227,7 @@ uuid
32233227
uuids
32243228
uuidv
32253229
vCPU
3230+
validators
32263231
varPop
32273232
varPopStable
32283233
varSamp
@@ -3240,6 +3245,8 @@ vectorscan
32403245
vendoring
32413246
verificationDepth
32423247
verificationMode
3248+
verifier
3249+
verifiers
32433250
versionedcollapsingmergetree
32443251
vhost
32453252
virtualized

contrib/jwt-cpp-cmake/CMakeLists.txt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
set(ENABLE_JWT_CPP_DEFAULT OFF)
2-
if(ENABLE_LIBRARIES AND CLICKHOUSE_CLOUD)
3-
set(ENABLE_JWT_CPP_DEFAULT ON)
4-
endif()
1+
set(ENABLE_JWT_CPP_DEFAULT ON)
52

63
option(ENABLE_JWT_CPP "Enable jwt-cpp library" ${ENABLE_JWT_CPP_DEFAULT})
74

@@ -20,4 +17,4 @@ set (JWT_CPP_INCLUDE_DIR "${ClickHouse_SOURCE_DIR}/contrib/jwt-cpp/include")
2017

2118
add_library (_jwt-cpp INTERFACE)
2219
target_include_directories(_jwt-cpp SYSTEM BEFORE INTERFACE ${JWT_CPP_INCLUDE_DIR})
23-
add_library(ch_contrib::jwt-cpp ALIAS _jwt-cpp)
20+
add_library(ch_contrib::jwt-cpp ALIAS _jwt-cpp)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ The following external authenticators and directories are supported:
1818
- [LDAP](/operations/external-authenticators/ldap#ldap-external-authenticator) [Authenticator](./ldap.md#ldap-external-authenticator) and [Directory](./ldap.md#ldap-external-user-directory)
1919
- Kerberos [Authenticator](/operations/external-authenticators/kerberos#kerberos-as-an-external-authenticator-for-existing-users)
2020
- [SSL X.509 authentication](/operations/external-authenticators/ssl-x509)
21-
- HTTP [Authenticator](./http.md)
21+
- HTTP [Authenticator](./http.md)
22+
- Token-based [Authenticator](./tokens.md)
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
---
2+
slug: /en/operations/external-authenticators/oauth
3+
title: "Token-based authentication"
4+
---
5+
import SelfManaged from '@site/docs/en/_snippets/_self_managed_only_no_roadmap.md';
6+
7+
<SelfManaged />
8+
9+
ClickHouse users can be authenticated using tokens. This works in two ways:
10+
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`.
12+
- 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.
13+
14+
Although not all tokens are JWTs, under the hood both ways are treated as the same authentication method to maintain better compatibility.
15+
16+
# Token Processors
17+
18+
## Configuration
19+
To use token-based authentication, add `token_processors` section to `config.xml` and define at least one token processor in it.
20+
Its contents are different for different token processor types.
21+
22+
**Common parameters**
23+
- `type` -- type of token processor. Supported values: "JWT", "Azure", "OpenID". Mandatory. Case-insensitive.
24+
- `token_cache_lifetime` -- maximum lifetime of cached token (in seconds). Optional, default: 3600.
25+
- `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".
27+
28+
For each type, there are additional specific parameters.
29+
If some parameters that are not required for current processor type are specified, they are ignored.
30+
If there are conflicting parameters (e.g `algo` is specified together with `jwks_uri`), an exception will be thrown.
31+
32+
## JWT (JSON Web Token)
33+
34+
JWT itself is a source of information about user.
35+
It is decoded locally and its integrity is verified using either static key or JWKS (JSON Web Key Set), either local or remote.
36+
37+
`algo`, `static_jwks`/`static_jwks_file` and `jwks_uri` are defining different JWT processing workflows, and they cannot be specified together.
38+
### JWT with static key:
39+
```xml
40+
<clickhouse>
41+
<token_processors>
42+
<my_static_key_validator>
43+
<type>jwt</type>
44+
<algo>HS256</algo>
45+
<static_key>my_static_secret</static_key>
46+
</my_static_key_validator>
47+
</token_processors>
48+
</clickhouse>
49+
```
50+
**Parameters:**
51+
- `algo` - Algorithm for validate signature. Mandatory. Supported values:
52+
53+
| HMAC | RSA | ECDSA | PSS | EdDSA |
54+
|-------| ----- | ------ | ----- | ------- |
55+
| HS256 | RS256 | ES256 | PS256 | Ed25519 |
56+
| HS384 | RS384 | ES384 | PS384 | Ed448 |
57+
| HS512 | RS512 | ES512 | PS512 | |
58+
| | | ES256K | | |
59+
Also supports None (though not recommended).
60+
`claims` - A string containing a JSON object that should be contained in the token payload. If this parameter is defined, token without corresponding payload will be considered invalid. Optional.
61+
- `static_key` - key for symmetric algorithms. Mandatory for `HS*` family algorithms.
62+
- `static_key_in_base64` - indicates if the `static_key` key is base64-encoded. Optional, default: `False`.
63+
- `public_key` - public key for asymmetric algorithms. Mandatory except for `HS*` family algorithms and `None`.
64+
- `private_key` - private key for asymmetric algorithms. Optional.
65+
- `public_key_password` - public key password. Optional.
66+
- `private_key_password` - private key password. Optional.
67+
68+
### JWT with static JWKS
69+
```xml
70+
<clickhouse>
71+
<token_processors>
72+
<my_static_jwks_validator>
73+
<type>jwt</type>
74+
<static_jwks>{"keys": [{"kty": "RSA", "alg": "RS256", "kid": "mykid", "n": "_public_key_mod_", "e": "AQAB"}]}</static_jwks>
75+
</my_static_jwks_validator>
76+
</token_processors>
77+
</clickhouse>
78+
```
79+
80+
**Parameters:**
81+
82+
- `static_jwks` - content of JWKS in JSON
83+
- `static_jwks_file` - path to a file with JWKS
84+
- `claims` - A string containing a JSON object that should be contained in the token payload. If this parameter is defined, token without corresponding payload will be considered invalid. Optional.
85+
- `verifier_leeway` - Clock skew tolerance (seconds). Useful for handling small differences in system clocks between ClickHouse and the token issuer. Optional.
86+
87+
:::note
88+
Only one of `static_jwks` or `static_jwks_file` keys must be present in one verifier
89+
:::
90+
91+
:::note
92+
Only RS* family algorithms are supported!
93+
:::
94+
95+
### JWT with remote JWKS
96+
```xml
97+
<clickhouse>
98+
<token_processors>
99+
<basic_auth_server>
100+
<type>jwt</type>
101+
<jwks_uri>http://localhost:8000/.well-known/jwks.json</jwks_uri>
102+
<jwks_cache_lifetime>3600</jwks_cache_lifetime>
103+
</basic_auth_server>
104+
</token_processors>
105+
</clickhouse>
106+
```
107+
108+
**Parameters:**
109+
110+
- `uri` - JWKS endpoint. Mandatory.
111+
- `jwks_cache_lifetime` - Period for resend request for refreshing JWKS. Optional, default: 3600.
112+
- `claims` - A string containing a JSON object that should be contained in the token payload. If this parameter is defined, token without corresponding payload will be considered invalid. Optional.
113+
- `verifier_leeway` - Clock skew tolerance (seconds). Useful for handling small differences in system clocks between ClickHouse and the token issuer. Optional.
114+
115+
116+
## Processors with external providers
117+
118+
Some tokens cannot be decoded and validated locally. External service is needed in this case. "Azure" and "OpenID" (a generic type) are supported now.
119+
120+
### Azure
121+
```xml
122+
<clickhouse>
123+
<token_processors>
124+
<azure_processor>
125+
<type>azure</type>
126+
</azure_processor>
127+
</token_processors>
128+
</clickhouse>
129+
```
130+
131+
No additional parameters are required.
132+
133+
### OpenID
134+
```xml
135+
<clickhouse>
136+
<token_processors>
137+
<oid_processor_1>
138+
<type>openid</type>
139+
<configuration_endpoint>url/.well-known/openid-configuration</configuration_endpoint>
140+
<verifier_leeway>60</verifier_leeway>
141+
<jwks_cache_lifetime>3600</jwks_cache_lifetime>
142+
</oid_processor_1>
143+
<oid_processor_2>
144+
<type>openid</type>
145+
<userinfo_endpoint>url/userinfo</userinfo_endpoint>
146+
<token_introspection_endpoint>url/tokeninfo</token_introspection_endpoint>
147+
<jwks_uri>url/.well-known/jwks.json</jwks_uri>
148+
<verifier_leeway>60</verifier_leeway>
149+
<jwks_cache_lifetime>3600</jwks_cache_lifetime>
150+
</oid_processor_2>
151+
</token_processors>
152+
</clickhouse>
153+
```
154+
155+
:::note
156+
Either `configuration_endpoint` or both `userinfo_endpoint` and `token_introspection_endpoint` (and, optionally, `jwks_uri`) shall be set. If none of them are set or all three are set, this is an invalid configuration that will not be parsed.
157+
:::
158+
159+
**Parameters:**
160+
161+
- `configuration_endpoint` - URI of OpenID configuration (often ends with `.well-known/openid-configuration`);
162+
- `userinfo_endpoint` - URI of endpoint that returns user information in exchange for a valid token;
163+
- `token_introspection_endpoint` - URI of token introspection endpoint (returns information about a valid token);
164+
- `jwks_uri` - URI of OpenID configuration (often ends with `.well-known/jwks.json`)
165+
- `jwks_cache_lifetime` - Period for resend request for refreshing JWKS. Optional, default: 3600.
166+
- `verifier_leeway` - Clock skew tolerance (seconds). Useful for handling small differences in system clocks between ClickHouse and the token issuer. Optional, default: 60
167+
168+
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`).
169+
170+
### Tokens cache
171+
To reduce number of requests to IdP, tokens are cached internally for no longer then `token_cache_lifetime` seconds.
172+
If token expires sooner than `token_cache_lifetime`, then cache entry for this token will only be valid while token is valid.
173+
If token lifetime is longer than `token_cache_lifetime`, cache entry for this token will be valid for `token_cache_lifetime`.
174+
175+
## Enabling token authentication for a user in `users.xml` {#enabling-jwt-auth-in-users-xml}
176+
177+
In order to enable token-based authentication for the user, specify `jwt` section instead of `password` or other similar sections in the user definition.
178+
179+
Parameters:
180+
- `claims` - An optional string containing a json object that should be contained in the token payload.
181+
182+
Example (goes into `users.xml`):
183+
```xml
184+
<clickhouse>
185+
<my_user>
186+
<jwt>
187+
<claims>{"resource_access":{"account": {"roles": ["view-profile"]}}}</claims>
188+
</jwt>
189+
</my_user>
190+
</clickhouse>
191+
```
192+
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.
194+
195+
:::note
196+
If `claims` is defined, this user will not be able to authenticate using opaque tokens, so, only JWT-based authentication will be available.
197+
:::
198+
199+
```
200+
{
201+
...
202+
"resource_access": {
203+
"account": {
204+
"roles": ["view-profile"]
205+
}
206+
},
207+
...
208+
}
209+
```
210+
211+
:::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.
213+
:::
214+
215+
## Enabling token authentication using SQL {#enabling-jwt-auth-using-sql}
216+
217+
Users with "JWT" authentication type cannot be created using SQL now.
218+
219+
## Identity Provider as an External User Directory {#idp-external-user-directory}
220+
221+
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.
223+
224+
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.
226+
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.
227+
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.
228+
229+
**Example**
230+
231+
```xml
232+
<clickhouse>
233+
<user_directories>
234+
<token>
235+
<processor>processor_name</processor>
236+
<common_roles>
237+
<token_test_role_1 />
238+
</common_roles>
239+
<roles_filter>
240+
\bclickhouse-[a-zA-Z0-9]+\b
241+
</roles_filter>
242+
</token>
243+
</user_directories>
244+
</clickhouse>
245+
```
246+
247+
:::note
248+
For now, no more than one `token` section can be defined inside `user_directories`. This _may_ change in future.
249+
:::
250+
251+
**Parameters**
252+
253+
- `processor` — Name of one of processors defined in `token_processors` config section described above. This parameter is mandatory and cannot be empty.
254+
- `common_roles` — Section with a list of locally defined roles that will be assigned to each user retrieved from the IdP. Optional.
255+
- `roles_filter` — Regex string for groups filtering. Only groups matching this regex will be mapped to roles. Optional.

src/Access/AccessControl.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <Access/UsersConfigAccessStorage.h>
66
#include <Access/DiskAccessStorage.h>
77
#include <Access/LDAPAccessStorage.h>
8+
#include <Access/TokenAccessStorage.h>
89
#include <Access/ContextAccess.h>
910
#include <Access/EnabledSettings.h>
1011
#include <Access/EnabledRolesInfo.h>
@@ -41,6 +42,7 @@ namespace ErrorCodes
4142
extern const int REQUIRED_PASSWORD;
4243
extern const int CANNOT_COMPILE_REGEXP;
4344
extern const int BAD_ARGUMENTS;
45+
extern const int INVALID_CONFIG_PARAMETER;
4446
}
4547

4648
namespace
@@ -421,6 +423,12 @@ void AccessControl::addLDAPStorage(const String & storage_name_, const Poco::Uti
421423
LOG_DEBUG(getLogger(), "Added {} access storage '{}', LDAP server name: {}", String(new_storage->getStorageType()), new_storage->getStorageName(), new_storage->getLDAPServerName());
422424
}
423425

426+
void AccessControl::addTokenStorage(const String & storage_name_, const Poco::Util::AbstractConfiguration & config_, const String & prefix_)
427+
{
428+
auto new_storage = std::make_shared<TokenAccessStorage>(storage_name_, *this, config_, prefix_);
429+
addStorage(new_storage);
430+
LOG_DEBUG(getLogger(), "Added {} access storage '{}'", String(new_storage->getStorageType()), new_storage->getStorageName());
431+
}
424432

425433
void AccessControl::addStoragesFromUserDirectoriesConfig(
426434
const Poco::Util::AbstractConfiguration & config,
@@ -433,6 +441,8 @@ void AccessControl::addStoragesFromUserDirectoriesConfig(
433441
Strings keys_in_user_directories;
434442
config.keys(key, keys_in_user_directories);
435443

444+
bool has_token_storage = false;
445+
436446
for (const String & key_in_user_directories : keys_in_user_directories)
437447
{
438448
String prefix = key + "." + key_in_user_directories;
@@ -446,6 +456,8 @@ void AccessControl::addStoragesFromUserDirectoriesConfig(
446456
type = DiskAccessStorage::STORAGE_TYPE;
447457
else if (type == "ldap")
448458
type = LDAPAccessStorage::STORAGE_TYPE;
459+
else if (type == "token")
460+
type = TokenAccessStorage::STORAGE_TYPE;
449461

450462
String name = config.getString(prefix + ".name", type);
451463

@@ -479,6 +491,14 @@ void AccessControl::addStoragesFromUserDirectoriesConfig(
479491
bool allow_backup = config.getBool(prefix + ".allow_backup", true);
480492
addReplicatedStorage(name, zookeeper_path, get_zookeeper_function, allow_backup);
481493
}
494+
else if (type == TokenAccessStorage::STORAGE_TYPE)
495+
{
496+
if (has_token_storage)
497+
throw Exception(ErrorCodes::INVALID_CONFIG_PARAMETER, "Only one `token` section can be defined.");
498+
499+
addTokenStorage(name, config, prefix);
500+
has_token_storage = true;
501+
}
482502
else
483503
throw Exception(ErrorCodes::UNKNOWN_ELEMENT_IN_CONFIG, "Unknown storage type '{}' at {} in config", type, prefix);
484504
}

src/Access/AccessControl.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ class AccessControl : public MultipleAccessStorage
9393
/// Adds LDAPAccessStorage which allows querying remote LDAP server for user info.
9494
void addLDAPStorage(const String & storage_name_, const Poco::Util::AbstractConfiguration & config_, const String & prefix_);
9595

96+
void addTokenStorage(const String & storage_name_, const Poco::Util::AbstractConfiguration & config_, const String & prefix_);
97+
9698
void addReplicatedStorage(const String & storage_name,
9799
const String & zookeeper_path,
98100
const zkutil::GetZooKeeper & get_zookeeper_function,

0 commit comments

Comments
 (0)