Skip to content

Commit 0e209fe

Browse files
committed
add keykloak support(2)
1 parent 24b6916 commit 0e209fe

File tree

3 files changed

+103
-33
lines changed

3 files changed

+103
-33
lines changed

src/Access/AccessTokenProcessor.cpp

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ std::unique_ptr<IAccessTokenProcessor> IAccessTokenProcessor::parseTokenProcesso
128128

129129
if (is_auto && !is_manual)
130130
{
131-
return std::make_unique<OpenIDAccessTokenProcessor>(name, cache_lifetime, email_regex_str, config.getString(prefix + ".configuration_endpoint"));
131+
return std::make_unique<OpenIDAccessTokenProcessor>(name, cache_lifetime, email_regex_str, config.getString(prefix + ".configuration_endpoint"), config.getString(prefix + ".groups_claim_name", ""));
132132
}
133133
else if (!is_auto && is_manual)
134134
{
@@ -335,57 +335,120 @@ String AzureAccessTokenProcessor::validateTokenAndGetUsername(const String & tok
335335
return getValueByKey(user_info_json, "sub");
336336
}
337337

338+
339+
OpenIDAccessTokenProcessor::OpenIDAccessTokenProcessor(const String & name_,
340+
const UInt64 cache_invalidation_interval_,
341+
const String & email_regex_str,
342+
const String & userinfo_endpoint_,
343+
const String & token_introspection_endpoint_,
344+
const String & jwks_uri_,
345+
const String & groups_claim_name_)
346+
: IAccessTokenProcessor(name_, cache_invalidation_interval_, email_regex_str),
347+
userinfo_endpoint(userinfo_endpoint_), token_introspection_endpoint(token_introspection_endpoint_), groups_claim_name(groups_claim_name_)
348+
{
349+
if (!jwks_uri_.empty())
350+
{
351+
jwt_validator.emplace(name_ + "jwks_val", jwks_uri_, cache_invalidation_interval_);
352+
}
353+
}
354+
338355
OpenIDAccessTokenProcessor::OpenIDAccessTokenProcessor(const String & name_,
339356
const UInt64 cache_invalidation_interval_,
340357
const String & email_regex_str,
341-
const String & openid_config_endpoint_)
358+
const String & openid_config_endpoint_,
359+
const String & groups_claim_name_)
342360
: IAccessTokenProcessor(name_, cache_invalidation_interval_, email_regex_str)
343361
{
344362
const picojson::object openid_config = getObjectFromURI(Poco::URI(openid_config_endpoint_));
345363

346364
if (!openid_config.contains("userinfo_endpoint") || !openid_config.contains("introspection_endpoint"))
347365
throw Exception(ErrorCodes::AUTHENTICATION_FAILED, "{}: Cannot extract userinfo_endpoint or introspection_endpoint from OIDC configuration, consider manual configuration.", name);
366+
367+
OpenIDAccessTokenProcessor(name_,
368+
cache_invalidation_interval_,
369+
email_regex_str,
370+
getValueByKey(openid_config, "userinfo_endpoint"),
371+
getValueByKey(openid_config, "introspection_endpoint"),
372+
openid_config.contains("jwks_uri") ? getValueByKey(openid_config, "jwks_uri") : "",
373+
groups_claim_name_);
348374
}
349375

350376
bool OpenIDAccessTokenProcessor::resolveAndValidate(const TokenCredentials & credentials)
351377
{
352378
const String & token = credentials.getToken();
379+
String username;
380+
picojson::object user_info_json;
353381

354-
try
382+
if (jwt_validator.has_value() && jwt_validator.value().validate("", token, username))
355383
{
356-
String username = validateTokenAndGetUsername(token);
357-
if (!username.empty())
384+
385+
try
358386
{
359-
/// Credentials are passed as const everywhere up the flow, so we have to comply,
360-
/// in this case const_cast looks acceptable.
361-
const_cast<TokenCredentials &>(credentials).setUserName(username);
387+
auto decoded_token = jwt::decode(token);
388+
user_info_json = decoded_token.get_payload_json();
389+
390+
/// TODO: Now we work only with Keycloak -- and it provides expires_at in token itself. Need to add actual token introspection logic for other OIDC providers.
391+
if (decoded_token.has_expires_at())
392+
const_cast<TokenCredentials &>(credentials).setExpiresAt(decoded_token.get_expires_at());
362393
}
363-
else
364-
LOG_TRACE(getLogger("AccessTokenProcessor"), "{}: Failed to get username with token", name);
394+
catch (const std::exception & ex)
395+
{
396+
LOG_TRACE(getLogger("AccessTokenProcessor"), "{}: Failed to process token as JWT: {}", name, ex.what());
397+
}
398+
}
399+
400+
/// If username or user info is empty -- local validation failed, trying introspection via provider
401+
if (username.empty() || user_info_json.empty())
402+
{
403+
try
404+
{
405+
user_info_json = getObjectFromURI(userinfo_endpoint, token);
406+
username = getValueByKey(user_info_json, "sub");
407+
}
408+
catch (...)
409+
{
410+
return false;
411+
}
412+
}
365413

414+
if (user_info_json.empty())
415+
{
416+
LOG_TRACE(getLogger("AccessTokenProcessor"), "{}: Failed to obtain user info", name);
417+
return false;
366418
}
367-
catch (...)
419+
else if (username.empty())
368420
{
421+
LOG_TRACE(getLogger("AccessTokenProcessor"), "{}: Failed to get username", name);
369422
return false;
370423
}
371424

372-
return true;
425+
/// Credentials are passed as const everywhere up the flow, so we have to comply,
426+
/// in this case const_cast is acceptable.
427+
const_cast<TokenCredentials &>(credentials).setUserName(username);
373428

374-
/// TODO: add proper groups functionality
375-
// try
376-
// {
377-
// const_cast<TokenCredentials &>(credentials).setExpiresAt(jwt::decode(token).get_expires_at());
378-
// }
379-
// catch (...) {
380-
// LOG_TRACE(getLogger("AccessTokenProcessor"),
381-
// "{}: No expiration data found in a valid token, will use default cache lifetime", name);
382-
// }
383-
}
429+
/// For now, list of groups is expected in a claim with specified name either in token itself or in userinfo response (Keycloak works this way)
430+
/// TODO: add support for custom endpoints for retrieving groups. Keycloak lists groups in /userinfo and token itself, which is not always the case.
431+
if (!groups_claim_name.empty() && user_info_json.contains(groups_claim_name))
432+
{
433+
if (!user_info_json[groups_claim_name].is<picojson::array>())
434+
{
435+
LOG_TRACE(getLogger("AccessTokenProcessor"),
436+
"{}: Failed to extract groups: invalid content in user data", name);
437+
return true;
438+
}
384439

385-
String OpenIDAccessTokenProcessor::validateTokenAndGetUsername(const String & token) const
386-
{
387-
picojson::object user_info_json = getObjectFromURI(userinfo_endpoint, token);
388-
return getValueByKey(user_info_json, "sub");
440+
std::set<String> external_groups_names;
441+
442+
picojson::array groups_array = user_info_json[groups_claim_name].get<picojson::array>();
443+
for (const auto & group: groups_array)
444+
{
445+
if (group.is<std::string>())
446+
external_groups_names.insert(group.get<std::string>());
447+
}
448+
const_cast<TokenCredentials &>(credentials).setGroups(external_groups_names);
449+
}
450+
451+
return true;
389452
}
390453

391454
}

src/Access/AccessTokenProcessor.h

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,24 +106,28 @@ class OpenIDAccessTokenProcessor : public IAccessTokenProcessor
106106
OpenIDAccessTokenProcessor(const String & name_,
107107
const UInt64 cache_invalidation_interval_,
108108
const String & email_regex_str,
109-
const String & openid_config_endpoint_);
109+
const String & openid_config_endpoint_,
110+
const String & groups_claim_name_);
110111

111112
/// Specify endpoints manually
112113
OpenIDAccessTokenProcessor(const String & name_,
113114
const UInt64 cache_invalidation_interval_,
114115
const String & email_regex_str,
115116
const String & userinfo_endpoint_,
116-
const String & token_introspection_endpoint_)
117-
: IAccessTokenProcessor(name_, cache_invalidation_interval_, email_regex_str),
118-
userinfo_endpoint(userinfo_endpoint_), token_introspection_endpoint(token_introspection_endpoint_) {}
117+
const String & token_introspection_endpoint_,
118+
const String & jwks_uri_,
119+
const String & groups_claim_name_);
119120

120121
bool resolveAndValidate(const TokenCredentials & credentials) override;
121122
private:
122-
const Poco::URI userinfo_endpoint;
123-
const Poco::URI token_introspection_endpoint;
123+
Poco::URI userinfo_endpoint;
124+
Poco::URI token_introspection_endpoint;
124125

126+
/// Access token is often a valid JWT, so we can validate it locally to avoid unnecesary network requests.
127+
std::optional<JWKSValidator> jwt_validator = std::nullopt;
125128

126-
String validateTokenAndGetUsername(const String & token) const;
129+
/// groups are expected under /userinfo endpoint under specified name
130+
const String groups_claim_name;
127131
};
128132

129133
}

src/Access/JWTValidator.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ class JWKSValidator : public IJWTValidator
6161
public:
6262
explicit JWKSValidator(const String & name_, std::shared_ptr<IJWKSProvider> provider_)
6363
: IJWTValidator(name_), provider(provider_) {}
64+
65+
explicit JWKSValidator(const String & name_, const String & uri, const size_t refresh_ms_)
66+
: JWKSValidator(name_, std::make_shared<JWKSClient>(uri, refresh_ms_)) {}
6467
private:
6568
void validateImpl(const jwt::decoded_jwt<jwt::traits::kazuho_picojson> & token) const override;
6669

0 commit comments

Comments
 (0)