Commit 8dbc550fd3bb0f0609ffdd8fbfe9e9edd3eb082f

Authored by Volodymyr Babak
Committed by Andrew Shvayka
1 parent d644dddc

Refactoring to review. Added tenantId and customerId. Added email tenant name strategy

... ... @@ -200,10 +200,10 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
200 200 .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
201 201 .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
202 202 .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
203   - if (oauth2Configuration.isEnabled()) {
  203 + if (oauth2Configuration != null && oauth2Configuration.isEnabled()) {
204 204 http.oauth2Login()
205 205 .loginPage("/oauth2Login")
206   - .loginProcessingUrl(oauth2Configuration.getClients().values().iterator().next().getLoginProcessingUrl())
  206 + .loginProcessingUrl(oauth2Configuration.getLoginProcessingUrl())
207 207 .successHandler(oauth2AuthenticationSuccessHandler);
208 208 }
209 209 }
... ...
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java renamed from application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BaseOAuth2ClientMapper.java
... ... @@ -36,9 +36,11 @@ import org.thingsboard.server.service.security.model.UserPrincipal;
36 36
37 37 import java.util.List;
38 38 import java.util.Optional;
  39 +import java.util.concurrent.locks.Lock;
  40 +import java.util.concurrent.locks.ReentrantLock;
39 41
40 42 @Slf4j
41   -public abstract class BaseOAuth2ClientMapper {
  43 +public abstract class AbstractOAuth2ClientMapper {
42 44
43 45 @Autowired
44 46 private UserService userService;
... ... @@ -49,6 +51,8 @@ public abstract class BaseOAuth2ClientMapper {
49 51 @Autowired
50 52 private CustomerService customerService;
51 53
  54 + private final Lock userCreationLock = new ReentrantLock();
  55 +
52 56 protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation) {
53 57 UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail());
54 58
... ... @@ -59,18 +63,30 @@ public abstract class BaseOAuth2ClientMapper {
59 63 }
60 64
61 65 if (user == null) {
62   - user = new User();
63   - if (StringUtils.isEmpty(oauth2User.getCustomerName())) {
64   - user.setAuthority(Authority.TENANT_ADMIN);
65   - } else {
66   - user.setAuthority(Authority.CUSTOMER_USER);
  66 + userCreationLock.lock();
  67 + try {
  68 + user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail());
  69 + if (user == null) {
  70 + user = new User();
  71 + if (oauth2User.getCustomerId() == null && StringUtils.isEmpty(oauth2User.getCustomerName())) {
  72 + user.setAuthority(Authority.TENANT_ADMIN);
  73 + } else {
  74 + user.setAuthority(Authority.CUSTOMER_USER);
  75 + }
  76 + TenantId tenantId = oauth2User.getTenantId() != null ?
  77 + oauth2User.getTenantId() : getTenantId(oauth2User.getTenantName());
  78 + user.setTenantId(tenantId);
  79 + CustomerId customerId = oauth2User.getCustomerId() != null ?
  80 + oauth2User.getCustomerId() : getCustomerId(user.getTenantId(), oauth2User.getCustomerName());
  81 + user.setCustomerId(customerId);
  82 + user.setEmail(oauth2User.getEmail());
  83 + user.setFirstName(oauth2User.getFirstName());
  84 + user.setLastName(oauth2User.getLastName());
  85 + user = userService.saveUser(user);
  86 + }
  87 + } finally {
  88 + userCreationLock.unlock();
67 89 }
68   - user.setTenantId(getTenantId(oauth2User.getTenantName()));
69   - user.setCustomerId(getCustomerId(user.getTenantId(), oauth2User.getCustomerName()));
70   - user.setEmail(oauth2User.getEmail());
71   - user.setFirstName(oauth2User.getFirstName());
72   - user.setLastName(oauth2User.getLastName());
73   - user = userService.saveUser(user);
74 90 }
75 91
76 92 try {
... ... @@ -78,7 +94,7 @@ public abstract class BaseOAuth2ClientMapper {
78 94 return (SecurityUser) new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()).getPrincipal();
79 95 } catch (Exception e) {
80 96 log.error("Can't get or create security user from oauth2 user", e);
81   - throw e;
  97 + throw new RuntimeException("Can't get or create security user from oauth2 user", e);
82 98 }
83 99 }
84 100
... ...
... ... @@ -28,7 +28,13 @@ import java.util.Map;
28 28
29 29 @Service(value = "basicOAuth2ClientMapper")
30 30 @Slf4j
31   -public class BasicOAuth2ClientMapper extends BaseOAuth2ClientMapper implements OAuth2ClientMapper {
  31 +public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
  32 +
  33 + private static final String START_PLACEHOLDER_PREFIX = "%{";
  34 + private static final String END_PLACEHOLDER_PREFIX = "}";
  35 + private static final String EMAIL_TENANT_STRATEGY = "email";
  36 + private static final String DOMAIN_TENANT_STRATEGY = "domain";
  37 + private static final String CUSTOM_TENANT_STRATEGY = "custom";
32 38
33 39 @Override
34 40 public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) {
... ... @@ -46,7 +52,7 @@ public class BasicOAuth2ClientMapper extends BaseOAuth2ClientMapper implements O
46 52 oauth2User.setFirstName(firstName);
47 53 }
48 54 if (!StringUtils.isEmpty(config.getBasic().getCustomerNameStrategyPattern())) {
49   - StrSubstitutor sub = new StrSubstitutor(attributes, "${", "}");
  55 + StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
50 56 String customerName = sub.replace(config.getBasic().getCustomerNameStrategyPattern());
51 57 oauth2User.setCustomerName(customerName);
52 58 }
... ... @@ -55,11 +61,13 @@ public class BasicOAuth2ClientMapper extends BaseOAuth2ClientMapper implements O
55 61
56 62 private String getTenantName(Map<String, Object> attributes, OAuth2ClientMapperConfig config) {
57 63 switch (config.getBasic().getTenantNameStrategy()) {
58   - case "domain":
  64 + case EMAIL_TENANT_STRATEGY:
  65 + return getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
  66 + case DOMAIN_TENANT_STRATEGY:
59 67 String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
60 68 return email.substring(email .indexOf("@") + 1);
61   - case "custom":
62   - StrSubstitutor sub = new StrSubstitutor(attributes, "${", "}");
  69 + case CUSTOM_TENANT_STRATEGY:
  70 + StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
63 71 return sub.replace(config.getBasic().getTenantNameStrategyPattern());
64 72 default:
65 73 throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!");
... ...
... ... @@ -15,6 +15,8 @@
15 15 */
16 16 package org.thingsboard.server.service.security.auth.oauth2;
17 17
  18 +import com.fasterxml.jackson.core.JsonProcessingException;
  19 +import com.fasterxml.jackson.databind.ObjectMapper;
18 20 import lombok.extern.slf4j.Slf4j;
19 21 import org.springframework.boot.web.client.RestTemplateBuilder;
20 22 import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
... ... @@ -27,7 +29,9 @@ import org.thingsboard.server.service.security.model.SecurityUser;
27 29
28 30 @Service(value = "customOAuth2ClientMapper")
29 31 @Slf4j
30   -public class CustomOAuth2ClientMapper extends BaseOAuth2ClientMapper implements OAuth2ClientMapper {
  32 +public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
  33 +
  34 + private static final ObjectMapper json = new ObjectMapper();
31 35
32 36 private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
33 37
... ... @@ -42,6 +46,18 @@ public class CustomOAuth2ClientMapper extends BaseOAuth2ClientMapper implements
42 46 restTemplateBuilder = restTemplateBuilder.basicAuthentication(custom.getUsername(), custom.getPassword());
43 47 }
44 48 RestTemplate restTemplate = restTemplateBuilder.build();
45   - return restTemplate.postForEntity(custom.getUrl(), token.getPrincipal(), OAuth2User.class).getBody();
  49 + String request;
  50 + try {
  51 + request = json.writeValueAsString(token.getPrincipal());
  52 + } catch (JsonProcessingException e) {
  53 + log.error("Can't convert principal to JSON string", e);
  54 + throw new RuntimeException("Can't convert principal to JSON string", e);
  55 + }
  56 + try {
  57 + return restTemplate.postForEntity(custom.getUrl(), request, OAuth2User.class).getBody();
  58 + } catch (Exception e) {
  59 + log.error("Can't connect to custom mapper endpoint", e);
  60 + throw new RuntimeException("Can't connect to custom mapper endpoint", e);
  61 + }
46 62 }
47 63 }
... ...
... ... @@ -98,72 +98,40 @@ security:
98 98 # Time allowed to claim the device in milliseconds
99 99 duration: "${SECURITY_CLAIM_DURATION:60000}" # 1 minute, note this value must equal claimDevices.timeToLiveInMinutes value
100 100 basic:
101   - enabled: false
  101 + enabled: "${SECURITY_BASIC_ENABLED:false}"
102 102 oauth2:
103   - enabled: true
  103 + enabled: "${SECURITY_OAUTH2_ENABLED:false}"
  104 + loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
104 105 clients:
105   - schwarz:
106   - registrationId: A
107   - loginButtonLabel: Auth0 #
108   - loginButtonIcon:
109   - clientName: Test app
110   - clientId: dVH9reqyqiXIG7M2wmamb0ySue8zaM4g
111   - clientSecret: EYAfAGxwkwoeYnb2o2cDgaWZB5k97OStpZQPPvcMMD-SVH2BuughTGeBazXtF5I6
112   - accessTokenUri: https://dev-r9m8ht0k.auth0.com/oauth/token
113   - authorizationUri: https://dev-r9m8ht0k.auth0.com/authorize
114   - scope: openid,profile,email
115   - redirectUriTemplate: http://localhost:8080/login/oauth2/code/
116   - loginProcessingUrl: /login/oauth2/code/
117   - jwkSetUri: https://dev-r9m8ht0k.auth0.com/.well-known/jwks.json
118   - authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials
119   - clientAuthenticationMethod: post # basic, post
120   - userInfoUri: https://dev-r9m8ht0k.auth0.com/userinfo
121   - userNameAttributeName: email
  106 + default:
  107 + loginButtonLabel: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_LABEL:Default}" # Label that going to be show on login screen
  108 + 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)
  109 + clientName: "${SECURITY_OAUTH2_DEFAULT_CLIENT_NAME:ClientName}"
  110 + clientId: "${SECURITY_OAUTH2_DEFAULT_CLIENT_ID:}"
  111 + clientSecret: "${SECURITY_OAUTH2_DEFAULT_CLIENT_SECRET:}"
  112 + accessTokenUri: "${SECURITY_OAUTH2_DEFAULT_ACCESS_TOKEN_URI:}"
  113 + authorizationUri: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_URI:}"
  114 + scope: "${SECURITY_OAUTH2_DEFAULT_SCOPE:}"
  115 + redirectUriTemplate: "${SECURITY_OAUTH2_DEFAULT_REDIRECT_URI_TEMPLATE:http://localhost:8080/login/oauth2/code/}" # Must be in sync with security.oauth2.loginProcessingUrl
  116 + jwkSetUri: "${SECURITY_OAUTH2_DEFAULT_JWK_SET_URI:}"
  117 + authorizationGrantType: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_GRANT_TYPE:authorization_code}" # authorization_code, implicit, refresh_token or client_credentials
  118 + clientAuthenticationMethod: "${SECURITY_OAUTH2_DEFAULT_CLIENT_AUTHENTICATION_METHOD:post}" # basic or post
  119 + userInfoUri: "${SECURITY_OAUTH2_DEFAULT_USER_INFO_URI:}"
  120 + userNameAttributeName: "${SECURITY_OAUTH2_DEFAULT_USER_NAME_ATTRIBUTE_NAME:email}"
122 121 mapperConfig:
123   - type: custom # basic or custom
  122 + type: "${SECURITY_OAUTH2_DEFAULT_MAPPER_TYPE:basic}" # basic or custom
124 123 basic:
125   - allowUserCreation: true # required
126   - emailAttributeKey: email # required
127   - firstNameAttributeKey:
128   - lastNameAttributeKey:
129   - tenantNameStrategy: domain # domain or custom
130   - tenantNameStrategyPattern:
131   - customerNameStrategyPattern:
  124 + allowUserCreation: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_ALLOW_USER_CREATION:true}" # Allows to create user if it not exists
  125 + emailAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_EMAIL_ATTRIBUTE_KEY:email}" # Attribute key to use as email for the user
  126 + firstNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_FIRST_NAME_ATTRIBUTE_KEY:}"
  127 + lastNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_LAST_NAME_ATTRIBUTE_KEY:}"
  128 + tenantNameStrategy: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY:domain}" # domain, email or custom
  129 + tenantNameStrategyPattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY_PATTERN:}"
  130 + customerNameStrategyPattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_CUSTOMER_NAME_STRATEGY_PATTERN:}"
132 131 custom:
133   - url: http://localhost:9090/oauth2/mapper
134   - username: admin
135   - password: bababa
136   - auth0:
137   - registrationId: B
138   - loginButtonLabel: Schwarz #
139   - loginButtonIcon: mdi:google
140   - clientName: Thingsboard Dev Test Q
141   - clientId: 5f5c0998-1d9b-4679-9610-6108fb91af2a
142   - clientSecret: h_kXVb7Ee1LgDDinix_nkAh_owWX7YCO783NNteF9AIOqlTWu2L03YoFjv5KL8yRVyx4uYAE-r_N3tFbupE8Kw
143   - accessTokenUri: https://federation-q.auth.schwarz/nidp/oauth/nam/token
144   - authorizationUri: https://federation-q.auth.schwarz/nidp/oauth/nam/authz
145   - scope: openid,profile,email,siam
146   - redirectUriTemplate: http://localhost:8080/login/oauth2/code/
147   - loginProcessingUrl: /login/oauth2/code/
148   - jwkSetUri: https://federation-q.auth.schwarz/nidp/oauth/nam/keys
149   - authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials
150   - clientAuthenticationMethod: post # basic, post
151   - userInfoUri: https://federation-q.auth.schwarz/nidp/oauth/nam/userinfo
152   - userNameAttributeName: mail
153   - mapperConfig:
154   - type: basic # simple or custom
155   - basic:
156   - allowUserCreation: true # required
157   - emailAttributeKey: CloudLoginName # required
158   - firstNameAttributeKey: givenName
159   - lastNameAttributeKey: sn
160   - tenantNameStrategy: custom # domain or custom
161   - tenantNameStrategyPattern: LOL ${region}
162   - customerNameStrategyPattern: GGG ${countrycode}
163   - custom:
164   - url: http://localhost:9090/oauth2/mapper
165   - username: test
166   - password: test
  132 + url: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_URL:}"
  133 + username: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_USERNAME:}"
  134 + password: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_PASSWORD:}"
167 135
168 136 # Dashboard parameters
169 137 dashboard:
... ...
... ... @@ -16,11 +16,15 @@
16 16 package org.thingsboard.server.dao.oauth2;
17 17
18 18 import lombok.Data;
  19 +import org.thingsboard.server.common.data.id.CustomerId;
  20 +import org.thingsboard.server.common.data.id.TenantId;
19 21
20 22 @Data
21 23 public class OAuth2User {
22 24 private String tenantName;
  25 + private TenantId tenantId;
23 26 private String customerName;
  27 + private CustomerId customerId;
24 28 private String email;
25 29 private String firstName;
26 30 private String lastName;
... ...
... ... @@ -20,7 +20,6 @@ import lombok.Data;
20 20 @Data
21 21 public class OAuth2Client {
22 22
23   - private String registrationId;
24 23 private String loginButtonLabel;
25 24 private String loginButtonIcon;
26 25 private String clientName;
... ... @@ -31,7 +30,6 @@ public class OAuth2Client {
31 30 private String scope;
32 31 private String redirectUriTemplate;
33 32 private String jwkSetUri;
34   - private String loginProcessingUrl;
35 33 private String authorizationGrantType;
36 34 private String clientAuthenticationMethod;
37 35 private String userInfoUri;
... ...
... ... @@ -21,8 +21,8 @@ import lombok.Data;
21 21 public class OAuth2ClientMapperConfig {
22 22
23 23 private String type;
24   - private CustomOAuth2ClientMapperConfig custom;
25 24 private BasicOAuth2ClientMapperConfig basic;
  25 + private CustomOAuth2ClientMapperConfig custom;
26 26
27 27 @Data
28 28 public static class BasicOAuth2ClientMapperConfig {
... ...
... ... @@ -40,13 +40,15 @@ import java.util.Map;
40 40 public class OAuth2Configuration {
41 41
42 42 private boolean enabled;
  43 + private String loginProcessingUrl;
43 44 private Map<String, OAuth2Client> clients = new HashMap<>();
44 45
45 46 @Bean
46 47 public ClientRegistrationRepository clientRegistrationRepository() {
47 48 List<ClientRegistration> result = new ArrayList<>();
48   - for (OAuth2Client client : clients.values()) {
49   - ClientRegistration registration = ClientRegistration.withRegistrationId(client.getRegistrationId())
  49 + for (Map.Entry<String, OAuth2Client> entry : clients.entrySet()) {
  50 + OAuth2Client client = entry.getValue();
  51 + ClientRegistration registration = ClientRegistration.withRegistrationId(entry.getKey())
50 52 .clientId(client.getClientId())
51 53 .authorizationUri(client.getAuthorizationUri())
52 54 .clientSecret(client.getClientSecret())
... ... @@ -68,9 +70,9 @@ public class OAuth2Configuration {
68 70 public OAuth2Client getClientByRegistrationId(String registrationId) {
69 71 OAuth2Client result = null;
70 72 if (clients != null && !clients.isEmpty()) {
71   - for (OAuth2Client client : clients.values()) {
72   - if (client.getRegistrationId().equals(registrationId)) {
73   - result = client;
  73 + for (String key : clients.keySet()) {
  74 + if (key.equals(registrationId)) {
  75 + result = clients.get(key);
74 76 break;
75 77 }
76 78 }
... ...
... ... @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
23 23 import java.util.ArrayList;
24 24 import java.util.Collections;
25 25 import java.util.List;
  26 +import java.util.Map;
26 27
27 28 @Slf4j
28 29 @Service
... ... @@ -33,15 +34,15 @@ public class OAuth2ServiceImpl implements OAuth2Service {
33 34
34 35 @Override
35 36 public List<OAuth2ClientInfo> getOAuth2Clients() {
36   - if (!oauth2Configuration.isEnabled()) {
  37 + if (oauth2Configuration == null || !oauth2Configuration.isEnabled()) {
37 38 return Collections.emptyList();
38 39 }
39 40 List<OAuth2ClientInfo> result = new ArrayList<>();
40   - for (OAuth2Client c : oauth2Configuration.getClients().values()) {
  41 + for (Map.Entry<String, OAuth2Client> entry : oauth2Configuration.getClients().entrySet()) {
41 42 OAuth2ClientInfo client = new OAuth2ClientInfo();
42   - client.setName(c.getLoginButtonLabel());
43   - client.setUrl(String.format("/oauth2/authorization/%s", c.getRegistrationId()));
44   - client.setIcon(c.getLoginButtonIcon());
  43 + client.setName(entry.getValue().getLoginButtonLabel());
  44 + client.setUrl(String.format("/oauth2/authorization/%s", entry.getKey()));
  45 + client.setIcon(entry.getValue().getLoginButtonIcon());
45 46 result.add(client);
46 47 }
47 48 return result;
... ...