From c0dfa4861ba231a0a2542c6be4c2bc9396370a0c Mon Sep 17 00:00:00 2001 From: VoBa Date: Mon, 4 May 2020 11:51:08 +0300 Subject: [PATCH] [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 --- application/pom.xml | 12 ++ .../ThingsboardSecurityConfiguration.java | 19 ++- .../server/controller/AuthController.java | 15 ++ .../oauth2/AbstractOAuth2ClientMapper.java | 128 ++++++++++++++++++ .../auth/oauth2/BasicOAuth2ClientMapper.java | 87 ++++++++++++ .../auth/oauth2/CustomOAuth2ClientMapper.java | 63 +++++++++ .../auth/oauth2/OAuth2ClientMapper.java | 24 ++++ .../oauth2/OAuth2ClientMapperProvider.java | 45 ++++++ .../Oauth2AuthenticationSuccessHandler.java | 70 ++++++++++ ...RestAwareAuthenticationSuccessHandler.java | 2 +- .../src/main/resources/thingsboard.yml | 35 +++++ .../server/dao/oauth2/OAuth2Service.java | 25 ++++ .../server/dao/oauth2/OAuth2User.java | 31 +++++ .../common/data/id/OAuth2IntegrationId.java | 35 +++++ .../common/data/oauth2/OAuth2ClientInfo.java | 45 ++++++ dao/pom.xml | 4 + .../server/dao/oauth2/OAuth2Client.java | 39 ++++++ .../dao/oauth2/OAuth2ClientMapperConfig.java | 44 ++++++ .../dao/oauth2/OAuth2Configuration.java | 82 +++++++++++ .../server/dao/oauth2/OAuth2ServiceImpl.java | 50 +++++++ pom.xml | 16 +++ ui-ngx/src/app/core/auth/auth.service.ts | 12 +- ui-ngx/src/app/core/guards/auth.guard.ts | 28 ++-- .../login/pages/login/login.component.html | 15 +- .../login/pages/login/login.component.scss | 40 +++++- .../login/pages/login/login.component.ts | 3 + ui-ngx/src/app/shared/models/login.models.ts | 6 + .../assets/locale/locale.constant-cs_CZ.json | 6 +- .../assets/locale/locale.constant-de_DE.json | 6 +- .../assets/locale/locale.constant-el_GR.json | 4 +- .../assets/locale/locale.constant-en_US.json | 4 +- .../assets/locale/locale.constant-es_ES.json | 4 +- .../assets/locale/locale.constant-fr_FR.json | 6 +- .../assets/locale/locale.constant-it_IT.json | 6 +- .../assets/locale/locale.constant-ja_JA.json | 4 +- .../assets/locale/locale.constant-ko_KR.json | 4 +- .../assets/locale/locale.constant-ro_RO.json | 6 +- .../assets/locale/locale.constant-ru_RU.json | 4 +- .../assets/locale/locale.constant-tr_TR.json | 6 +- .../assets/locale/locale.constant-uk_UA.json | 6 +- ui/src/app/api/login.service.js | 15 +- ui/src/app/app.run.js | 35 ++++- ui/src/app/locale/locale.constant-cs_CZ.json | 6 +- ui/src/app/locale/locale.constant-en_US.json | 6 +- ui/src/app/locale/locale.constant-ru_RU.json | 4 +- ui/src/app/locale/locale.constant-uk_UA.json | 6 +- ui/src/app/login/login.scss | 36 +++++ ui/src/app/login/login.tpl.html | 22 ++- 48 files changed, 1117 insertions(+), 54 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2User.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2IntegrationId.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java diff --git a/application/pom.xml b/application/pom.xml index 986486bd83..6ca5822d21 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -132,6 +132,18 @@ org.springframework.boot spring-boot-starter-websocket + + org.springframework.cloud + spring-cloud-starter-oauth2 + + + org.springframework.security + spring-security-oauth2-client + + + org.springframework.security + spring-security-oauth2-jose + io.jsonwebtoken jjwt diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index fee10133d7..c112f1feaa 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/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); + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 42da043a91..3097b9c2b1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/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 getOath2Clients() throws ThingsboardException { + try { + return oauth2Service.getOAuth2Clients(); + } catch (Exception e) { + throw handleException(e); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java new file mode 100644 index 0000000000..e659e1fa96 --- /dev/null +++ b/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 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 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(); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java new file mode 100644 index 0000000000..935f7f5e3a --- /dev/null +++ b/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 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 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 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; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java new file mode 100644 index 0000000000..31e2c2bbec --- /dev/null +++ b/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); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java new file mode 100644 index 0000000000..196bfe7b50 --- /dev/null +++ b/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); +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java new file mode 100644 index 0000000000..e1c5b694bb --- /dev/null +++ b/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!"); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java new file mode 100644 index 0000000000..be8f7ca7c2 --- /dev/null +++ b/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()); + } +} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java index aa55818084..4983071c5f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java +++ b/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; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index dfaf610b35..5c663de996 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/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: diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java new file mode 100644 index 0000000000..d72b6ef98c --- /dev/null +++ b/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 getOAuth2Clients(); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2User.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2User.java new file mode 100644 index 0000000000..c0075633e5 --- /dev/null +++ b/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; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2IntegrationId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OAuth2IntegrationId.java new file mode 100644 index 0000000000..30fd55d204 --- /dev/null +++ b/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)); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java new file mode 100644 index 0000000000..0ee5832e63 --- /dev/null +++ b/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 { + + 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(); + } +} diff --git a/dao/pom.xml b/dao/pom.xml index 63ca39c715..aa31fed8ef 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -115,6 +115,10 @@ org.springframework spring-web provided + + + org.springframework.security + spring-security-oauth2-client com.datastax.cassandra diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Client.java new file mode 100644 index 0000000000..9676d55f5f --- /dev/null +++ b/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; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ClientMapperConfig.java new file mode 100644 index 0000000000..ec4f199549 --- /dev/null +++ b/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; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Configuration.java new file mode 100644 index 0000000000..e54f9a5b3e --- /dev/null +++ b/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 clients = new HashMap<>(); + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + List result = new ArrayList<>(); + for (Map.Entry entry : clients.entrySet()) { + OAuth2Client client = entry.getValue(); + ClientRegistration registration = ClientRegistration.withRegistrationId(entry.getKey()) + .clientId(client.getClientId()) + .authorizationUri(client.getAuthorizationUri()) + .clientSecret(client.getClientSecret()) + .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; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java new file mode 100644 index 0000000000..43e11244b0 --- /dev/null +++ b/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 getOAuth2Clients() { + if (oauth2Configuration == null || !oauth2Configuration.isEnabled()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (Map.Entry 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; + } +} diff --git a/pom.xml b/pom.xml index 6c14156bb5..8844edc9e3 100755 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,7 @@ ${basedir} thingsboard 2.2.4.RELEASE + 2.1.2.RELEASE 5.2.2.RELEASE 5.2.2.RELEASE 2.2.4.RELEASE @@ -462,6 +463,21 @@ spring-boot-starter-security ${spring-boot.version} + + org.springframework.cloud + spring-cloud-starter-oauth2 + ${spring-oauth2.version} + + + org.springframework.security + spring-security-oauth2-client + ${spring.version} + + + org.springframework.security + spring-security-oauth2-jose + ${spring.version} + org.springframework.boot spring-boot-starter-web diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 438627205f..0faf7d6769 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/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 = null; private refreshTokenSubject: ReplaySubject = null; private jwtHelper = new JwtHelperService(); @@ -193,6 +194,15 @@ export class AuthService { }); } + public loadOAuth2Clients(): Observable> { + return this.http.post>(`/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) { diff --git a/ui-ngx/src/app/core/guards/auth.guard.ts b/ui-ngx/src/app/core/guards/auth.guard.ts index 98a0e781d2..02a5be8300 100644 --- a/ui-ngx/src/app/core/guards/auth.guard.ts +++ b/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[] = []; + 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); } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html index 5da3aeba56..caf65f52b8 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -16,7 +16,7 @@ --> +
+
+
+
{{ "login.or" | translate | uppercase }}
+
+
+ + + +
diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.scss b/ui-ngx/src/app/modules/login/pages/login/login.component.scss index 88d7bbf199..2127439234 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.scss +++ b/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; } } } diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.ts b/ui-ngx/src/app/modules/login/pages/login/login.component.ts index 6fe81e44ef..a9717179d6 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.ts +++ b/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 = null; constructor(protected store: Store, private authService: AuthService, @@ -44,6 +46,7 @@ export class LoginComponent extends PageComponent implements OnInit { } ngOnInit() { + this.oauth2Clients = this.authService.oauth2Clients; } login(): void { diff --git a/ui-ngx/src/app/shared/models/login.models.ts b/ui-ngx/src/app/shared/models/login.models.ts index 19b40fd50e..7c22bd3952 100644 --- a/ui-ngx/src/app/shared/models/login.models.ts +++ b/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; +} diff --git a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json index 2b8048409c..182c6b8282 100644 --- a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json +++ b/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", diff --git a/ui-ngx/src/assets/locale/locale.constant-de_DE.json b/ui-ngx/src/assets/locale/locale.constant-de_DE.json index 5b58389f7a..d7b1869bfd 100644 --- a/ui-ngx/src/assets/locale/locale.constant-de_DE.json +++ b/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", diff --git a/ui-ngx/src/assets/locale/locale.constant-el_GR.json b/ui-ngx/src/assets/locale/locale.constant-el_GR.json index 40ebe9e66f..2df1635fd2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-el_GR.json +++ b/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": "Όνομα", diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4ef7b845c7..54b7eca099 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index 6dce1d9be9..4d631a1028 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/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", diff --git a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json index 29b840d6f2..a5e16b23a0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json +++ b/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", diff --git a/ui-ngx/src/assets/locale/locale.constant-it_IT.json b/ui-ngx/src/assets/locale/locale.constant-it_IT.json index 8b86e87638..0ba48a9d6c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-it_IT.json +++ b/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", diff --git a/ui-ngx/src/assets/locale/locale.constant-ja_JA.json b/ui-ngx/src/assets/locale/locale.constant-ja_JA.json index f1651519ec..980daadaa6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ja_JA.json +++ b/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": "上", diff --git a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json b/ui-ngx/src/assets/locale/locale.constant-ko_KR.json index 1760334552..e5f09184b9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ko_KR.json +++ b/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": "상단", diff --git a/ui-ngx/src/assets/locale/locale.constant-ro_RO.json b/ui-ngx/src/assets/locale/locale.constant-ro_RO.json index 5a08ab42af..93326e512a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ro_RO.json +++ b/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", diff --git a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json index 0d6c9abdc9..bab457052c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json +++ b/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": "Верх", diff --git a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json b/ui-ngx/src/assets/locale/locale.constant-tr_TR.json index bb99098127..93f2133227 100644 --- a/ui-ngx/src/assets/locale/locale.constant-tr_TR.json +++ b/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", diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index fa0da05e0a..670aa58fc5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/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": "Угорі", diff --git a/ui/src/app/api/login.service.js b/ui/src/app/api/login.service.js index 0707070ed5..292268642f 100644 --- a/ui/src/app/api/login.service.js +++ b/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; + } } diff --git a/ui/src/app/app.run.js b/ui/src/app/app.run.js index 5e56a83eaf..3255c0a917 100644 --- a/ui/src/app/app.run.js +++ b/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', diff --git a/ui/src/app/locale/locale.constant-cs_CZ.json b/ui/src/app/locale/locale.constant-cs_CZ.json index 2a297cd9bb..13247f188a 100644 --- a/ui/src/app/locale/locale.constant-cs_CZ.json +++ b/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", diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index fa4689fc75..521669fd02 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/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", diff --git a/ui/src/app/locale/locale.constant-ru_RU.json b/ui/src/app/locale/locale.constant-ru_RU.json index e93c92124c..473698a091 100644 --- a/ui/src/app/locale/locale.constant-ru_RU.json +++ b/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": "Верх", diff --git a/ui/src/app/locale/locale.constant-uk_UA.json b/ui/src/app/locale/locale.constant-uk_UA.json index c19049bc11..79c2ece00f 100644 --- a/ui/src/app/locale/locale.constant-uk_UA.json +++ b/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": "Угорі", diff --git a/ui/src/app/login/login.scss b/ui/src/app/login/login.scss index a83b9c09ac..16520544fb 100644 --- a/ui/src/app/login/login.scss +++ b/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; + } + } } diff --git a/ui/src/app/login/login.tpl.html b/ui/src/app/login/login.tpl.html index 6e9d7d8977..e4f7f2559e 100644 --- a/ui/src/app/login/login.tpl.html +++ b/ui/src/app/login/login.tpl.html @@ -24,7 +24,7 @@ md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading">