Commit a563fdab3f1a1e37338190bbf933d55268551fec

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

Added basic and custom OAuth2 user mappers

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