@@ -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+
338355OpenIDAccessTokenProcessor::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
350376bool 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}
0 commit comments