From 8dbc550fd3bb0f0609ffdd8fbfe9e9edd3eb082f Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 1 May 2020 17:03:57 +0300 Subject: [PATCH] Refactoring to review. Added tenantId and customerId. Added email tenant name strategy --- .../ThingsboardSecurityConfiguration.java | 4 +- ...r.java => AbstractOAuth2ClientMapper.java} | 42 ++++++--- .../auth/oauth2/BasicOAuth2ClientMapper.java | 18 ++-- .../auth/oauth2/CustomOAuth2ClientMapper.java | 20 ++++- .../src/main/resources/thingsboard.yml | 90 ++++++------------- .../server/dao/oauth2/OAuth2User.java | 4 + .../server/dao/oauth2/OAuth2Client.java | 2 - .../dao/oauth2/OAuth2ClientMapperConfig.java | 2 +- .../dao/oauth2/OAuth2Configuration.java | 12 +-- .../server/dao/oauth2/OAuth2ServiceImpl.java | 11 +-- 10 files changed, 109 insertions(+), 96 deletions(-) rename application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/{BaseOAuth2ClientMapper.java => AbstractOAuth2ClientMapper.java} (70%) diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 2798c6990f..9e967158b4 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -200,10 +200,10 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); - if (oauth2Configuration.isEnabled()) { + if (oauth2Configuration != null && oauth2Configuration.isEnabled()) { http.oauth2Login() .loginPage("/oauth2Login") - .loginProcessingUrl(oauth2Configuration.getClients().values().iterator().next().getLoginProcessingUrl()) + .loginProcessingUrl(oauth2Configuration.getLoginProcessingUrl()) .successHandler(oauth2AuthenticationSuccessHandler); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BaseOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java similarity index 70% rename from application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BaseOAuth2ClientMapper.java rename to application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java index 10334a8430..a5c53f6cff 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BaseOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java @@ -36,9 +36,11 @@ import org.thingsboard.server.service.security.model.UserPrincipal; import java.util.List; import java.util.Optional; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; @Slf4j -public abstract class BaseOAuth2ClientMapper { +public abstract class AbstractOAuth2ClientMapper { @Autowired private UserService userService; @@ -49,6 +51,8 @@ public abstract class BaseOAuth2ClientMapper { @Autowired private CustomerService customerService; + private final Lock userCreationLock = new ReentrantLock(); + protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation) { UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail()); @@ -59,18 +63,30 @@ public abstract class BaseOAuth2ClientMapper { } if (user == null) { - user = new User(); - if (StringUtils.isEmpty(oauth2User.getCustomerName())) { - user.setAuthority(Authority.TENANT_ADMIN); - } else { - user.setAuthority(Authority.CUSTOMER_USER); + userCreationLock.lock(); + try { + user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail()); + if (user == null) { + user = new User(); + if (oauth2User.getCustomerId() == null && StringUtils.isEmpty(oauth2User.getCustomerName())) { + user.setAuthority(Authority.TENANT_ADMIN); + } else { + user.setAuthority(Authority.CUSTOMER_USER); + } + TenantId tenantId = oauth2User.getTenantId() != null ? + oauth2User.getTenantId() : getTenantId(oauth2User.getTenantName()); + user.setTenantId(tenantId); + CustomerId customerId = oauth2User.getCustomerId() != null ? + oauth2User.getCustomerId() : getCustomerId(user.getTenantId(), oauth2User.getCustomerName()); + user.setCustomerId(customerId); + user.setEmail(oauth2User.getEmail()); + user.setFirstName(oauth2User.getFirstName()); + user.setLastName(oauth2User.getLastName()); + user = userService.saveUser(user); + } + } finally { + userCreationLock.unlock(); } - user.setTenantId(getTenantId(oauth2User.getTenantName())); - user.setCustomerId(getCustomerId(user.getTenantId(), oauth2User.getCustomerName())); - user.setEmail(oauth2User.getEmail()); - user.setFirstName(oauth2User.getFirstName()); - user.setLastName(oauth2User.getLastName()); - user = userService.saveUser(user); } try { @@ -78,7 +94,7 @@ public abstract class BaseOAuth2ClientMapper { return (SecurityUser) new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()).getPrincipal(); } catch (Exception e) { log.error("Can't get or create security user from oauth2 user", e); - throw e; + throw new RuntimeException("Can't get or create security user from oauth2 user", e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java index 2e87d57c72..935f7f5e3a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java @@ -28,7 +28,13 @@ import java.util.Map; @Service(value = "basicOAuth2ClientMapper") @Slf4j -public class BasicOAuth2ClientMapper extends BaseOAuth2ClientMapper implements OAuth2ClientMapper { +public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { + + private static final String START_PLACEHOLDER_PREFIX = "%{"; + private static final String END_PLACEHOLDER_PREFIX = "}"; + private static final String EMAIL_TENANT_STRATEGY = "email"; + private static final String DOMAIN_TENANT_STRATEGY = "domain"; + private static final String CUSTOM_TENANT_STRATEGY = "custom"; @Override public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) { @@ -46,7 +52,7 @@ public class BasicOAuth2ClientMapper extends BaseOAuth2ClientMapper implements O oauth2User.setFirstName(firstName); } if (!StringUtils.isEmpty(config.getBasic().getCustomerNameStrategyPattern())) { - StrSubstitutor sub = new StrSubstitutor(attributes, "${", "}"); + StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX); String customerName = sub.replace(config.getBasic().getCustomerNameStrategyPattern()); oauth2User.setCustomerName(customerName); } @@ -55,11 +61,13 @@ public class BasicOAuth2ClientMapper extends BaseOAuth2ClientMapper implements O private String getTenantName(Map attributes, OAuth2ClientMapperConfig config) { switch (config.getBasic().getTenantNameStrategy()) { - case "domain": + case EMAIL_TENANT_STRATEGY: + return getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); + case DOMAIN_TENANT_STRATEGY: String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); return email.substring(email .indexOf("@") + 1); - case "custom": - StrSubstitutor sub = new StrSubstitutor(attributes, "${", "}"); + case CUSTOM_TENANT_STRATEGY: + StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX); return sub.replace(config.getBasic().getTenantNameStrategyPattern()); default: throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!"); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java index cada6a7958..31e2c2bbec 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.service.security.auth.oauth2; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; @@ -27,7 +29,9 @@ import org.thingsboard.server.service.security.model.SecurityUser; @Service(value = "customOAuth2ClientMapper") @Slf4j -public class CustomOAuth2ClientMapper extends BaseOAuth2ClientMapper implements OAuth2ClientMapper { +public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { + + private static final ObjectMapper json = new ObjectMapper(); private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); @@ -42,6 +46,18 @@ public class CustomOAuth2ClientMapper extends BaseOAuth2ClientMapper implements restTemplateBuilder = restTemplateBuilder.basicAuthentication(custom.getUsername(), custom.getPassword()); } RestTemplate restTemplate = restTemplateBuilder.build(); - return restTemplate.postForEntity(custom.getUrl(), token.getPrincipal(), OAuth2User.class).getBody(); + String request; + try { + request = json.writeValueAsString(token.getPrincipal()); + } catch (JsonProcessingException e) { + log.error("Can't convert principal to JSON string", e); + throw new RuntimeException("Can't convert principal to JSON string", e); + } + try { + return restTemplate.postForEntity(custom.getUrl(), request, OAuth2User.class).getBody(); + } catch (Exception e) { + log.error("Can't connect to custom mapper endpoint", e); + throw new RuntimeException("Can't connect to custom mapper endpoint", e); + } } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index e2808bc8f6..1aada9dd9d 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -98,72 +98,40 @@ security: # Time allowed to claim the device in milliseconds duration: "${SECURITY_CLAIM_DURATION:60000}" # 1 minute, note this value must equal claimDevices.timeToLiveInMinutes value basic: - enabled: false + enabled: "${SECURITY_BASIC_ENABLED:false}" oauth2: - enabled: true + enabled: "${SECURITY_OAUTH2_ENABLED:false}" + loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}" clients: - schwarz: - registrationId: A - loginButtonLabel: Auth0 # - loginButtonIcon: - clientName: Test app - clientId: dVH9reqyqiXIG7M2wmamb0ySue8zaM4g - clientSecret: EYAfAGxwkwoeYnb2o2cDgaWZB5k97OStpZQPPvcMMD-SVH2BuughTGeBazXtF5I6 - accessTokenUri: https://dev-r9m8ht0k.auth0.com/oauth/token - authorizationUri: https://dev-r9m8ht0k.auth0.com/authorize - scope: openid,profile,email - redirectUriTemplate: http://localhost:8080/login/oauth2/code/ - loginProcessingUrl: /login/oauth2/code/ - jwkSetUri: https://dev-r9m8ht0k.auth0.com/.well-known/jwks.json - authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials - clientAuthenticationMethod: post # basic, post - userInfoUri: https://dev-r9m8ht0k.auth0.com/userinfo - userNameAttributeName: email + default: + loginButtonLabel: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_LABEL:Default}" # Label that going to be show on login screen + loginButtonIcon: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_ICON:}" # Icon that going to be show on login screen. Material design icon ID (https://material.angularjs.org/latest/api/directive/mdIcon) + clientName: "${SECURITY_OAUTH2_DEFAULT_CLIENT_NAME:ClientName}" + clientId: "${SECURITY_OAUTH2_DEFAULT_CLIENT_ID:}" + clientSecret: "${SECURITY_OAUTH2_DEFAULT_CLIENT_SECRET:}" + accessTokenUri: "${SECURITY_OAUTH2_DEFAULT_ACCESS_TOKEN_URI:}" + authorizationUri: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_URI:}" + scope: "${SECURITY_OAUTH2_DEFAULT_SCOPE:}" + redirectUriTemplate: "${SECURITY_OAUTH2_DEFAULT_REDIRECT_URI_TEMPLATE:http://localhost:8080/login/oauth2/code/}" # Must be in sync with security.oauth2.loginProcessingUrl + jwkSetUri: "${SECURITY_OAUTH2_DEFAULT_JWK_SET_URI:}" + authorizationGrantType: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_GRANT_TYPE:authorization_code}" # authorization_code, implicit, refresh_token or client_credentials + clientAuthenticationMethod: "${SECURITY_OAUTH2_DEFAULT_CLIENT_AUTHENTICATION_METHOD:post}" # basic or post + userInfoUri: "${SECURITY_OAUTH2_DEFAULT_USER_INFO_URI:}" + userNameAttributeName: "${SECURITY_OAUTH2_DEFAULT_USER_NAME_ATTRIBUTE_NAME:email}" mapperConfig: - type: custom # basic or custom + type: "${SECURITY_OAUTH2_DEFAULT_MAPPER_TYPE:basic}" # basic or custom basic: - allowUserCreation: true # required - emailAttributeKey: email # required - firstNameAttributeKey: - lastNameAttributeKey: - tenantNameStrategy: domain # domain or custom - tenantNameStrategyPattern: - customerNameStrategyPattern: + allowUserCreation: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_ALLOW_USER_CREATION:true}" # Allows to create user if it not exists + emailAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_EMAIL_ATTRIBUTE_KEY:email}" # Attribute key to use as email for the user + firstNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_FIRST_NAME_ATTRIBUTE_KEY:}" + lastNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_LAST_NAME_ATTRIBUTE_KEY:}" + tenantNameStrategy: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY:domain}" # domain, email or custom + tenantNameStrategyPattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY_PATTERN:}" + customerNameStrategyPattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_CUSTOMER_NAME_STRATEGY_PATTERN:}" custom: - url: http://localhost:9090/oauth2/mapper - username: admin - password: bababa - auth0: - registrationId: B - loginButtonLabel: Schwarz # - loginButtonIcon: mdi:google - clientName: Thingsboard Dev Test Q - clientId: 5f5c0998-1d9b-4679-9610-6108fb91af2a - clientSecret: h_kXVb7Ee1LgDDinix_nkAh_owWX7YCO783NNteF9AIOqlTWu2L03YoFjv5KL8yRVyx4uYAE-r_N3tFbupE8Kw - accessTokenUri: https://federation-q.auth.schwarz/nidp/oauth/nam/token - authorizationUri: https://federation-q.auth.schwarz/nidp/oauth/nam/authz - scope: openid,profile,email,siam - redirectUriTemplate: http://localhost:8080/login/oauth2/code/ - loginProcessingUrl: /login/oauth2/code/ - jwkSetUri: https://federation-q.auth.schwarz/nidp/oauth/nam/keys - authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials - clientAuthenticationMethod: post # basic, post - userInfoUri: https://federation-q.auth.schwarz/nidp/oauth/nam/userinfo - userNameAttributeName: mail - mapperConfig: - type: basic # simple or custom - basic: - allowUserCreation: true # required - emailAttributeKey: CloudLoginName # required - firstNameAttributeKey: givenName - lastNameAttributeKey: sn - tenantNameStrategy: custom # domain or custom - tenantNameStrategyPattern: LOL ${region} - customerNameStrategyPattern: GGG ${countrycode} - custom: - url: http://localhost:9090/oauth2/mapper - username: test - password: test + url: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_URL:}" + username: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_USERNAME:}" + password: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_PASSWORD:}" # Dashboard parameters dashboard: diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2User.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2User.java index 6337171369..c0075633e5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2User.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2User.java @@ -16,11 +16,15 @@ package org.thingsboard.server.dao.oauth2; import lombok.Data; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; @Data public class OAuth2User { private String tenantName; + private TenantId tenantId; private String customerName; + private CustomerId customerId; private String email; private String firstName; private String lastName; diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java index 1327e418cc..9676d55f5f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java @@ -20,7 +20,6 @@ import lombok.Data; @Data public class OAuth2Client { - private String registrationId; private String loginButtonLabel; private String loginButtonIcon; private String clientName; @@ -31,7 +30,6 @@ public class OAuth2Client { private String scope; private String redirectUriTemplate; private String jwkSetUri; - private String loginProcessingUrl; private String authorizationGrantType; private String clientAuthenticationMethod; private String userInfoUri; diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java index 2c8b7cbfc7..ec4f199549 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java @@ -21,8 +21,8 @@ import lombok.Data; public class OAuth2ClientMapperConfig { private String type; - private CustomOAuth2ClientMapperConfig custom; private BasicOAuth2ClientMapperConfig basic; + private CustomOAuth2ClientMapperConfig custom; @Data public static class BasicOAuth2ClientMapperConfig { diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java index fa51121d75..150ed85f9a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java @@ -40,13 +40,15 @@ import java.util.Map; public class OAuth2Configuration { private boolean enabled; + private String loginProcessingUrl; private Map clients = new HashMap<>(); @Bean public ClientRegistrationRepository clientRegistrationRepository() { List result = new ArrayList<>(); - for (OAuth2Client client : clients.values()) { - ClientRegistration registration = ClientRegistration.withRegistrationId(client.getRegistrationId()) + for (Map.Entry entry : clients.entrySet()) { + OAuth2Client client = entry.getValue(); + ClientRegistration registration = ClientRegistration.withRegistrationId(entry.getKey()) .clientId(client.getClientId()) .authorizationUri(client.getAuthorizationUri()) .clientSecret(client.getClientSecret()) @@ -68,9 +70,9 @@ public class OAuth2Configuration { public OAuth2Client getClientByRegistrationId(String registrationId) { OAuth2Client result = null; if (clients != null && !clients.isEmpty()) { - for (OAuth2Client client : clients.values()) { - if (client.getRegistrationId().equals(registrationId)) { - result = client; + for (String key : clients.keySet()) { + if (key.equals(registrationId)) { + result = clients.get(key); break; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java index 14aa390259..43e11244b0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; @Slf4j @Service @@ -33,15 +34,15 @@ public class OAuth2ServiceImpl implements OAuth2Service { @Override public List getOAuth2Clients() { - if (!oauth2Configuration.isEnabled()) { + if (oauth2Configuration == null || !oauth2Configuration.isEnabled()) { return Collections.emptyList(); } List result = new ArrayList<>(); - for (OAuth2Client c : oauth2Configuration.getClients().values()) { + for (Map.Entry entry : oauth2Configuration.getClients().entrySet()) { OAuth2ClientInfo client = new OAuth2ClientInfo(); - client.setName(c.getLoginButtonLabel()); - client.setUrl(String.format("/oauth2/authorization/%s", c.getRegistrationId())); - client.setIcon(c.getLoginButtonIcon()); + client.setName(entry.getValue().getLoginButtonLabel()); + client.setUrl(String.format("/oauth2/authorization/%s", entry.getKey())); + client.setIcon(entry.getValue().getLoginButtonIcon()); result.add(client); } return result;