Browse Source

Added basic and custom OAuth2 user mappers

pull/2717/head
Volodymyr Babak 7 years ago
committed by Andrew Shvayka
parent
commit
a563fdab3f
  1. 81
      application/src/main/java/org/thingsboard/server/config/ThingsboardOAuth2Configuration.java
  2. 14
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  3. 15
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  4. 125
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth/Oauth2AuthenticationSuccessHandler.java
  5. 112
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BaseOAuth2ClientMapper.java
  6. 79
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java
  7. 47
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  8. 24
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java
  9. 45
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java
  10. 70
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  11. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  12. 77
      application/src/main/resources/thingsboard.yml
  13. 25
      common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java
  14. 27
      common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2User.java
  15. 35
      common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2IntegrationId.java
  16. 45
      common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java
  17. 4
      dao/pom.xml
  18. 41
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java
  19. 44
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java
  20. 80
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java
  21. 49
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java
  22. 3
      pom.xml
  23. 15
      ui/src/app/api/login.service.js
  24. 35
      ui/src/app/app.run.js
  25. 6
      ui/src/app/locale/locale.constant-cs_CZ.json
  26. 6
      ui/src/app/locale/locale.constant-en_US.json
  27. 4
      ui/src/app/locale/locale.constant-ru_RU.json
  28. 6
      ui/src/app/locale/locale.constant-uk_UA.json
  29. 36
      ui/src/app/login/login.scss
  30. 23
      ui/src/app/login/login.tpl.html

81
application/src/main/java/org/thingsboard/server/config/ThingsboardOAuth2Configuration.java

@ -1,81 +0,0 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import java.util.Collections;
@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
@Configuration
public class ThingsboardOAuth2Configuration {
@Value("${security.oauth2.registrationId}")
private String registrationId;
@Value("${security.oauth2.userNameAttributeName}")
private String userNameAttributeName;
@Value("${security.oauth2.client.clientId}")
private String clientId;
@Value("${security.oauth2.client.clientName}")
private String clientName;
@Value("${security.oauth2.client.clientSecret}")
private String clientSecret;
@Value("${security.oauth2.client.accessTokenUri}")
private String accessTokenUri;
@Value("${security.oauth2.client.authorizationUri}")
private String authorizationUri;
@Value("${security.oauth2.client.redirectUriTemplate}")
private String redirectUriTemplate;
@Value("${security.oauth2.client.scope}")
private String scope;
@Value("${security.oauth2.client.jwkSetUri}")
private String jwkSetUri;
@Value("${security.oauth2.client.authorizationGrantType}")
private String authorizationGrantType;
@Value("${security.oauth2.client.clientAuthenticationMethod}")
private String clientAuthenticationMethod;
@Value("${security.oauth2.resource.userInfoUri}")
private String userInfoUri;
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
ClientRegistration registration = ClientRegistration.withRegistrationId(registrationId)
.clientId(clientId)
.authorizationUri(authorizationUri)
.clientSecret(clientSecret)
.tokenUri(accessTokenUri)
.redirectUriTemplate(redirectUriTemplate)
.scope(scope.split(","))
.clientName(clientName)
.authorizationGrantType(new AuthorizationGrantType(authorizationGrantType))
.userInfoUri(userInfoUri)
.userNameAttributeName(userNameAttributeName)
.jwkSetUri(jwkSetUri)
.clientAuthenticationMethod(new ClientAuthenticationMethod(clientAuthenticationMethod))
.build();
return new InMemoryClientRegistrationRepository(Collections.singletonList(registration));
}
}

14
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -18,8 +18,6 @@ package org.thingsboard.server.config;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -41,6 +39,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter; import org.springframework.web.filter.CorsFilter;
import org.thingsboard.server.dao.audit.AuditLogLevelFilter; import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter; import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter;
@ -89,10 +88,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
@Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider;
@Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider; @Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider;
@Value("${security.oauth2.enabled}") @Autowired(required = false) OAuth2Configuration oauth2Configuration;
private boolean oauth2Enabled;
@Value("${security.oauth2.client.loginProcessingUrl}")
private String loginProcessingUrl;
@Autowired @Autowired
@Qualifier("jwtHeaderTokenExtractor") @Qualifier("jwtHeaderTokenExtractor")
@ -204,10 +200,12 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
if (oauth2Enabled) { if (oauth2Configuration.isEnabled()) {
http.oauth2Login() http.oauth2Login()
.loginProcessingUrl(loginProcessingUrl) .loginPage("/oauth2Login")
.loginProcessingUrl(oauth2Configuration.getClients().values().iterator().next().getLoginProcessingUrl())
.successHandler(oauth2AuthenticationSuccessHandler); .successHandler(oauth2AuthenticationSuccessHandler);
// .and().oauth2Login().loginProcessingUrl();
} }
} }

15
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -38,8 +38,10 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
@ -55,6 +57,7 @@ import ua_parser.Client;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.List;
@RestController @RestController
@TbCoreComponent @TbCoreComponent
@ -80,6 +83,9 @@ public class AuthController extends BaseController {
@Autowired @Autowired
private AuditLogService auditLogService; private AuditLogService auditLogService;
@Autowired
private OAuth2Service oauth2Service;
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
@RequestMapping(value = "/auth/user", method = RequestMethod.GET) @RequestMapping(value = "/auth/user", method = RequestMethod.GET)
public @ResponseBody User getUser() throws ThingsboardException { public @ResponseBody User getUser() throws ThingsboardException {
@ -330,4 +336,13 @@ public class AuthController extends BaseController {
} }
} }
@RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST)
@ResponseBody
public List<OAuth2ClientInfo> getOath2Clients() throws ThingsboardException {
try {
return oauth2Service.getOAuth2Clients();
} catch (Exception e) {
throw handleException(e);
}
}
} }

125
application/src/main/java/org/thingsboard/server/service/security/auth/oauth/Oauth2AuthenticationSuccessHandler.java

@ -1,125 +0,0 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.oauth;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component(value="oauth2AuthenticationSuccessHandler")
@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final ObjectMapper mapper;
private final JwtTokenFactory tokenFactory;
private final RefreshTokenRepository refreshTokenRepository;
private final SystemSecurityService systemSecurityService;
private final UserService userService;
@Autowired
public Oauth2AuthenticationSuccessHandler(final ObjectMapper mapper,
final JwtTokenFactory tokenFactory,
final RefreshTokenRepository refreshTokenRepository,
final UserService userService,
final SystemSecurityService systemSecurityService) {
this.mapper = mapper;
this.tokenFactory = tokenFactory;
this.refreshTokenRepository = refreshTokenRepository;
this.userService = userService;
this.systemSecurityService = systemSecurityService;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Object object = authentication.getPrincipal();
System.out.println(object);
// active user check
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, "tenant@thingsboard.org");
SecurityUser securityUser = (SecurityUser) authenticateByUsernameAndPassword(principal,"tenant@thingsboard.org", "tenant").getPrincipal();
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
Map<String, String> tokenMap = new HashMap<String, String>();
tokenMap.put("token", accessToken.getToken());
tokenMap.put("refreshToken", refreshToken.getToken());
// response.setStatus(HttpStatus.OK.value());
// response.setContentType(MediaType.APPLICATION_JSON_VALUE);
// mapper.writeValue(response.getWriter(), tokenMap);
request.setAttribute("token", accessToken.getToken());
response.addHeader("token", accessToken.getToken());
getRedirectStrategy().sendRedirect(request, response, "http://localhost:4200/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken());
}
private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) {
User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, username);
if (user == null) {
throw new UsernameNotFoundException("User not found: " + username);
}
try {
UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId());
if (userCredentials == null) {
throw new UsernameNotFoundException("User credentials not found");
}
try {
systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, username, password);
} catch (LockedException e) {
throw e;
}
if (user.getAuthority() == null)
throw new InsufficientAuthenticationException("User has no authority assigned");
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
} catch (Exception e) {
throw e;
}
}
}

112
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BaseOAuth2ClientMapper.java

@ -0,0 +1,112 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.oauth2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import java.util.List;
import java.util.Optional;
@Slf4j
public abstract class BaseOAuth2ClientMapper {
@Autowired
private UserService userService;
@Autowired
private TenantService tenantService;
@Autowired
private CustomerService customerService;
protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation) {
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail());
User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail());
if (user == null && !allowUserCreation) {
throw new UsernameNotFoundException("User not found: " + oauth2User.getEmail());
}
if (user == null) {
user = new User();
if (StringUtils.isEmpty(oauth2User.getCustomerName())) {
user.setAuthority(Authority.TENANT_ADMIN);
} else {
user.setAuthority(Authority.CUSTOMER_USER);
}
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 {
SecurityUser securityUser = new SecurityUser(user, true, principal);
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;
}
}
private TenantId getTenantId(String tenantName) {
List<Tenant> tenants = tenantService.findTenants(new TextPageLink(1, tenantName)).getData();
Tenant tenant;
if (tenants == null || tenants.isEmpty()) {
tenant = new Tenant();
tenant.setTitle(tenantName);
tenant = tenantService.saveTenant(tenant);
} else {
tenant = tenants.get(0);
}
return tenant.getTenantId();
}
private CustomerId getCustomerId(TenantId tenantId, String customerName) {
if (StringUtils.isEmpty(customerName)) {
return null;
}
Optional<Customer> customerOpt = customerService.findCustomerByTenantIdAndTitle(tenantId, customerName);
if (customerOpt.isPresent()) {
return customerOpt.get().getId();
} else {
Customer customer = new Customer();
customer.setTenantId(tenantId);
customer.setTitle(customerName);
return customerService.saveCustomer(customer).getId();
}
}
}

79
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java

@ -0,0 +1,79 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.oauth2;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.text.StrSubstitutor;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Map;
@Service(value = "basicOAuth2ClientMapper")
@Slf4j
public class BasicOAuth2ClientMapper extends BaseOAuth2ClientMapper implements OAuth2ClientMapper {
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) {
OAuth2User oauth2User = new OAuth2User();
Map<String, Object> attributes = token.getPrincipal().getAttributes();
String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
oauth2User.setEmail(email);
oauth2User.setTenantName(getTenantName(attributes, config));
if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) {
String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey());
oauth2User.setLastName(lastName);
}
if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) {
String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey());
oauth2User.setFirstName(firstName);
}
if (!StringUtils.isEmpty(config.getBasic().getCustomerNameStrategyPattern())) {
StrSubstitutor sub = new StrSubstitutor(attributes, "${", "}");
String customerName = sub.replace(config.getBasic().getCustomerNameStrategyPattern());
oauth2User.setCustomerName(customerName);
}
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.getBasic().isAllowUserCreation());
}
private String getTenantName(Map<String, Object> attributes, OAuth2ClientMapperConfig config) {
switch (config.getBasic().getTenantNameStrategy()) {
case "domain":
String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
return email.substring(email .indexOf("@") + 1);
case "custom":
StrSubstitutor sub = new StrSubstitutor(attributes, "${", "}");
return sub.replace(config.getBasic().getTenantNameStrategyPattern());
default:
throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!");
}
}
private String getStringAttributeByKey(Map<String, Object> attributes, String key) {
String result = null;
try {
result = (String) attributes.get(key);
} catch (Exception e) {
log.warn("Can't convert attribute to String by key " + key);
}
return result;
}
}

47
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java

@ -0,0 +1,47 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.oauth2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser;
@Service(value = "customOAuth2ClientMapper")
@Slf4j
public class CustomOAuth2ClientMapper extends BaseOAuth2ClientMapper implements OAuth2ClientMapper {
private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) {
OAuth2User oauth2User = getOAuth2User(token, config.getCustom());
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.getBasic().isAllowUserCreation());
}
public OAuth2User getOAuth2User(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig.CustomOAuth2ClientMapperConfig custom) {
if (!StringUtils.isEmpty(custom.getUsername()) && !StringUtils.isEmpty(custom.getPassword())) {
restTemplateBuilder = restTemplateBuilder.basicAuthentication(custom.getUsername(), custom.getPassword());
}
RestTemplate restTemplate = restTemplateBuilder.build();
return restTemplate.postForEntity(custom.getUrl(), token.getPrincipal(), OAuth2User.class).getBody();
}
}

24
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.oauth2;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.thingsboard.server.dao.oauth2.OAuth2ClientMapperConfig;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface OAuth2ClientMapper {
SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config);
}

45
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.oauth2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class OAuth2ClientMapperProvider {
@Autowired
@Qualifier("basicOAuth2ClientMapper")
private OAuth2ClientMapper basicOAuth2ClientMapper;
@Autowired
@Qualifier("customOAuth2ClientMapper")
private OAuth2ClientMapper customOAuth2ClientMapper;
public OAuth2ClientMapper getOAuth2ClientMapperByType(String oauth2ClientType) {
switch (oauth2ClientType) {
case "custom":
return customOAuth2ClientMapper;
case "basic":
return basicOAuth2ClientMapper;
default:
throw new RuntimeException("OAuth2ClientMapper with type " + oauth2ClientType + " is not supported!");
}
}
}

70
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java

@ -0,0 +1,70 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.auth.oauth2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.thingsboard.server.dao.oauth2.OAuth2Client;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component(value = "oauth2AuthenticationSuccessHandler")
@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenFactory tokenFactory;
private final RefreshTokenRepository refreshTokenRepository;
private final OAuth2ClientMapperProvider oauth2ClientMapperProvider;
private final OAuth2Configuration oauth2Configuration;
@Autowired
public Oauth2AuthenticationSuccessHandler(final JwtTokenFactory tokenFactory,
final RefreshTokenRepository refreshTokenRepository,
final OAuth2ClientMapperProvider oauth2ClientMapperProvider,
final OAuth2Configuration oauth2Configuration) {
this.tokenFactory = tokenFactory;
this.refreshTokenRepository = refreshTokenRepository;
this.oauth2ClientMapperProvider = oauth2ClientMapperProvider;
this.oauth2Configuration = oauth2Configuration;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
OAuth2Client oauth2Client = oauth2Configuration.getClientByRegistrationId(token.getAuthorizedClientRegistrationId());
OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(oauth2Client.getMapperConfig().getType());
SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oauth2Client.getMapperConfig());
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
getRedirectStrategy().sendRedirect(request, response, "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken());
}
}

2
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java

@ -36,7 +36,7 @@ import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Component(value="defaultAuthenticationSuccessHandler") @Component(value = "defaultAuthenticationSuccessHandler")
public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectMapper mapper; private final ObjectMapper mapper;
private final JwtTokenFactory tokenFactory; private final JwtTokenFactory tokenFactory;

77
application/src/main/resources/thingsboard.yml

@ -99,29 +99,13 @@ security:
duration: "${SECURITY_CLAIM_DURATION:60000}" # 1 minute, note this value must equal claimDevices.timeToLiveInMinutes value duration: "${SECURITY_CLAIM_DURATION:60000}" # 1 minute, note this value must equal claimDevices.timeToLiveInMinutes value
basic: basic:
enabled: false enabled: false
# oauth2: oauth2:
# enabled: true enabled: true
# registrationId: A clients:
# userNameAttributeName: email schwarz:
# client: registrationId: A
# clientName: Thingsboard Dev Test Q loginButtonLabel: Auth0 #
# clientId: 5f5c0998-1d9b-4679-9610-6108fb91af2a loginButtonIcon:
# 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
# resource:
# userInfoUri: https://federation-q.auth.schwarz/nidp/oauth/nam/userinfo
oauth2:
enabled: true
registrationId: A
userNameAttributeName: email
client:
clientName: Test app clientName: Test app
clientId: dVH9reqyqiXIG7M2wmamb0ySue8zaM4g clientId: dVH9reqyqiXIG7M2wmamb0ySue8zaM4g
clientSecret: EYAfAGxwkwoeYnb2o2cDgaWZB5k97OStpZQPPvcMMD-SVH2BuughTGeBazXtF5I6 clientSecret: EYAfAGxwkwoeYnb2o2cDgaWZB5k97OStpZQPPvcMMD-SVH2BuughTGeBazXtF5I6
@ -133,8 +117,53 @@ security:
jwkSetUri: https://dev-r9m8ht0k.auth0.com/.well-known/jwks.json jwkSetUri: https://dev-r9m8ht0k.auth0.com/.well-known/jwks.json
authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials
clientAuthenticationMethod: post # basic, post clientAuthenticationMethod: post # basic, post
resource:
userInfoUri: https://dev-r9m8ht0k.auth0.com/userinfo userInfoUri: https://dev-r9m8ht0k.auth0.com/userinfo
userNameAttributeName: email
mapperConfig:
type: custom # basic or custom
basic:
allowUserCreation: true # required
emailAttributeKey: email # required
firstNameAttributeKey:
lastNameAttributeKey:
tenantNameStrategy: domain # domain or custom
tenantNameStrategyPattern:
customerNameStrategyPattern:
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
# Dashboard parameters # Dashboard parameters
dashboard: dashboard:

25
common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.oauth2;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import java.util.List;
public interface OAuth2Service {
List<OAuth2ClientInfo> getOAuth2Clients();
}

27
common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2User.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.oauth2;
import lombok.Data;
@Data
public class OAuth2User {
private String tenantName;
private String customerName;
private String email;
private String firstName;
private String lastName;
}

35
common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2IntegrationId.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.UUID;
public class OAuth2IntegrationId extends UUIDBased {
private static final long serialVersionUID = 1L;
@JsonCreator
public OAuth2IntegrationId(@JsonProperty("id") UUID id) {
super(id);
}
public static OAuth2IntegrationId fromString(String oauth2IntegrationId) {
return new OAuth2IntegrationId(UUID.fromString(oauth2IntegrationId));
}
}

45
common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.oauth2;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.id.OAuth2IntegrationId;
@EqualsAndHashCode(callSuper = true)
@Data
public class OAuth2ClientInfo extends BaseData<OAuth2IntegrationId> {
private String name;
private String icon;
private String url;
public OAuth2ClientInfo() {
super();
}
public OAuth2ClientInfo(OAuth2IntegrationId id) {
super(id);
}
public OAuth2ClientInfo(OAuth2ClientInfo oauth2ClientInfo) {
super(oauth2ClientInfo);
this.name = oauth2ClientInfo.getName();
this.icon = oauth2ClientInfo.getIcon();
this.url = oauth2ClientInfo.getUrl();
}
}

4
dao/pom.xml

@ -115,6 +115,10 @@
<groupId>org.springframework</groupId> <groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId> <artifactId>spring-web</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.datastax.cassandra</groupId> <groupId>com.datastax.cassandra</groupId>

41
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.oauth2;
import lombok.Data;
@Data
public class OAuth2Client {
private String registrationId;
private String loginButtonLabel;
private String loginButtonIcon;
private String clientName;
private String clientId;
private String clientSecret;
private String accessTokenUri;
private String authorizationUri;
private String scope;
private String redirectUriTemplate;
private String jwkSetUri;
private String loginProcessingUrl;
private String authorizationGrantType;
private String clientAuthenticationMethod;
private String userInfoUri;
private String userNameAttributeName;
private OAuth2ClientMapperConfig mapperConfig;
}

44
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.oauth2;
import lombok.Data;
@Data
public class OAuth2ClientMapperConfig {
private String type;
private CustomOAuth2ClientMapperConfig custom;
private BasicOAuth2ClientMapperConfig basic;
@Data
public static class BasicOAuth2ClientMapperConfig {
private boolean allowUserCreation;
private String emailAttributeKey;
private String firstNameAttributeKey;
private String lastNameAttributeKey;
private String tenantNameStrategy;
private String tenantNameStrategyPattern;
private String customerNameStrategyPattern;
}
@Data
public static class CustomOAuth2ClientMapperConfig {
private String url;
private String username;
private String password;
}
}

80
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java

@ -0,0 +1,80 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.oauth2;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Configuration
@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true", matchIfMissing = true)
@ConfigurationProperties(prefix = "security.oauth2")
@Data
@Slf4j
public class OAuth2Configuration {
private boolean enabled;
private Map<String, OAuth2Client> clients = new HashMap<>();
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> result = new ArrayList<>();
for (OAuth2Client client : clients.values()) {
ClientRegistration registration = ClientRegistration.withRegistrationId(client.getRegistrationId())
.clientId(client.getClientId())
.authorizationUri(client.getAuthorizationUri())
.clientSecret(client.getClientSecret())
.tokenUri(client.getAccessTokenUri())
.redirectUriTemplate(client.getRedirectUriTemplate())
.scope(client.getScope().split(","))
.clientName(client.getClientName())
.authorizationGrantType(new AuthorizationGrantType(client.getAuthorizationGrantType()))
.userInfoUri(client.getUserInfoUri())
.userNameAttributeName(client.getUserNameAttributeName())
.jwkSetUri(client.getJwkSetUri())
.clientAuthenticationMethod(new ClientAuthenticationMethod(client.getClientAuthenticationMethod()))
.build();
result.add(registration);
}
return new InMemoryClientRegistrationRepository(result);
}
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;
break;
}
}
}
return result;
}
}

49
dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.oauth2;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Slf4j
@Service
public class OAuth2ServiceImpl implements OAuth2Service {
@Autowired(required = false)
OAuth2Configuration oauth2Configuration;
@Override
public List<OAuth2ClientInfo> getOAuth2Clients() {
if (!oauth2Configuration.isEnabled()) {
return Collections.emptyList();
}
List<OAuth2ClientInfo> result = new ArrayList<>();
for (OAuth2Client c : oauth2Configuration.getClients().values()) {
OAuth2ClientInfo client = new OAuth2ClientInfo();
client.setName(c.getLoginButtonLabel());
client.setUrl(String.format("/oauth2/authorization/%s", c.getRegistrationId()));
client.setIcon(c.getLoginButtonIcon());
result.add(client);
}
return result;
}
}

3
pom.xml

@ -31,6 +31,7 @@
<main.dir>${basedir}</main.dir> <main.dir>${basedir}</main.dir>
<pkg.user>thingsboard</pkg.user> <pkg.user>thingsboard</pkg.user>
<spring-boot.version>2.2.4.RELEASE</spring-boot.version> <spring-boot.version>2.2.4.RELEASE</spring-boot.version>
<spring-oauth2.version>2.1.2.RELEASE</spring-oauth2.version>
<spring.version>5.2.2.RELEASE</spring.version> <spring.version>5.2.2.RELEASE</spring.version>
<spring-security.version>5.2.2.RELEASE</spring-security.version> <spring-security.version>5.2.2.RELEASE</spring-security.version>
<spring-data-redis.version>2.2.4.RELEASE</spring-data-redis.version> <spring-data-redis.version>2.2.4.RELEASE</spring-data-redis.version>
@ -461,7 +462,7 @@
<dependency> <dependency>
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId> <artifactId>spring-cloud-starter-oauth2</artifactId>
<version>${spring-boot.version}</version> <version>${spring-oauth2.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>

15
ui/src/app/api/login.service.js

@ -18,7 +18,7 @@ export default angular.module('thingsboard.api.login', [])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function LoginService($http, $q) { function LoginService($http, $q, $rootScope) {
var service = { var service = {
activate: activate, activate: activate,
@ -28,6 +28,7 @@ function LoginService($http, $q) {
publicLogin: publicLogin, publicLogin: publicLogin,
resetPassword: resetPassword, resetPassword: resetPassword,
sendResetPasswordLink: sendResetPasswordLink, sendResetPasswordLink: sendResetPasswordLink,
loadOAuth2Clients: loadOAuth2Clients
} }
return service; return service;
@ -109,4 +110,16 @@ function LoginService($http, $q) {
}); });
return deferred.promise; return deferred.promise;
} }
function loadOAuth2Clients(){
var deferred = $q.defer();
var url = '/api/noauth/oauth2Clients';
$http.post(url).then(function success(response) {
$rootScope.oauth2Clients = response.data;
deferred.resolve();
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
} }

35
ui/src/app/app.run.js

@ -17,7 +17,7 @@ import Flow from '@flowjs/ng-flow/dist/ng-flow-standalone.min';
import UrlHandler from './url.handler'; import UrlHandler from './url.handler';
/*@ngInject*/ /*@ngInject*/
export default function AppRun($rootScope, $window, $injector, $location, $log, $state, $mdDialog, $filter, loginService, userService, $translate) { export default function AppRun($rootScope, $window, $injector, $location, $log, $state, $mdDialog, $filter, $q, loginService, userService, $translate) {
$window.Flow = Flow; $window.Flow = Flow;
var frame = null; var frame = null;
@ -41,11 +41,13 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
} }
initWatchers(); initWatchers();
var skipStateChange = false;
function initWatchers() { function initWatchers() {
$rootScope.unauthenticatedHandle = $rootScope.$on('unauthenticated', function (event, doLogout) { $rootScope.unauthenticatedHandle = $rootScope.$on('unauthenticated', function (event, doLogout) {
if (doLogout) { if (doLogout) {
$state.go('login'); gotoPublicModule('login');
} else { } else {
UrlHandler($injector, $location); UrlHandler($injector, $location);
} }
@ -61,6 +63,11 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
$rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) { $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) {
if (skipStateChange) {
skipStateChange = false;
return;
}
function waitForUserLoaded() { function waitForUserLoaded() {
if ($rootScope.userLoadedHandle) { if ($rootScope.userLoadedHandle) {
$rootScope.userLoadedHandle(); $rootScope.userLoadedHandle();
@ -128,7 +135,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
redirectParams.toName = to.name; redirectParams.toName = to.name;
redirectParams.params = params; redirectParams.params = params;
userService.setRedirectParams(redirectParams); userService.setRedirectParams(redirectParams);
$state.go('login', params); gotoPublicModule('login', params);
} else {
evt.preventDefault();
gotoPublicModule(to.name, params);
} }
} }
} else { } else {
@ -158,6 +168,23 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
userService.gotoDefaultPlace(params); userService.gotoDefaultPlace(params);
} }
function gotoPublicModule(name, params) {
let tasks = [];
if (name === "login") {
tasks.push(loginService.loadOAuth2Clients());
}
$q.all(tasks).then(
() => {
skipStateChange = true;
$state.go(name, params);
},
() => {
skipStateChange = true;
$state.go(name, params);
}
);
}
function showForbiddenDialog() { function showForbiddenDialog() {
if (forbiddenDialog === null) { if (forbiddenDialog === null) {
$translate(['access.access-forbidden', $translate(['access.access-forbidden',

6
ui/src/app/locale/locale.constant-cs_CZ.json

@ -1136,7 +1136,7 @@
"total": "celkem" "total": "celkem"
}, },
"login": { "login": {
"login": "Přihlásit", "login": "Přihlásit se",
"request-password-reset": "Vyžádat reset hesla", "request-password-reset": "Vyžádat reset hesla",
"reset-password": "Reset hesla", "reset-password": "Reset hesla",
"create-password": "Vytvořit heslo", "create-password": "Vytvořit heslo",
@ -1150,7 +1150,9 @@
"new-password": "Nové heslo", "new-password": "Nové heslo",
"new-password-again": "Nové heslo znovu", "new-password-again": "Nové heslo znovu",
"password-link-sent-message": "Odkaz pro reset hesla byl úspěšně odeslán!", "password-link-sent-message": "Odkaz pro reset hesla byl úspěšně odeslán!",
"email": "Email" "email": "Email",
"login-with": "Přihlásit se přes {{name}}",
"or": "nebo"
}, },
"position": { "position": {
"top": "Nahoře", "top": "Nahoře",

6
ui/src/app/locale/locale.constant-en_US.json

@ -1317,7 +1317,7 @@
} }
}, },
"login": { "login": {
"login": "Login", "login": "Log in",
"request-password-reset": "Request Password Reset", "request-password-reset": "Request Password Reset",
"reset-password": "Reset Password", "reset-password": "Reset Password",
"create-password": "Create Password", "create-password": "Create Password",
@ -1332,7 +1332,9 @@
"new-password": "New password", "new-password": "New password",
"new-password-again": "New password again", "new-password-again": "New password again",
"password-link-sent-message": "Password reset link was successfully sent!", "password-link-sent-message": "Password reset link was successfully sent!",
"email": "Email" "email": "Email",
"login-with": "Login with {{name}}",
"or": "or"
}, },
"position": { "position": {
"top": "Top", "top": "Top",

4
ui/src/app/locale/locale.constant-ru_RU.json

@ -1246,7 +1246,9 @@
"new-password": "Новый пароль", "new-password": "Новый пароль",
"new-password-again": "Повторите новый пароль", "new-password-again": "Повторите новый пароль",
"password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!", "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!",
"email": "Эл. адрес" "email": "Эл. адрес",
"login-with": "Войти через {{name}}",
"or": "или"
}, },
"position": { "position": {
"top": "Верх", "top": "Верх",

6
ui/src/app/locale/locale.constant-uk_UA.json

@ -1646,7 +1646,7 @@
} }
}, },
"login": { "login": {
"login": "Вхід", "login": "Увійти",
"request-password-reset": "Запит скидання пароля", "request-password-reset": "Запит скидання пароля",
"reset-password": "Скинути пароль", "reset-password": "Скинути пароль",
"create-password": "Створити пароль", "create-password": "Створити пароль",
@ -1661,7 +1661,9 @@
"new-password": "Новий пароль", "new-password": "Новий пароль",
"new-password-again": "Повторіть новий пароль", "new-password-again": "Повторіть новий пароль",
"password-link-sent-message": "Посилання для скидання пароля було успішно надіслано!", "password-link-sent-message": "Посилання для скидання пароля було успішно надіслано!",
"email": "Електронна пошта" "email": "Електронна пошта",
"login-with": "Увійти через {{name}}",
"or": "або"
}, },
"position": { "position": {
"top": "Угорі", "top": "Угорі",

36
ui/src/app/login/login.scss

@ -22,6 +22,10 @@ md-card.tb-login-card {
width: 450px !important; width: 450px !important;
} }
.tb-padding {
padding: 8px;
}
md-card-title { md-card-title {
img.tb-login-logo { img.tb-login-logo {
height: 50px; height: 50px;
@ -31,4 +35,36 @@ md-card.tb-login-card {
md-card-content { md-card-content {
margin-top: -50px; margin-top: -50px;
} }
md-input-container .md-errors-spacer {
display: none;
}
.oauth-container{
.container-divider {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
margin: 10px 0;
.line {
flex: 1;
}
.text {
padding-right: 10px;
padding-left: 10px;
}
}
.material-icons{
width: 20px;
min-width: 20px;
height: 20px;
min-height: 20px;
margin: 0 4px;
}
}
} }

23
ui/src/app/login/login.tpl.html

@ -24,7 +24,7 @@
md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear> md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
<md-card-content> <md-card-content>
<form class="login-form" ng-submit="vm.login()"> <form class="login-form" ng-submit="vm.login()">
<div layout="column" layout-padding="" id="toast-parent"> <div layout="column" class="tb-padding" id="toast-parent">
<span style="height: 50px;"></span> <span style="height: 50px;"></span>
<md-input-container class="md-block"> <md-input-container class="md-block">
<label translate>login.username</label> <label translate>login.username</label>
@ -40,14 +40,23 @@
</md-icon> </md-icon>
<input id="password-input" type="password" ng-model="vm.user.password"/> <input id="password-input" type="password" ng-model="vm.user.password"/>
</md-input-container> </md-input-container>
<div layout-gt-sm="column" layout-align="space-between stretch"> <div layout-gt-sm="column" layout-align="center end" class="tb-padding">
<div layout-gt-sm="column" layout-align="space-between end"> <md-button ui-sref="login.resetPasswordRequest">{{ 'login.forgot-password' | translate }}
<md-button ui-sref="login.resetPasswordRequest">{{ 'login.forgot-password' | translate }} </md-button>
</md-button>
</div>
</div> </div>
<md-button class="md-raised" type="submit">{{ 'login.login' | translate }}</md-button> <md-button class="md-raised" type="submit">{{ 'login.login' | translate }}</md-button>
<a href="oauth2/authorization/A">OAUTH2 LOGIN</a> <div class="oauth-container" layout="column" ng-if="oauth2Clients.length">
<div class="container-divider">
<div class="line"><md-divider></md-divider></div>
<div class="text mat-typography">{{ "login.or" | translate | uppercase }}</div>
<div class="line"><md-divider></md-divider></div>
</div>
<md-button ng-repeat="oauth2Client in oauth2Clients" class="md-raised"
layout="row" layout-align="center center" ng-href="{{ oauth2Client.url }}" target="_self">
<md-icon class="material-icons md-18" md-svg-icon="{{ oauth2Client.icon }}"></md-icon>
{{ 'login.login-with' | translate: {name: oauth2Client.name} }}
</md-button>
</div>
</div> </div>
</form> </form>
</md-card-content> </md-card-content>

Loading…
Cancel
Save