Browse Source

[WIP] [3.0] Added OAuth2 Support (#2709)

* Added base impl for OAuth-2

* Added basic and custom OAuth2 user mappers

* Removed comment line

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

* Revert debug logger

* Fixed compilation

* Test fixed

* Create UI for OAuthService

* Revert package-lock.json

* Add translate login es_ES

Co-authored-by: Vladyslav_Prykhodko <vprykhodko@thingsboard.io>
pull/2729/head
VoBa 6 years ago
committed by GitHub
parent
commit
c0dfa4861b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      application/pom.xml
  2. 19
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  3. 15
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  4. 128
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java
  5. 87
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java
  6. 63
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  7. 24
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java
  8. 45
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java
  9. 70
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  11. 35
      application/src/main/resources/thingsboard.yml
  12. 25
      common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java
  13. 31
      common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2User.java
  14. 35
      common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2IntegrationId.java
  15. 45
      common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java
  16. 4
      dao/pom.xml
  17. 39
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java
  18. 44
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java
  19. 82
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java
  20. 50
      dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java
  21. 16
      pom.xml
  22. 12
      ui-ngx/src/app/core/auth/auth.service.ts
  23. 28
      ui-ngx/src/app/core/guards/auth.guard.ts
  24. 15
      ui-ngx/src/app/modules/login/pages/login/login.component.html
  25. 40
      ui-ngx/src/app/modules/login/pages/login/login.component.scss
  26. 3
      ui-ngx/src/app/modules/login/pages/login/login.component.ts
  27. 6
      ui-ngx/src/app/shared/models/login.models.ts
  28. 6
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  29. 6
      ui-ngx/src/assets/locale/locale.constant-de_DE.json
  30. 4
      ui-ngx/src/assets/locale/locale.constant-el_GR.json
  31. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  32. 4
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  33. 6
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json
  34. 6
      ui-ngx/src/assets/locale/locale.constant-it_IT.json
  35. 4
      ui-ngx/src/assets/locale/locale.constant-ja_JA.json
  36. 4
      ui-ngx/src/assets/locale/locale.constant-ko_KR.json
  37. 6
      ui-ngx/src/assets/locale/locale.constant-ro_RO.json
  38. 4
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  39. 6
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json
  40. 6
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json
  41. 15
      ui/src/app/api/login.service.js
  42. 35
      ui/src/app/app.run.js
  43. 6
      ui/src/app/locale/locale.constant-cs_CZ.json
  44. 6
      ui/src/app/locale/locale.constant-en_US.json
  45. 4
      ui/src/app/locale/locale.constant-ru_RU.json
  46. 6
      ui/src/app/locale/locale.constant-uk_UA.json
  47. 36
      ui/src/app/login/login.scss
  48. 22
      ui/src/app/login/login.tpl.html

12
application/pom.xml

@ -132,6 +132,18 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>

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

@ -39,6 +39,7 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter;
@ -73,12 +74,22 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**";
@Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler;
@Autowired private AuthenticationSuccessHandler successHandler;
@Autowired(required = false)
@Qualifier("oauth2AuthenticationSuccessHandler")
private AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler;
@Autowired
@Qualifier("defaultAuthenticationSuccessHandler")
private AuthenticationSuccessHandler successHandler;
@Autowired private AuthenticationFailureHandler failureHandler;
@Autowired private RestAuthenticationProvider restAuthenticationProvider;
@Autowired private JwtAuthenticationProvider jwtAuthenticationProvider;
@Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider;
@Autowired(required = false) OAuth2Configuration oauth2Configuration;
@Autowired
@Qualifier("jwtHeaderTokenExtractor")
private TokenExtractor jwtHeaderTokenExtractor;
@ -189,6 +200,12 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
.addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
if (oauth2Configuration != null && oauth2Configuration.isEnabled()) {
http.oauth2Login()
.loginPage("/oauth2Login")
.loginProcessingUrl(oauth2Configuration.getLoginProcessingUrl())
.successHandler(oauth2AuthenticationSuccessHandler);
}
}

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.ThingsboardException;
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.dao.audit.AuditLogService;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
@ -55,6 +57,7 @@ import ua_parser.Client;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
@RestController
@TbCoreComponent
@ -80,6 +83,9 @@ public class AuthController extends BaseController {
@Autowired
private AuditLogService auditLogService;
@Autowired
private OAuth2Service oauth2Service;
@PreAuthorize("isAuthenticated()")
@RequestMapping(value = "/auth/user", method = RequestMethod.GET)
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);
}
}
}

128
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java

@ -0,0 +1,128 @@
/**
* 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.PageLink;
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;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public abstract class AbstractOAuth2ClientMapper {
@Autowired
private UserService userService;
@Autowired
private TenantService tenantService;
@Autowired
private CustomerService customerService;
private final Lock userCreationLock = new ReentrantLock();
protected SecurityUser getOrCreateSecurityUserFromOAuth2User(OAuth2User oauth2User, boolean allowUserCreation) {
UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail());
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) {
userCreationLock.lock();
try {
user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail());
if (user == null) {
user = new User();
if (oauth2User.getCustomerId() == null && StringUtils.isEmpty(oauth2User.getCustomerName())) {
user.setAuthority(Authority.TENANT_ADMIN);
} else {
user.setAuthority(Authority.CUSTOMER_USER);
}
TenantId tenantId = oauth2User.getTenantId() != null ?
oauth2User.getTenantId() : getTenantId(oauth2User.getTenantName());
user.setTenantId(tenantId);
CustomerId customerId = oauth2User.getCustomerId() != null ?
oauth2User.getCustomerId() : getCustomerId(user.getTenantId(), oauth2User.getCustomerName());
user.setCustomerId(customerId);
user.setEmail(oauth2User.getEmail());
user.setFirstName(oauth2User.getFirstName());
user.setLastName(oauth2User.getLastName());
user = userService.saveUser(user);
}
} finally {
userCreationLock.unlock();
}
}
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 new RuntimeException("Can't get or create security user from oauth2 user", e);
}
}
private TenantId getTenantId(String tenantName) {
List<Tenant> tenants = tenantService.findTenants(new PageLink(1, 0, 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();
}
}
}

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

@ -0,0 +1,87 @@
/**
* 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 AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
private static final String START_PLACEHOLDER_PREFIX = "%{";
private static final String END_PLACEHOLDER_PREFIX = "}";
private static final String EMAIL_TENANT_STRATEGY = "email";
private static final String DOMAIN_TENANT_STRATEGY = "domain";
private static final String CUSTOM_TENANT_STRATEGY = "custom";
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, OAuth2ClientMapperConfig config) {
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, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
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 EMAIL_TENANT_STRATEGY:
return getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
case DOMAIN_TENANT_STRATEGY:
String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
return email.substring(email .indexOf("@") + 1);
case CUSTOM_TENANT_STRATEGY:
StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
return sub.replace(config.getBasic().getTenantNameStrategyPattern());
default:
throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!");
}
}
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;
}
}

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

@ -0,0 +1,63 @@
/**
* 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 com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
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 AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
private static final ObjectMapper json = new ObjectMapper();
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();
String request;
try {
request = json.writeValueAsString(token.getPrincipal());
} catch (JsonProcessingException e) {
log.error("Can't convert principal to JSON string", e);
throw new RuntimeException("Can't convert principal to JSON string", e);
}
try {
return restTemplate.postForEntity(custom.getUrl(), request, OAuth2User.class).getBody();
} catch (Exception e) {
log.error("Can't connect to custom mapper endpoint", e);
throw new RuntimeException("Can't connect to custom mapper endpoint", e);
}
}
}

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.Map;
@Component
@Component(value = "defaultAuthenticationSuccessHandler")
public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectMapper mapper;
private final JwtTokenFactory tokenFactory;

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

@ -97,6 +97,41 @@ security:
allowClaimingByDefault: "${SECURITY_CLAIM_ALLOW_CLAIMING_BY_DEFAULT:true}"
# Time allowed to claim the device in milliseconds
duration: "${SECURITY_CLAIM_DURATION:60000}" # 1 minute, note this value must equal claimDevices.timeToLiveInMinutes value
basic:
enabled: "${SECURITY_BASIC_ENABLED:false}"
oauth2:
enabled: "${SECURITY_OAUTH2_ENABLED:false}"
loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
clients:
default:
loginButtonLabel: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_LABEL:Default}" # Label that going to be show on login screen
loginButtonIcon: "${SECURITY_OAUTH2_DEFAULT_LOGIN_BUTTON_ICON:}" # Icon that going to be show on login screen. Material design icon ID (https://material.angularjs.org/latest/api/directive/mdIcon)
clientName: "${SECURITY_OAUTH2_DEFAULT_CLIENT_NAME:ClientName}"
clientId: "${SECURITY_OAUTH2_DEFAULT_CLIENT_ID:}"
clientSecret: "${SECURITY_OAUTH2_DEFAULT_CLIENT_SECRET:}"
accessTokenUri: "${SECURITY_OAUTH2_DEFAULT_ACCESS_TOKEN_URI:}"
authorizationUri: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_URI:}"
scope: "${SECURITY_OAUTH2_DEFAULT_SCOPE:}"
redirectUriTemplate: "${SECURITY_OAUTH2_DEFAULT_REDIRECT_URI_TEMPLATE:http://localhost:8080/login/oauth2/code/}" # Must be in sync with security.oauth2.loginProcessingUrl
jwkSetUri: "${SECURITY_OAUTH2_DEFAULT_JWK_SET_URI:}"
authorizationGrantType: "${SECURITY_OAUTH2_DEFAULT_AUTHORIZATION_GRANT_TYPE:authorization_code}" # authorization_code, implicit, refresh_token or client_credentials
clientAuthenticationMethod: "${SECURITY_OAUTH2_DEFAULT_CLIENT_AUTHENTICATION_METHOD:post}" # basic or post
userInfoUri: "${SECURITY_OAUTH2_DEFAULT_USER_INFO_URI:}"
userNameAttributeName: "${SECURITY_OAUTH2_DEFAULT_USER_NAME_ATTRIBUTE_NAME:email}"
mapperConfig:
type: "${SECURITY_OAUTH2_DEFAULT_MAPPER_TYPE:basic}" # basic or custom
basic:
allowUserCreation: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_ALLOW_USER_CREATION:true}" # Allows to create user if it not exists
emailAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_EMAIL_ATTRIBUTE_KEY:email}" # Attribute key to use as email for the user
firstNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_FIRST_NAME_ATTRIBUTE_KEY:}"
lastNameAttributeKey: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_LAST_NAME_ATTRIBUTE_KEY:}"
tenantNameStrategy: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY:domain}" # domain, email or custom
tenantNameStrategyPattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_TENANT_NAME_STRATEGY_PATTERN:}"
customerNameStrategyPattern: "${SECURITY_OAUTH2_DEFAULT_MAPPER_BASIC_CUSTOMER_NAME_STRATEGY_PATTERN:}"
custom:
url: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_URL:}"
username: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_USERNAME:}"
password: "${SECURITY_OAUTH2_DEFAULT_MAPPER_CUSTOM_PASSWORD:}"
# Dashboard parameters
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();
}

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

@ -0,0 +1,31 @@
/**
* 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 org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
@Data
public class OAuth2User {
private String tenantName;
private TenantId tenantId;
private String customerName;
private CustomerId customerId;
private String email;
private String firstName;
private String lastName;
}

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>
<artifactId>spring-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>com.datastax.cassandra</groupId>

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

@ -0,0 +1,39 @@
/**
* 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 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 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 BasicOAuth2ClientMapperConfig basic;
private CustomOAuth2ClientMapperConfig custom;
@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;
}
}

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

@ -0,0 +1,82 @@
/**
* 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")
@ConfigurationProperties(prefix = "security.oauth2")
@Data
@Slf4j
public class OAuth2Configuration {
private boolean enabled;
private String loginProcessingUrl;
private Map<String, OAuth2Client> clients = new HashMap<>();
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> result = new ArrayList<>();
for (Map.Entry<String, OAuth2Client> entry : clients.entrySet()) {
OAuth2Client client = entry.getValue();
ClientRegistration registration = ClientRegistration.withRegistrationId(entry.getKey())
.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 (String key : clients.keySet()) {
if (key.equals(registrationId)) {
result = clients.get(key);
break;
}
}
}
return result;
}
}

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

@ -0,0 +1,50 @@
/**
* 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;
import java.util.Map;
@Slf4j
@Service
public class OAuth2ServiceImpl implements OAuth2Service {
@Autowired(required = false)
OAuth2Configuration oauth2Configuration;
@Override
public List<OAuth2ClientInfo> getOAuth2Clients() {
if (oauth2Configuration == null || !oauth2Configuration.isEnabled()) {
return Collections.emptyList();
}
List<OAuth2ClientInfo> result = new ArrayList<>();
for (Map.Entry<String, OAuth2Client> entry : oauth2Configuration.getClients().entrySet()) {
OAuth2ClientInfo client = new OAuth2ClientInfo();
client.setName(entry.getValue().getLoginButtonLabel());
client.setUrl(String.format("/oauth2/authorization/%s", entry.getKey()));
client.setIcon(entry.getValue().getLoginButtonIcon());
result.add(client);
}
return result;
}
}

16
pom.xml

@ -31,6 +31,7 @@
<main.dir>${basedir}</main.dir>
<pkg.user>thingsboard</pkg.user>
<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-security.version>5.2.2.RELEASE</spring-security.version>
<spring-data-redis.version>2.2.4.RELEASE</spring-data-redis.version>
@ -462,6 +463,21 @@
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>${spring-oauth2.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

12
ui-ngx/src/app/core/auth/auth.service.ts

@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http';
import { forkJoin, Observable, of, throwError } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
import { LoginRequest, LoginResponse, OAuth2Client, PublicLoginRequest } from '@shared/models/login.models';
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
import { defaultHttpOptions } from '../http/http-utils';
import { ReplaySubject } from 'rxjs/internal/ReplaySubject';
@ -65,6 +65,7 @@ export class AuthService {
}
redirectUrl: string;
oauth2Clients: Array<OAuth2Client> = null;
private refreshTokenSubject: ReplaySubject<LoginResponse> = null;
private jwtHelper = new JwtHelperService();
@ -193,6 +194,15 @@ export class AuthService {
});
}
public loadOAuth2Clients(): Observable<Array<OAuth2Client>> {
return this.http.post<Array<OAuth2Client>>(`/api/noauth/oauth2Clients`,
null, defaultHttpOptions()).pipe(
tap((OAuth2Clients) => {
this.oauth2Clients = OAuth2Clients;
})
);
}
private forceDefaultPlace(authState?: AuthState, path?: string, params?: any): boolean {
if (authState && authState.authUser) {
if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) {

28
ui-ngx/src/app/core/guards/auth.guard.ts

@ -20,9 +20,9 @@ import { AuthService } from '../auth/auth.service';
import { select, Store } from '@ngrx/store';
import { AppState } from '../core.state';
import { selectAuth } from '../auth/auth.selectors';
import { catchError, map, skipWhile, take } from 'rxjs/operators';
import { catchError, map, mergeMap, skipWhile, take } from 'rxjs/operators';
import { AuthState } from '../auth/auth.models';
import { Observable, of } from 'rxjs';
import { forkJoin, Observable, of } from 'rxjs';
import { enterZone } from '@core/operator/enterZone';
import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service';
@ -54,7 +54,7 @@ export class AuthGuard implements CanActivate, CanActivateChild {
state: RouterStateSnapshot) {
return this.getAuthState().pipe(
map((authState) => {
mergeMap((authState) => {
const url: string = state.url;
let lastChild = state.root;
@ -78,13 +78,21 @@ export class AuthGuard implements CanActivate, CanActivateChild {
if (publicId && publicId.length > 0) {
this.authService.setUserFromJwtToken(null, null, false);
this.authService.reloadUser();
return false;
return of(false);
} else if (!isPublic) {
this.authService.redirectUrl = url;
// this.authService.gotoDefaultPlace(false);
return this.authService.defaultUrl(false);
return of(this.authService.defaultUrl(false));
} else {
return true;
const tasks: Observable<any>[] = [];
if (path === 'login') {
tasks.push(this.authService.loadOAuth2Clients());
}
return forkJoin(tasks).pipe(
map(() => {
return true;
})
);
}
} else {
if (authState.authUser.isPublic) {
@ -95,20 +103,20 @@ export class AuthGuard implements CanActivate, CanActivateChild {
} else {
this.authService.logout();
}
return false;
return of(false);
}
}
const defaultUrl = this.authService.defaultUrl(true, authState, path, params);
if (defaultUrl) {
// this.authService.gotoDefaultPlace(true);
return defaultUrl;
return of(defaultUrl);
} else {
const authority = Authority[authState.authUser.authority];
if (data.auth && data.auth.indexOf(authority) === -1) {
this.dialogService.forbidden();
return false;
return of(false);
} else {
return true;
return of(true);
}
}
}

15
ui-ngx/src/app/modules/login/pages/login/login.component.html

@ -16,7 +16,7 @@
-->
<div class="tb-login-content mat-app-background tb-dark" fxFlex fxLayoutAlign="center center">
<mat-card style="height: 100%; max-height: 525px; overflow-y: auto;">
<mat-card style="max-height: 80vh; overflow-y: auto;">
<mat-card-content>
<form class="tb-login-form" [formGroup]="loginFormGroup" (ngSubmit)="login()">
<fieldset [disabled]="isLoading$ | async" fxLayout="column">
@ -49,6 +49,19 @@
<button mat-raised-button color="accent" [disabled]="(isLoading$ | async)"
type="submit">{{ 'login.login' | translate }}</button>
</div>
<div class="oauth-container" fxLayout="column" fxLayoutGap="16px" *ngIf="oauth2Clients?.length">
<div class="container-divider">
<div class="line"><mat-divider></mat-divider></div>
<div class="text mat-typography">{{ "login.or" | translate | uppercase }}</div>
<div class="line"><mat-divider></mat-divider></div>
</div>
<ng-container *ngFor="let oauth2Client of oauth2Clients">
<button mat-raised-button color="primary" class="centered" routerLink="{{ oauth2Client.url }}">
<mat-icon class="material-icons md-18" svgIcon="{{ oauth2Client.icon }}"></mat-icon>
{{ 'login.login-with' | translate: {name: oauth2Client.name} }}
</button>
</ng-container>
</div>
</div>
</fieldset>
</form>

40
ui-ngx/src/app/modules/login/pages/login/login.component.scss

@ -32,8 +32,44 @@
}
.tb-action-button{
padding-top: 20px;
padding-bottom: 20px;
padding: 20px 0 16px;
}
}
.oauth-container{
padding: 0;
.container-divider {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
.line {
flex: 1;
}
.mat-divider-horizontal{
position: relative;
}
.text {
padding-right: 10px;
padding-left: 10px;
}
}
.material-icons{
width: 20px;
min-width: 20px;
}
.centered ::ng-deep .mat-button-wrapper {
display: flex;
justify-content: center;
align-items: center;
}
}
}

3
ui-ngx/src/app/modules/login/pages/login/login.component.ts

@ -23,6 +23,7 @@ import { FormBuilder } from '@angular/forms';
import { HttpErrorResponse } from '@angular/common/http';
import { Constants } from '@shared/models/constants';
import { Router } from '@angular/router';
import { OAuth2Client } from '@shared/models/login.models';
@Component({
selector: 'tb-login',
@ -35,6 +36,7 @@ export class LoginComponent extends PageComponent implements OnInit {
username: '',
password: ''
});
oauth2Clients: Array<OAuth2Client> = null;
constructor(protected store: Store<AppState>,
private authService: AuthService,
@ -44,6 +46,7 @@ export class LoginComponent extends PageComponent implements OnInit {
}
ngOnInit() {
this.oauth2Clients = this.authService.oauth2Clients;
}
login(): void {

6
ui-ngx/src/app/shared/models/login.models.ts

@ -27,3 +27,9 @@ export interface LoginResponse {
token: string;
refreshToken: string;
}
export interface OAuth2Client {
name: string;
icon?: string;
url: string;
}

6
ui-ngx/src/assets/locale/locale.constant-cs_CZ.json

@ -1137,7 +1137,7 @@
"total": "celkem"
},
"login": {
"login": "Přihlásit",
"login": "Přihlásit se",
"request-password-reset": "Vyžádat reset hesla",
"reset-password": "Reset hesla",
"create-password": "Vytvořit heslo",
@ -1151,7 +1151,9 @@
"new-password": "Nové heslo",
"new-password-again": "Nové heslo znovu",
"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": {
"top": "Nahoře",

6
ui-ngx/src/assets/locale/locale.constant-de_DE.json

@ -1150,7 +1150,7 @@
"total": "Gesamt"
},
"login": {
"login": "Login",
"login": "Anmelden",
"request-password-reset": "Passwortzurücksetzung anfordern",
"reset-password": "Passwort zurücksetzen",
"create-password": "Passwort erstellen",
@ -1164,7 +1164,9 @@
"new-password": "Neues Passwort",
"new-password-again": "Neues Passwort wiederholen",
"password-link-sent-message": "Der Link zum Zurücksetzen des Passworts wurde erfolgreich versendet!",
"email": "E-Mail"
"email": "E-Mail",
"login-with": "Mit {{name}} anmelden",
"or": "oder"
},
"position": {
"top": "Oben",

4
ui-ngx/src/assets/locale/locale.constant-el_GR.json

@ -1706,7 +1706,9 @@
"password-link-sent-message": "Ο σύνδεσμος επαναφοράς κωδικού πρόσβασης στάλθηκε με επιτυχία!",
"email": "Email",
"no-account": "Δεν έχετε λογαριασμό;",
"create-account": "Δημιουργία λογαριασμού"
"create-account": "Δημιουργία λογαριασμού",
"login-with": "Σύνδεση μέσω {{name}}",
"or": "ή"
},
"signup": {
"firstname": "Όνομα",

4
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1353,7 +1353,9 @@
"new-password": "New password",
"new-password-again": "New password again",
"password-link-sent-message": "Password reset link was successfully sent!",
"email": "Email"
"email": "Email",
"login-with": "Login with {{name}}",
"or": "or"
},
"position": {
"top": "Top",

4
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -1230,7 +1230,9 @@
"new-password": "Nueva contraseña",
"new-password-again": "Repita la nueva contraseña",
"password-link-sent-message": "¡El enlace para el restablecer la contraseña fue enviado correctamente!",
"email": "Correo electrónico"
"email": "Correo electrónico",
"login-with": "Iniciar sesión con {{name}}",
"or": "o"
},
"position": {
"top": "Superior",

6
ui-ngx/src/assets/locale/locale.constant-fr_FR.json

@ -1198,7 +1198,7 @@
"create-password": "Créer un mot de passe",
"email": "Email",
"forgot-password": "Mot de passe oublié?",
"login": "Login",
"login": "Connexion",
"new-password": "Nouveau mot de passe",
"new-password-again": "nouveau mot de passe",
"password-again": "Mot de passe à nouveau",
@ -1209,7 +1209,9 @@
"request-password-reset": "Demander la réinitialisation du mot de passe",
"reset-password": "Réinitialiser le mot de passe",
"sign-in": "Veuillez vous connecter",
"username": "Nom d'utilisateur (courriel)"
"username": "Nom d'utilisateur (courriel)",
"login-with": "Se connecter avec {{name}}",
"or": "ou"
},
"position": {
"bottom": "Bas",

6
ui-ngx/src/assets/locale/locale.constant-it_IT.json

@ -1161,7 +1161,7 @@
"total": "totale"
},
"login": {
"login": "Login",
"login": "Accedi",
"request-password-reset": "Richiesta reset password",
"reset-password": "Reset Password",
"create-password": "Crea Password",
@ -1175,7 +1175,9 @@
"new-password": "Nuova password",
"new-password-again": "Ripeti nuova password",
"password-link-sent-message": "Link reset password inviato con successo!",
"email": "Email"
"email": "Email",
"login-with": "Accedi con {{name}}",
"or": "o"
},
"position": {
"top": "Alto",

4
ui-ngx/src/assets/locale/locale.constant-ja_JA.json

@ -1025,7 +1025,9 @@
"new-password": "新しいパスワード",
"new-password-again": "新しいパスワードを再入力",
"password-link-sent-message": "パスワードリセットリンクが正常に送信されました!",
"email": "Eメール"
"email": "Eメール",
"login-with": "{{name}}でログイン",
"or": "または"
},
"position": {
"top": "上",

4
ui-ngx/src/assets/locale/locale.constant-ko_KR.json

@ -934,7 +934,9 @@
"new-password": "새 비밀번호",
"new-password-again": "새 비밀번호 확인",
"password-link-sent-message": "비밀번호 재설정 링크가 성공적으로 전송되었습니다!",
"email": "이메일"
"email": "이메일",
"login-with": "{{name}}으로 로그인",
"or": "또는"
},
"position": {
"top": "상단",

6
ui-ngx/src/assets/locale/locale.constant-ro_RO.json

@ -1231,7 +1231,7 @@
}
},
"login": {
"login": "Intră în Cont",
"login": "Conectare",
"request-password-reset": "Solicită Resetarea Parolei",
"reset-password": "Resetează Parolă",
"create-password": "Creează Parolă",
@ -1246,7 +1246,9 @@
"new-password": "Parolă nouă",
"new-password-again": "Verificare parolă nouă",
"password-link-sent-message": "Ți-am trimis pe eMail un link pentru resetarea parolei",
"email": "eMail"
"email": "eMail",
"login-with": "Conectare cu {{name}}",
"or": "sau"
},
"position": {
"top": "Sus",

4
ui-ngx/src/assets/locale/locale.constant-ru_RU.json

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

6
ui-ngx/src/assets/locale/locale.constant-tr_TR.json

@ -1091,7 +1091,7 @@
"total": "toplam"
},
"login": {
"login": "Oturum aç",
"login": "Giriş Yap",
"request-password-reset": "Parola Sıfırlama İsteği Gönder",
"reset-password": "Parola Sıfırla",
"create-password": "Parola Oluştur",
@ -1105,7 +1105,9 @@
"new-password": "Yeni parola",
"new-password-again": "Yeni parola tekrarı",
"password-link-sent-message": "Parola sıfırlama e-postası başarıyla gönderildi!",
"email": "E-posta"
"email": "E-posta",
"login-with": "{{name}} ile Giriş Yap",
"or": "ya da"
},
"position": {
"top": "Üst",

6
ui-ngx/src/assets/locale/locale.constant-uk_UA.json

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

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

@ -18,7 +18,7 @@ export default angular.module('thingsboard.api.login', [])
.name;
/*@ngInject*/
function LoginService($http, $q) {
function LoginService($http, $q, $rootScope) {
var service = {
activate: activate,
@ -28,6 +28,7 @@ function LoginService($http, $q) {
publicLogin: publicLogin,
resetPassword: resetPassword,
sendResetPasswordLink: sendResetPasswordLink,
loadOAuth2Clients: loadOAuth2Clients
}
return service;
@ -109,4 +110,16 @@ function LoginService($http, $q) {
});
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';
/*@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;
var frame = null;
@ -41,11 +41,13 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
}
initWatchers();
var skipStateChange = false;
function initWatchers() {
$rootScope.unauthenticatedHandle = $rootScope.$on('unauthenticated', function (event, doLogout) {
if (doLogout) {
$state.go('login');
gotoPublicModule('login');
} else {
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) {
if (skipStateChange) {
skipStateChange = false;
return;
}
function waitForUserLoaded() {
if ($rootScope.userLoadedHandle) {
$rootScope.userLoadedHandle();
@ -128,7 +135,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
redirectParams.toName = to.name;
redirectParams.params = params;
userService.setRedirectParams(redirectParams);
$state.go('login', params);
gotoPublicModule('login', params);
} else {
evt.preventDefault();
gotoPublicModule(to.name, params);
}
}
} else {
@ -158,6 +168,23 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
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() {
if (forbiddenDialog === null) {
$translate(['access.access-forbidden',

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

@ -1136,7 +1136,7 @@
"total": "celkem"
},
"login": {
"login": "Přihlásit",
"login": "Přihlásit se",
"request-password-reset": "Vyžádat reset hesla",
"reset-password": "Reset hesla",
"create-password": "Vytvořit heslo",
@ -1150,7 +1150,9 @@
"new-password": "Nové heslo",
"new-password-again": "Nové heslo znovu",
"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": {
"top": "Nahoře",

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

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

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

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

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

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

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

@ -22,6 +22,10 @@ md-card.tb-login-card {
width: 450px !important;
}
.tb-padding {
padding: 8px;
}
md-card-title {
img.tb-login-logo {
height: 50px;
@ -31,4 +35,36 @@ md-card.tb-login-card {
md-card-content {
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;
}
}
}

22
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-card-content>
<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>
<md-input-container class="md-block">
<label translate>login.username</label>
@ -40,13 +40,23 @@
</md-icon>
<input id="password-input" type="password" ng-model="vm.user.password"/>
</md-input-container>
<div layout-gt-sm="column" layout-align="space-between stretch">
<div layout-gt-sm="column" layout-align="space-between end">
<md-button ui-sref="login.resetPasswordRequest">{{ 'login.forgot-password' | translate }}
</md-button>
</div>
<div layout-gt-sm="column" layout-align="center end" class="tb-padding">
<md-button ui-sref="login.resetPasswordRequest">{{ 'login.forgot-password' | translate }}
</md-button>
</div>
<md-button class="md-raised" type="submit">{{ 'login.login' | translate }}</md-button>
<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>
</form>
</md-card-content>

Loading…
Cancel
Save