Commit a563fdab3f1a1e37338190bbf933d55268551fec

Authored by Volodymyr Babak
Committed by Andrew Shvayka
1 parent 7fc46010

Added basic and custom OAuth2 user mappers

Showing 30 changed files with 916 additions and 259 deletions
1   -/**
2   - * Copyright © 2016-2020 The Thingsboard Authors
3   - *
4   - * Licensed under the Apache License, Version 2.0 (the "License");
5   - * you may not use this file except in compliance with the License.
6   - * You may obtain a copy of the License at
7   - *
8   - * http://www.apache.org/licenses/LICENSE-2.0
9   - *
10   - * Unless required by applicable law or agreed to in writing, software
11   - * distributed under the License is distributed on an "AS IS" BASIS,
12   - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   - * See the License for the specific language governing permissions and
14   - * limitations under the License.
15   - */
16   -package org.thingsboard.server.config;
17   -
18   -import org.springframework.beans.factory.annotation.Value;
19   -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
20   -import org.springframework.context.annotation.Bean;
21   -import org.springframework.context.annotation.Configuration;
22   -import org.springframework.security.oauth2.client.registration.ClientRegistration;
23   -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
24   -import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
25   -import org.springframework.security.oauth2.core.AuthorizationGrantType;
26   -import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
27   -
28   -import java.util.Collections;
29   -
30   -@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
31   -@Configuration
32   -public class ThingsboardOAuth2Configuration {
33   -
34   - @Value("${security.oauth2.registrationId}")
35   - private String registrationId;
36   - @Value("${security.oauth2.userNameAttributeName}")
37   - private String userNameAttributeName;
38   -
39   - @Value("${security.oauth2.client.clientId}")
40   - private String clientId;
41   - @Value("${security.oauth2.client.clientName}")
42   - private String clientName;
43   - @Value("${security.oauth2.client.clientSecret}")
44   - private String clientSecret;
45   - @Value("${security.oauth2.client.accessTokenUri}")
46   - private String accessTokenUri;
47   - @Value("${security.oauth2.client.authorizationUri}")
48   - private String authorizationUri;
49   - @Value("${security.oauth2.client.redirectUriTemplate}")
50   - private String redirectUriTemplate;
51   - @Value("${security.oauth2.client.scope}")
52   - private String scope;
53   - @Value("${security.oauth2.client.jwkSetUri}")
54   - private String jwkSetUri;
55   - @Value("${security.oauth2.client.authorizationGrantType}")
56   - private String authorizationGrantType;
57   - @Value("${security.oauth2.client.clientAuthenticationMethod}")
58   - private String clientAuthenticationMethod;
59   -
60   - @Value("${security.oauth2.resource.userInfoUri}")
61   - private String userInfoUri;
62   -
63   - @Bean
64   - public ClientRegistrationRepository clientRegistrationRepository() {
65   - ClientRegistration registration = ClientRegistration.withRegistrationId(registrationId)
66   - .clientId(clientId)
67   - .authorizationUri(authorizationUri)
68   - .clientSecret(clientSecret)
69   - .tokenUri(accessTokenUri)
70   - .redirectUriTemplate(redirectUriTemplate)
71   - .scope(scope.split(","))
72   - .clientName(clientName)
73   - .authorizationGrantType(new AuthorizationGrantType(authorizationGrantType))
74   - .userInfoUri(userInfoUri)
75   - .userNameAttributeName(userNameAttributeName)
76   - .jwkSetUri(jwkSetUri)
77   - .clientAuthenticationMethod(new ClientAuthenticationMethod(clientAuthenticationMethod))
78   - .build();
79   - return new InMemoryClientRegistrationRepository(Collections.singletonList(registration));
80   - }
81   -}
\ No newline at end of file
... ... @@ -18,8 +18,6 @@ package org.thingsboard.server.config;
18 18 import com.fasterxml.jackson.databind.ObjectMapper;
19 19 import org.springframework.beans.factory.annotation.Autowired;
20 20 import org.springframework.beans.factory.annotation.Qualifier;
21   -import org.springframework.beans.factory.annotation.Required;
22   -import org.springframework.beans.factory.annotation.Value;
23 21 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
24 22 import org.springframework.boot.autoconfigure.security.SecurityProperties;
25 23 import org.springframework.context.annotation.Bean;
... ... @@ -41,6 +39,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
41 39 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
42 40 import org.springframework.web.filter.CorsFilter;
43 41 import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
  42 +import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
44 43 import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
45 44 import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
46 45 import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter;
... ... @@ -89,10 +88,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
89 88 @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider;
90 89 @Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider;
91 90
92   - @Value("${security.oauth2.enabled}")
93   - private boolean oauth2Enabled;
94   - @Value("${security.oauth2.client.loginProcessingUrl}")
95   - private String loginProcessingUrl;
  91 + @Autowired(required = false) OAuth2Configuration oauth2Configuration;
96 92
97 93 @Autowired
98 94 @Qualifier("jwtHeaderTokenExtractor")
... ... @@ -204,10 +200,12 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
204 200 .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
205 201 .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
206 202 .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
207   - if (oauth2Enabled) {
  203 + if (oauth2Configuration.isEnabled()) {
208 204 http.oauth2Login()
209   - .loginProcessingUrl(loginProcessingUrl)
  205 + .loginPage("/oauth2Login")
  206 + .loginProcessingUrl(oauth2Configuration.getClients().values().iterator().next().getLoginProcessingUrl())
210 207 .successHandler(oauth2AuthenticationSuccessHandler);
  208 +// .and().oauth2Login().loginProcessingUrl();
211 209 }
212 210 }
213 211
... ...
... ... @@ -38,8 +38,10 @@ import org.thingsboard.server.common.data.audit.ActionType;
38 38 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
39 39 import org.thingsboard.server.common.data.exception.ThingsboardException;
40 40 import org.thingsboard.server.common.data.id.TenantId;
  41 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
41 42 import org.thingsboard.server.common.data.security.UserCredentials;
42 43 import org.thingsboard.server.dao.audit.AuditLogService;
  44 +import org.thingsboard.server.dao.oauth2.OAuth2Service;
43 45 import org.thingsboard.server.queue.util.TbCoreComponent;
44 46 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
45 47 import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
... ... @@ -55,6 +57,7 @@ import ua_parser.Client;
55 57 import javax.servlet.http.HttpServletRequest;
56 58 import java.net.URI;
57 59 import java.net.URISyntaxException;
  60 +import java.util.List;
58 61
59 62 @RestController
60 63 @TbCoreComponent
... ... @@ -80,6 +83,9 @@ public class AuthController extends BaseController {
80 83 @Autowired
81 84 private AuditLogService auditLogService;
82 85
  86 + @Autowired
  87 + private OAuth2Service oauth2Service;
  88 +
83 89 @PreAuthorize("isAuthenticated()")
84 90 @RequestMapping(value = "/auth/user", method = RequestMethod.GET)
85 91 public @ResponseBody User getUser() throws ThingsboardException {
... ... @@ -330,4 +336,13 @@ public class AuthController extends BaseController {
330 336 }
331 337 }
332 338
  339 + @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST)
  340 + @ResponseBody
  341 + public List<OAuth2ClientInfo> getOath2Clients() throws ThingsboardException {
  342 + try {
  343 + return oauth2Service.getOAuth2Clients();
  344 + } catch (Exception e) {
  345 + throw handleException(e);
  346 + }
  347 + }
333 348 }
... ...
1   -/**
2   - * Copyright © 2016-2020 The Thingsboard Authors
3   - *
4   - * Licensed under the Apache License, Version 2.0 (the "License");
5   - * you may not use this file except in compliance with the License.
6   - * You may obtain a copy of the License at
7   - *
8   - * http://www.apache.org/licenses/LICENSE-2.0
9   - *
10   - * Unless required by applicable law or agreed to in writing, software
11   - * distributed under the License is distributed on an "AS IS" BASIS,
12   - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   - * See the License for the specific language governing permissions and
14   - * limitations under the License.
15   - */
16   -package org.thingsboard.server.service.security.auth.oauth;
17   -
18   -import com.fasterxml.jackson.databind.ObjectMapper;
19   -import org.springframework.beans.factory.annotation.Autowired;
20   -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
21   -import org.springframework.security.authentication.InsufficientAuthenticationException;
22   -import org.springframework.security.authentication.LockedException;
23   -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
24   -import org.springframework.security.core.Authentication;
25   -import org.springframework.security.core.userdetails.UsernameNotFoundException;
26   -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
27   -import org.springframework.stereotype.Component;
28   -import org.thingsboard.server.common.data.User;
29   -import org.thingsboard.server.common.data.id.TenantId;
30   -import org.thingsboard.server.common.data.security.UserCredentials;
31   -import org.thingsboard.server.dao.user.UserService;
32   -import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
33   -import org.thingsboard.server.service.security.model.SecurityUser;
34   -import org.thingsboard.server.service.security.model.UserPrincipal;
35   -import org.thingsboard.server.service.security.model.token.JwtToken;
36   -import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
37   -import org.thingsboard.server.service.security.system.SystemSecurityService;
38   -
39   -import javax.servlet.ServletException;
40   -import javax.servlet.http.HttpServletRequest;
41   -import javax.servlet.http.HttpServletResponse;
42   -import java.io.IOException;
43   -import java.util.HashMap;
44   -import java.util.Map;
45   -
46   -@Component(value="oauth2AuthenticationSuccessHandler")
47   -@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
48   -public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
49   -
50   - private final ObjectMapper mapper;
51   - private final JwtTokenFactory tokenFactory;
52   - private final RefreshTokenRepository refreshTokenRepository;
53   - private final SystemSecurityService systemSecurityService;
54   - private final UserService userService;
55   -
56   - @Autowired
57   - public Oauth2AuthenticationSuccessHandler(final ObjectMapper mapper,
58   - final JwtTokenFactory tokenFactory,
59   - final RefreshTokenRepository refreshTokenRepository,
60   - final UserService userService,
61   - final SystemSecurityService systemSecurityService) {
62   - this.mapper = mapper;
63   - this.tokenFactory = tokenFactory;
64   - this.refreshTokenRepository = refreshTokenRepository;
65   - this.userService = userService;
66   - this.systemSecurityService = systemSecurityService;
67   - }
68   -
69   - @Override
70   - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
71   - Object object = authentication.getPrincipal();
72   -
73   - System.out.println(object);
74   -
75   - // active user check
76   -
77   - UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, "tenant@thingsboard.org");
78   - SecurityUser securityUser = (SecurityUser) authenticateByUsernameAndPassword(principal,"tenant@thingsboard.org", "tenant").getPrincipal();
79   -
80   - JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
81   - JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
82   -
83   - Map<String, String> tokenMap = new HashMap<String, String>();
84   - tokenMap.put("token", accessToken.getToken());
85   - tokenMap.put("refreshToken", refreshToken.getToken());
86   -
87   -// response.setStatus(HttpStatus.OK.value());
88   -// response.setContentType(MediaType.APPLICATION_JSON_VALUE);
89   -// mapper.writeValue(response.getWriter(), tokenMap);
90   -
91   - request.setAttribute("token", accessToken.getToken());
92   - response.addHeader("token", accessToken.getToken());
93   -
94   - getRedirectStrategy().sendRedirect(request, response, "http://localhost:4200/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken());
95   - }
96   -
97   - private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) {
98   - User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, username);
99   - if (user == null) {
100   - throw new UsernameNotFoundException("User not found: " + username);
101   - }
102   -
103   - try {
104   -
105   - UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId());
106   - if (userCredentials == null) {
107   - throw new UsernameNotFoundException("User credentials not found");
108   - }
109   -
110   - try {
111   - systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, username, password);
112   - } catch (LockedException e) {
113   - throw e;
114   - }
115   -
116   - if (user.getAuthority() == null)
117   - throw new InsufficientAuthenticationException("User has no authority assigned");
118   -
119   - SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
120   - return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
121   - } catch (Exception e) {
122   - throw e;
123   - }
124   - }
125   -}
\ No newline at end of file
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.beans.factory.annotation.Autowired;
  20 +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  21 +import org.springframework.security.core.userdetails.UsernameNotFoundException;
  22 +import org.springframework.util.StringUtils;
  23 +import org.thingsboard.server.common.data.Customer;
  24 +import org.thingsboard.server.common.data.Tenant;
  25 +import org.thingsboard.server.common.data.User;
  26 +import org.thingsboard.server.common.data.id.CustomerId;
  27 +import org.thingsboard.server.common.data.id.TenantId;
  28 +import org.thingsboard.server.common.data.page.TextPageLink;
  29 +import org.thingsboard.server.common.data.security.Authority;
  30 +import org.thingsboard.server.dao.customer.CustomerService;
  31 +import org.thingsboard.server.dao.oauth2.OAuth2User;
  32 +import org.thingsboard.server.dao.tenant.TenantService;
  33 +import org.thingsboard.server.dao.user.UserService;
  34 +import org.thingsboard.server.service.security.model.SecurityUser;
  35 +import org.thingsboard.server.service.security.model.UserPrincipal;
  36 +
  37 +import java.util.List;
  38 +import java.util.Optional;
  39 +
  40 +@Slf4j
  41 +public abstract class BaseOAuth2ClientMapper {
  42 +
  43 + @Autowired
  44 + private UserService userService;
  45 +
  46 + @Autowired
  47 + private TenantService tenantService;
  48 +
  49 + @Autowired
  50 + private CustomerService customerService;
  51 +
  52 + protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation) {
  53 + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail());
  54 +
  55 + User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail());
  56 +
  57 + if (user == null && !allowUserCreation) {
  58 + throw new UsernameNotFoundException("User not found: " + oauth2User.getEmail());
  59 + }
  60 +
  61 + 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);
  67 + }
  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 + }
  75 +
  76 + try {
  77 + SecurityUser securityUser = new SecurityUser(user, true, principal);
  78 + return (SecurityUser) new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()).getPrincipal();
  79 + } catch (Exception e) {
  80 + log.error("Can't get or create security user from oauth2 user", e);
  81 + throw e;
  82 + }
  83 + }
  84 +
  85 + private TenantId getTenantId(String tenantName) {
  86 + List<Tenant> tenants = tenantService.findTenants(new TextPageLink(1, tenantName)).getData();
  87 + Tenant tenant;
  88 + if (tenants == null || tenants.isEmpty()) {
  89 + tenant = new Tenant();
  90 + tenant.setTitle(tenantName);
  91 + tenant = tenantService.saveTenant(tenant);
  92 + } else {
  93 + tenant = tenants.get(0);
  94 + }
  95 + return tenant.getTenantId();
  96 + }
  97 +
  98 + private CustomerId getCustomerId(TenantId tenantId, String customerName) {
  99 + if (StringUtils.isEmpty(customerName)) {
  100 + return null;
  101 + }
  102 + Optional<Customer> customerOpt = customerService.findCustomerByTenantIdAndTitle(tenantId, customerName);
  103 + if (customerOpt.isPresent()) {
  104 + return customerOpt.get().getId();
  105 + } else {
  106 + Customer customer = new Customer();
  107 + customer.setTenantId(tenantId);
  108 + customer.setTitle(customerName);
  109 + return customerService.saveCustomer(customer).getId();
  110 + }
  111 + }
  112 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.apache.commons.lang3.text.StrSubstitutor;
  20 +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
  21 +import org.springframework.stereotype.Service;
  22 +import org.springframework.util.StringUtils;
  23 +import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig;
  24 +import org.thingsboard.server.dao.oauth2.OAuth2User;
  25 +import org.thingsboard.server.service.security.model.SecurityUser;
  26 +
  27 +import java.util.Map;
  28 +
  29 +@Service(value = "basicOAuth2ClientMapper")
  30 +@Slf4j
  31 +public class BasicOAuth2ClientMapper extends BaseOAuth2ClientMapper implements OAuth2ClientMapper {
  32 +
  33 + @Override
  34 + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) {
  35 + OAuth2User oauth2User = new OAuth2User();
  36 + Map<String, Object> attributes = token.getPrincipal().getAttributes();
  37 + String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
  38 + oauth2User.setEmail(email);
  39 + oauth2User.setTenantName(getTenantName(attributes, config));
  40 + if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) {
  41 + String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey());
  42 + oauth2User.setLastName(lastName);
  43 + }
  44 + if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) {
  45 + String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey());
  46 + oauth2User.setFirstName(firstName);
  47 + }
  48 + if (!StringUtils.isEmpty(config.getBasic().getCustomerNameStrategyPattern())) {
  49 + StrSubstitutor sub = new StrSubstitutor(attributes, "${", "}");
  50 + String customerName = sub.replace(config.getBasic().getCustomerNameStrategyPattern());
  51 + oauth2User.setCustomerName(customerName);
  52 + }
  53 + return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.getBasic().isAllowUserCreation());
  54 + }
  55 +
  56 + private String getTenantName(Map<String, Object> attributes, OAuth2ClientMapperConfig config) {
  57 + switch (config.getBasic().getTenantNameStrategy()) {
  58 + case "domain":
  59 + String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
  60 + return email.substring(email .indexOf("@") + 1);
  61 + case "custom":
  62 + StrSubstitutor sub = new StrSubstitutor(attributes, "${", "}");
  63 + return sub.replace(config.getBasic().getTenantNameStrategyPattern());
  64 + default:
  65 + throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!");
  66 + }
  67 + }
  68 +
  69 + private String getStringAttributeByKey(Map<String, Object> attributes, String key) {
  70 + String result = null;
  71 + try {
  72 + result = (String) attributes.get(key);
  73 +
  74 + } catch (Exception e) {
  75 + log.warn("Can't convert attribute to String by key " + key);
  76 + }
  77 + return result;
  78 + }
  79 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.boot.web.client.RestTemplateBuilder;
  20 +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
  21 +import org.springframework.stereotype.Service;
  22 +import org.springframework.util.StringUtils;
  23 +import org.springframework.web.client.RestTemplate;
  24 +import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig;
  25 +import org.thingsboard.server.dao.oauth2.OAuth2User;
  26 +import org.thingsboard.server.service.security.model.SecurityUser;
  27 +
  28 +@Service(value = "customOAuth2ClientMapper")
  29 +@Slf4j
  30 +public class CustomOAuth2ClientMapper extends BaseOAuth2ClientMapper implements OAuth2ClientMapper {
  31 +
  32 + private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
  33 +
  34 + @Override
  35 + public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) {
  36 + OAuth2User oauth2User = getOAuth2User(token, config.getCustom());
  37 + return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.getBasic().isAllowUserCreation());
  38 + }
  39 +
  40 + public OAuth2User getOAuth2User(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig.CustomOAuth2ClientMapperConfig custom) {
  41 + if (!StringUtils.isEmpty(custom.getUsername()) && !StringUtils.isEmpty(custom.getPassword())) {
  42 + restTemplateBuilder = restTemplateBuilder.basicAuthentication(custom.getUsername(), custom.getPassword());
  43 + }
  44 + RestTemplate restTemplate = restTemplateBuilder.build();
  45 + return restTemplate.postForEntity(custom.getUrl(), token.getPrincipal(), OAuth2User.class).getBody();
  46 + }
  47 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
  19 +import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig;
  20 +import org.thingsboard.server.service.security.model.SecurityUser;
  21 +
  22 +public interface OAuth2ClientMapper {
  23 + SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config);
  24 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.beans.factory.annotation.Autowired;
  20 +import org.springframework.beans.factory.annotation.Qualifier;
  21 +import org.springframework.stereotype.Component;
  22 +
  23 +@Component
  24 +@Slf4j
  25 +public class OAuth2ClientMapperProvider {
  26 +
  27 + @Autowired
  28 + @Qualifier("basicOAuth2ClientMapper")
  29 + private OAuth2ClientMapper basicOAuth2ClientMapper;
  30 +
  31 + @Autowired
  32 + @Qualifier("customOAuth2ClientMapper")
  33 + private OAuth2ClientMapper customOAuth2ClientMapper;
  34 +
  35 + public OAuth2ClientMapper getOAuth2ClientMapperByType(String oauth2ClientType) {
  36 + switch (oauth2ClientType) {
  37 + case "custom":
  38 + return customOAuth2ClientMapper;
  39 + case "basic":
  40 + return basicOAuth2ClientMapper;
  41 + default:
  42 + throw new RuntimeException("OAuth2ClientMapper with type " + oauth2ClientType + " is not supported!");
  43 + }
  44 + }
  45 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import org.springframework.beans.factory.annotation.Autowired;
  19 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  20 +import org.springframework.security.core.Authentication;
  21 +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
  22 +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
  23 +import org.springframework.stereotype.Component;
  24 +import org.thingsboard.server.dao.oauth2.OAuth2Client;
  25 +import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
  26 +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
  27 +import org.thingsboard.server.service.security.model.SecurityUser;
  28 +import org.thingsboard.server.service.security.model.token.JwtToken;
  29 +import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
  30 +
  31 +import javax.servlet.http.HttpServletRequest;
  32 +import javax.servlet.http.HttpServletResponse;
  33 +import java.io.IOException;
  34 +
  35 +@Component(value = "oauth2AuthenticationSuccessHandler")
  36 +@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
  37 +public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
  38 +
  39 + private final JwtTokenFactory tokenFactory;
  40 + private final RefreshTokenRepository refreshTokenRepository;
  41 + private final OAuth2ClientMapperProvider oauth2ClientMapperProvider;
  42 + private final OAuth2Configuration oauth2Configuration;
  43 +
  44 + @Autowired
  45 + public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory,
  46 + final RefreshTokenRepository refreshTokenRepository,
  47 + final OAuth2ClientMapperProvider oauth2ClientMapperProvider,
  48 + final OAuth2Configuration oauth2Configuration) {
  49 + this.tokenFactory = tokenFactory;
  50 + this.refreshTokenRepository = refreshTokenRepository;
  51 + this.oauth2ClientMapperProvider = oauth2ClientMapperProvider;
  52 + this.oauth2Configuration = oauth2Configuration;
  53 + }
  54 +
  55 + @Override
  56 + public void onAuthenticationSuccess(HttpServletRequest request,
  57 + HttpServletResponse response,
  58 + Authentication authentication) throws IOException {
  59 + OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
  60 +
  61 + OAuth2Client oauth2Client = oauth2Configuration.getClientByRegistrationId(token.getAuthorizedClientRegistrationId());
  62 + OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(oauth2Client.getMapperConfig().getType());
  63 + SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oauth2Client.getMapperConfig());
  64 +
  65 + JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
  66 + JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
  67 +
  68 + getRedirectStrategy().sendRedirect(request, response, "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken());
  69 + }
  70 +}
\ No newline at end of file
... ...
... ... @@ -36,7 +36,7 @@ import java.io.IOException;
36 36 import java.util.HashMap;
37 37 import java.util.Map;
38 38
39   -@Component(value="defaultAuthenticationSuccessHandler")
  39 +@Component(value = "defaultAuthenticationSuccessHandler")
40 40 public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
41 41 private final ObjectMapper mapper;
42 42 private final JwtTokenFactory tokenFactory;
... ...
... ... @@ -99,29 +99,13 @@ security:
99 99 duration: "${SECURITY_CLAIM_DURATION:60000}" # 1 minute, note this value must equal claimDevices.timeToLiveInMinutes value
100 100 basic:
101 101 enabled: false
102   - # oauth2:
103   - # enabled: true
104   - # registrationId: A
105   - # userNameAttributeName: email
106   - # client:
107   - # clientName: Thingsboard Dev Test Q
108   - # clientId: 5f5c0998-1d9b-4679-9610-6108fb91af2a
109   - # clientSecret: h_kXVb7Ee1LgDDinix_nkAh_owWX7YCO783NNteF9AIOqlTWu2L03YoFjv5KL8yRVyx4uYAE-r_N3tFbupE8Kw
110   - # accessTokenUri: https://federation-q.auth.schwarz/nidp/oauth/nam/token
111   - # authorizationUri: https://federation-q.auth.schwarz/nidp/oauth/nam/authz
112   - # scope: openid,profile,email,siam
113   - # redirectUriTemplate: http://localhost:8080/login/oauth2/code/
114   - # loginProcessingUrl: /login/oauth2/code/
115   - # jwkSetUri: https://federation-q.auth.schwarz/nidp/oauth/nam/keys
116   - # authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials
117   - # clientAuthenticationMethod: post # basic, post
118   - # resource:
119   - # userInfoUri: https://federation-q.auth.schwarz/nidp/oauth/nam/userinfo
120   - oauth2:
121   - enabled: true
122   - registrationId: A
123   - userNameAttributeName: email
124   - client:
  102 + oauth2:
  103 + enabled: true
  104 + clients:
  105 + schwarz:
  106 + registrationId: A
  107 + loginButtonLabel: Auth0 #
  108 + loginButtonIcon:
125 109 clientName: Test app
126 110 clientId: dVH9reqyqiXIG7M2wmamb0ySue8zaM4g
127 111 clientSecret: EYAfAGxwkwoeYnb2o2cDgaWZB5k97OStpZQPPvcMMD-SVH2BuughTGeBazXtF5I6
... ... @@ -133,8 +117,53 @@ security:
133 117 jwkSetUri: https://dev-r9m8ht0k.auth0.com/.well-known/jwks.json
134 118 authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials
135 119 clientAuthenticationMethod: post # basic, post
136   - resource:
137 120 userInfoUri: https://dev-r9m8ht0k.auth0.com/userinfo
  121 + userNameAttributeName: email
  122 + mapperConfig:
  123 + type: custom # basic or custom
  124 + basic:
  125 + allowUserCreation: true # required
  126 + emailAttributeKey: email # required
  127 + firstNameAttributeKey:
  128 + lastNameAttributeKey:
  129 + tenantNameStrategy: domain # domain or custom
  130 + tenantNameStrategyPattern:
  131 + customerNameStrategyPattern:
  132 + 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
138 167
139 168 # Dashboard parameters
140 169 dashboard:
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.oauth2;
  17 +
  18 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
  19 +
  20 +import java.util.List;
  21 +
  22 +public interface OAuth2Service {
  23 +
  24 + List<OAuth2ClientInfo> getOAuth2Clients();
  25 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.oauth2;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class OAuth2User {
  22 + private String tenantName;
  23 + private String customerName;
  24 + private String email;
  25 + private String firstName;
  26 + private String lastName;
  27 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.id;
  17 +
  18 +import com.fasterxml.jackson.annotation.JsonCreator;
  19 +import com.fasterxml.jackson.annotation.JsonProperty;
  20 +
  21 +import java.util.UUID;
  22 +
  23 +public class OAuth2IntegrationId extends UUIDBased {
  24 +
  25 + private static final long serialVersionUID = 1L;
  26 +
  27 + @JsonCreator
  28 + public OAuth2IntegrationId(@JsonProperty("id") UUID id) {
  29 + super(id);
  30 + }
  31 +
  32 + public static OAuth2IntegrationId fromString(String oauth2IntegrationId) {
  33 + return new OAuth2IntegrationId(UUID.fromString(oauth2IntegrationId));
  34 + }
  35 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.oauth2;
  17 +
  18 +import lombok.Data;
  19 +import lombok.EqualsAndHashCode;
  20 +import org.thingsboard.server.common.data.BaseData;
  21 +import org.thingsboard.server.common.data.id.OAuth2IntegrationId;
  22 +
  23 +@EqualsAndHashCode(callSuper = true)
  24 +@Data
  25 +public class OAuth2ClientInfo extends BaseData<OAuth2IntegrationId> {
  26 +
  27 + private String name;
  28 + private String icon;
  29 + private String url;
  30 +
  31 + public OAuth2ClientInfo() {
  32 + super();
  33 + }
  34 +
  35 + public OAuth2ClientInfo(OAuth2IntegrationId id) {
  36 + super(id);
  37 + }
  38 +
  39 + public OAuth2ClientInfo(OAuth2ClientInfo oauth2ClientInfo) {
  40 + super(oauth2ClientInfo);
  41 + this.name = oauth2ClientInfo.getName();
  42 + this.icon = oauth2ClientInfo.getIcon();
  43 + this.url = oauth2ClientInfo.getUrl();
  44 + }
  45 +}
... ...
... ... @@ -116,6 +116,10 @@
116 116 <artifactId>spring-web</artifactId>
117 117 <scope>provided</scope>
118 118 </dependency>
  119 + <dependency>
  120 + <groupId>org.springframework.security</groupId>
  121 + <artifactId>spring-security-oauth2-client</artifactId>
  122 + </dependency>
119 123 <dependency>
120 124 <groupId>com.datastax.cassandra</groupId>
121 125 <artifactId>cassandra-driver-core</artifactId>
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.oauth2;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class OAuth2Client {
  22 +
  23 + private String registrationId;
  24 + private String loginButtonLabel;
  25 + private String loginButtonIcon;
  26 + private String clientName;
  27 + private String clientId;
  28 + private String clientSecret;
  29 + private String accessTokenUri;
  30 + private String authorizationUri;
  31 + private String scope;
  32 + private String redirectUriTemplate;
  33 + private String jwkSetUri;
  34 + private String loginProcessingUrl;
  35 + private String authorizationGrantType;
  36 + private String clientAuthenticationMethod;
  37 + private String userInfoUri;
  38 + private String userNameAttributeName;
  39 + private OAuth2ClientMapperConfig mapperConfig;
  40 +
  41 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.oauth2;
  17 +
  18 +import lombok.Data;
  19 +
  20 +@Data
  21 +public class OAuth2ClientMapperConfig {
  22 +
  23 + private String type;
  24 + private CustomOAuth2ClientMapperConfig custom;
  25 + private BasicOAuth2ClientMapperConfig basic;
  26 +
  27 + @Data
  28 + public static class BasicOAuth2ClientMapperConfig {
  29 + private boolean allowUserCreation;
  30 + private String emailAttributeKey;
  31 + private String firstNameAttributeKey;
  32 + private String lastNameAttributeKey;
  33 + private String tenantNameStrategy;
  34 + private String tenantNameStrategyPattern;
  35 + private String customerNameStrategyPattern;
  36 + }
  37 +
  38 + @Data
  39 + public static class CustomOAuth2ClientMapperConfig {
  40 + private String url;
  41 + private String username;
  42 + private String password;
  43 + }
  44 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.oauth2;
  17 +
  18 +import lombok.Data;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  21 +import org.springframework.boot.context.properties.ConfigurationProperties;
  22 +import org.springframework.context.annotation.Bean;
  23 +import org.springframework.context.annotation.Configuration;
  24 +import org.springframework.security.oauth2.client.registration.ClientRegistration;
  25 +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
  26 +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
  27 +import org.springframework.security.oauth2.core.AuthorizationGrantType;
  28 +import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
  29 +
  30 +import java.util.ArrayList;
  31 +import java.util.HashMap;
  32 +import java.util.List;
  33 +import java.util.Map;
  34 +
  35 +@Configuration
  36 +@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true", matchIfMissing = true)
  37 +@ConfigurationProperties(prefix = "security.oauth2")
  38 +@Data
  39 +@Slf4j
  40 +public class OAuth2Configuration {
  41 +
  42 + private boolean enabled;
  43 + private Map<String, OAuth2Client> clients = new HashMap<>();
  44 +
  45 + @Bean
  46 + public ClientRegistrationRepository clientRegistrationRepository() {
  47 + List<ClientRegistration> result = new ArrayList<>();
  48 + for (OAuth2Client client : clients.values()) {
  49 + ClientRegistration registration = ClientRegistration.withRegistrationId(client.getRegistrationId())
  50 + .clientId(client.getClientId())
  51 + .authorizationUri(client.getAuthorizationUri())
  52 + .clientSecret(client.getClientSecret())
  53 + .tokenUri(client.getAccessTokenUri())
  54 + .redirectUriTemplate(client.getRedirectUriTemplate())
  55 + .scope(client.getScope().split(","))
  56 + .clientName(client.getClientName())
  57 + .authorizationGrantType(new AuthorizationGrantType(client.getAuthorizationGrantType()))
  58 + .userInfoUri(client.getUserInfoUri())
  59 + .userNameAttributeName(client.getUserNameAttributeName())
  60 + .jwkSetUri(client.getJwkSetUri())
  61 + .clientAuthenticationMethod(new ClientAuthenticationMethod(client.getClientAuthenticationMethod()))
  62 + .build();
  63 + result.add(registration);
  64 + }
  65 + return new InMemoryClientRegistrationRepository(result);
  66 + }
  67 +
  68 + public OAuth2Client getClientByRegistrationId(String registrationId) {
  69 + OAuth2Client result = null;
  70 + if (clients != null && !clients.isEmpty()) {
  71 + for (OAuth2Client client : clients.values()) {
  72 + if (client.getRegistrationId().equals(registrationId)) {
  73 + result = client;
  74 + break;
  75 + }
  76 + }
  77 + }
  78 + return result;
  79 + }
  80 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.oauth2;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.beans.factory.annotation.Autowired;
  20 +import org.springframework.stereotype.Service;
  21 +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
  22 +
  23 +import java.util.ArrayList;
  24 +import java.util.Collections;
  25 +import java.util.List;
  26 +
  27 +@Slf4j
  28 +@Service
  29 +public class OAuth2ServiceImpl implements OAuth2Service {
  30 +
  31 + @Autowired(required = false)
  32 + OAuth2Configuration oauth2Configuration;
  33 +
  34 + @Override
  35 + public List<OAuth2ClientInfo> getOAuth2Clients() {
  36 + if (!oauth2Configuration.isEnabled()) {
  37 + return Collections.emptyList();
  38 + }
  39 + List<OAuth2ClientInfo> result = new ArrayList<>();
  40 + for (OAuth2Client c : oauth2Configuration.getClients().values()) {
  41 + OAuth2ClientInfo client = new OAuth2ClientInfo();
  42 + client.setName(c.getLoginButtonLabel());
  43 + client.setUrl(String.format("/oauth2/authorization/%s", c.getRegistrationId()));
  44 + client.setIcon(c.getLoginButtonIcon());
  45 + result.add(client);
  46 + }
  47 + return result;
  48 + }
  49 +}
... ...
... ... @@ -31,6 +31,7 @@
31 31 <main.dir>${basedir}</main.dir>
32 32 <pkg.user>thingsboard</pkg.user>
33 33 <spring-boot.version>2.2.4.RELEASE</spring-boot.version>
  34 + <spring-oauth2.version>2.1.2.RELEASE</spring-oauth2.version>
34 35 <spring.version>5.2.2.RELEASE</spring.version>
35 36 <spring-security.version>5.2.2.RELEASE</spring-security.version>
36 37 <spring-data-redis.version>2.2.4.RELEASE</spring-data-redis.version>
... ... @@ -461,7 +462,7 @@
461 462 <dependency>
462 463 <groupId>org.springframework.cloud</groupId>
463 464 <artifactId>spring-cloud-starter-oauth2</artifactId>
464   - <version>${spring-boot.version}</version>
  465 + <version>${spring-oauth2.version}</version>
465 466 </dependency>
466 467 <dependency>
467 468 <groupId>org.springframework.security</groupId>
... ...
... ... @@ -18,7 +18,7 @@ export default angular.module('thingsboard.api.login', [])
18 18 .name;
19 19
20 20 /*@ngInject*/
21   -function LoginService($http, $q) {
  21 +function LoginService($http, $q, $rootScope) {
22 22
23 23 var service = {
24 24 activate: activate,
... ... @@ -28,6 +28,7 @@ function LoginService($http, $q) {
28 28 publicLogin: publicLogin,
29 29 resetPassword: resetPassword,
30 30 sendResetPasswordLink: sendResetPasswordLink,
  31 + loadOAuth2Clients: loadOAuth2Clients
31 32 }
32 33
33 34 return service;
... ... @@ -109,4 +110,16 @@ function LoginService($http, $q) {
109 110 });
110 111 return deferred.promise;
111 112 }
  113 +
  114 + function loadOAuth2Clients(){
  115 + var deferred = $q.defer();
  116 + var url = '/api/noauth/oauth2Clients';
  117 + $http.post(url).then(function success(response) {
  118 + $rootScope.oauth2Clients = response.data;
  119 + deferred.resolve();
  120 + }, function fail() {
  121 + deferred.reject();
  122 + });
  123 + return deferred.promise;
  124 + }
112 125 }
... ...
... ... @@ -17,7 +17,7 @@ import Flow from '@flowjs/ng-flow/dist/ng-flow-standalone.min';
17 17 import UrlHandler from './url.handler';
18 18
19 19 /*@ngInject*/
20   -export default function AppRun($rootScope, $window, $injector, $location, $log, $state, $mdDialog, $filter, loginService, userService, $translate) {
  20 +export default function AppRun($rootScope, $window, $injector, $location, $log, $state, $mdDialog, $filter, $q, loginService, userService, $translate) {
21 21
22 22 $window.Flow = Flow;
23 23 var frame = null;
... ... @@ -41,11 +41,13 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
41 41 }
42 42
43 43 initWatchers();
44   -
  44 +
  45 + var skipStateChange = false;
  46 +
45 47 function initWatchers() {
46 48 $rootScope.unauthenticatedHandle = $rootScope.$on('unauthenticated', function (event, doLogout) {
47 49 if (doLogout) {
48   - $state.go('login');
  50 + gotoPublicModule('login');
49 51 } else {
50 52 UrlHandler($injector, $location);
51 53 }
... ... @@ -61,6 +63,11 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
61 63
62 64 $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) {
63 65
  66 + if (skipStateChange) {
  67 + skipStateChange = false;
  68 + return;
  69 + }
  70 +
64 71 function waitForUserLoaded() {
65 72 if ($rootScope.userLoadedHandle) {
66 73 $rootScope.userLoadedHandle();
... ... @@ -128,7 +135,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
128 135 redirectParams.toName = to.name;
129 136 redirectParams.params = params;
130 137 userService.setRedirectParams(redirectParams);
131   - $state.go('login', params);
  138 + gotoPublicModule('login', params);
  139 + } else {
  140 + evt.preventDefault();
  141 + gotoPublicModule(to.name, params);
132 142 }
133 143 }
134 144 } else {
... ... @@ -158,6 +168,23 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
158 168 userService.gotoDefaultPlace(params);
159 169 }
160 170
  171 + function gotoPublicModule(name, params) {
  172 + let tasks = [];
  173 + if (name === "login") {
  174 + tasks.push(loginService.loadOAuth2Clients());
  175 + }
  176 + $q.all(tasks).then(
  177 + () => {
  178 + skipStateChange = true;
  179 + $state.go(name, params);
  180 + },
  181 + () => {
  182 + skipStateChange = true;
  183 + $state.go(name, params);
  184 + }
  185 + );
  186 + }
  187 +
161 188 function showForbiddenDialog() {
162 189 if (forbiddenDialog === null) {
163 190 $translate(['access.access-forbidden',
... ...
... ... @@ -1136,7 +1136,7 @@
1136 1136 "total": "celkem"
1137 1137 },
1138 1138 "login": {
1139   - "login": "Přihlásit",
  1139 + "login": "Přihlásit se",
1140 1140 "request-password-reset": "Vyžádat reset hesla",
1141 1141 "reset-password": "Reset hesla",
1142 1142 "create-password": "Vytvořit heslo",
... ... @@ -1150,7 +1150,9 @@
1150 1150 "new-password": "Nové heslo",
1151 1151 "new-password-again": "Nové heslo znovu",
1152 1152 "password-link-sent-message": "Odkaz pro reset hesla byl úspěšně odeslán!",
1153   - "email": "Email"
  1153 + "email": "Email",
  1154 + "login-with": "Přihlásit se přes {{name}}",
  1155 + "or": "nebo"
1154 1156 },
1155 1157 "position": {
1156 1158 "top": "Nahoře",
... ...
... ... @@ -1317,7 +1317,7 @@
1317 1317 }
1318 1318 },
1319 1319 "login": {
1320   - "login": "Login",
  1320 + "login": "Log in",
1321 1321 "request-password-reset": "Request Password Reset",
1322 1322 "reset-password": "Reset Password",
1323 1323 "create-password": "Create Password",
... ... @@ -1332,7 +1332,9 @@
1332 1332 "new-password": "New password",
1333 1333 "new-password-again": "New password again",
1334 1334 "password-link-sent-message": "Password reset link was successfully sent!",
1335   - "email": "Email"
  1335 + "email": "Email",
  1336 + "login-with": "Login with {{name}}",
  1337 + "or": "or"
1336 1338 },
1337 1339 "position": {
1338 1340 "top": "Top",
... ...
... ... @@ -1246,7 +1246,9 @@
1246 1246 "new-password": "Новый пароль",
1247 1247 "new-password-again": "Повторите новый пароль",
1248 1248 "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!",
1249   - "email": "Эл. адрес"
  1249 + "email": "Эл. адрес",
  1250 + "login-with": "Войти через {{name}}",
  1251 + "or": "или"
1250 1252 },
1251 1253 "position": {
1252 1254 "top": "Верх",
... ...
... ... @@ -1646,7 +1646,7 @@
1646 1646 }
1647 1647 },
1648 1648 "login": {
1649   - "login": "Вхід",
  1649 + "login": "Увійти",
1650 1650 "request-password-reset": "Запит скидання пароля",
1651 1651 "reset-password": "Скинути пароль",
1652 1652 "create-password": "Створити пароль",
... ... @@ -1661,7 +1661,9 @@
1661 1661 "new-password": "Новий пароль",
1662 1662 "new-password-again": "Повторіть новий пароль",
1663 1663 "password-link-sent-message": "Посилання для скидання пароля було успішно надіслано!",
1664   - "email": "Електронна пошта"
  1664 + "email": "Електронна пошта",
  1665 + "login-with": "Увійти через {{name}}",
  1666 + "or": "або"
1665 1667 },
1666 1668 "position": {
1667 1669 "top": "Угорі",
... ...
... ... @@ -22,6 +22,10 @@ md-card.tb-login-card {
22 22 width: 450px !important;
23 23 }
24 24
  25 + .tb-padding {
  26 + padding: 8px;
  27 + }
  28 +
25 29 md-card-title {
26 30 img.tb-login-logo {
27 31 height: 50px;
... ... @@ -31,4 +35,36 @@ md-card.tb-login-card {
31 35 md-card-content {
32 36 margin-top: -50px;
33 37 }
  38 +
  39 + md-input-container .md-errors-spacer {
  40 + display: none;
  41 + }
  42 +
  43 + .oauth-container{
  44 + .container-divider {
  45 + display: flex;
  46 + flex-direction: row;
  47 + align-items: center;
  48 + justify-content: center;
  49 + width: 100%;
  50 + margin: 10px 0;
  51 +
  52 + .line {
  53 + flex: 1;
  54 + }
  55 +
  56 + .text {
  57 + padding-right: 10px;
  58 + padding-left: 10px;
  59 + }
  60 + }
  61 +
  62 + .material-icons{
  63 + width: 20px;
  64 + min-width: 20px;
  65 + height: 20px;
  66 + min-height: 20px;
  67 + margin: 0 4px;
  68 + }
  69 + }
34 70 }
... ...
... ... @@ -24,7 +24,7 @@
24 24 md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
25 25 <md-card-content>
26 26 <form class="login-form" ng-submit="vm.login()">
27   - <div layout="column" layout-padding="" id="toast-parent">
  27 + <div layout="column" class="tb-padding" id="toast-parent">
28 28 <span style="height: 50px;"></span>
29 29 <md-input-container class="md-block">
30 30 <label translate>login.username</label>
... ... @@ -40,14 +40,23 @@
40 40 </md-icon>
41 41 <input id="password-input" type="password" ng-model="vm.user.password"/>
42 42 </md-input-container>
43   - <div layout-gt-sm="column" layout-align="space-between stretch">
44   - <div layout-gt-sm="column" layout-align="space-between end">
45   - <md-button ui-sref="login.resetPasswordRequest">{{ 'login.forgot-password' | translate }}
46   - </md-button>
47   - </div>
  43 + <div layout-gt-sm="column" layout-align="center end" class="tb-padding">
  44 + <md-button ui-sref="login.resetPasswordRequest">{{ 'login.forgot-password' | translate }}
  45 + </md-button>
48 46 </div>
49 47 <md-button class="md-raised" type="submit">{{ 'login.login' | translate }}</md-button>
50   - <a href="oauth2/authorization/A">OAUTH2 LOGIN</a>
  48 + <div class="oauth-container" layout="column" ng-if="oauth2Clients.length">
  49 + <div class="container-divider">
  50 + <div class="line"><md-divider></md-divider></div>
  51 + <div class="text mat-typography">{{ "login.or" | translate | uppercase }}</div>
  52 + <div class="line"><md-divider></md-divider></div>
  53 + </div>
  54 + <md-button ng-repeat="oauth2Client in oauth2Clients" class="md-raised"
  55 + layout="row" layout-align="center center" ng-href="{{ oauth2Client.url }}" target="_self">
  56 + <md-icon class="material-icons md-18" md-svg-icon="{{ oauth2Client.icon }}"></md-icon>
  57 + {{ 'login.login-with' | translate: {name: oauth2Client.name} }}
  58 + </md-button>
  59 + </div>
51 60 </div>
52 61 </form>
53 62 </md-card-content>
... ...