diff --git a/application/pom.xml b/application/pom.xml index 21a319491d..55009709d1 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/ThingsboardOAuth2Configuration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardOAuth2Configuration.java new file mode 100644 index 0000000000..45048fddf3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardOAuth2Configuration.java @@ -0,0 +1,81 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +import java.util.Collections; + +@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true") +@Configuration +public class ThingsboardOAuth2Configuration { + + @Value("${security.oauth2.registrationId}") + private String registrationId; + @Value("${security.oauth2.userNameAttributeName}") + private String userNameAttributeName; + + @Value("${security.oauth2.client.clientId}") + private String clientId; + @Value("${security.oauth2.client.clientName}") + private String clientName; + @Value("${security.oauth2.client.clientSecret}") + private String clientSecret; + @Value("${security.oauth2.client.accessTokenUri}") + private String accessTokenUri; + @Value("${security.oauth2.client.authorizationUri}") + private String authorizationUri; + @Value("${security.oauth2.client.redirectUriTemplate}") + private String redirectUriTemplate; + @Value("${security.oauth2.client.scope}") + private String scope; + @Value("${security.oauth2.client.jwkSetUri}") + private String jwkSetUri; + @Value("${security.oauth2.client.authorizationGrantType}") + private String authorizationGrantType; + @Value("${security.oauth2.client.clientAuthenticationMethod}") + private String clientAuthenticationMethod; + + @Value("${security.oauth2.resource.userInfoUri}") + private String userInfoUri; + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + ClientRegistration registration = ClientRegistration.withRegistrationId(registrationId) + .clientId(clientId) + .authorizationUri(authorizationUri) + .clientSecret(clientSecret) + .tokenUri(accessTokenUri) + .redirectUriTemplate(redirectUriTemplate) + .scope(scope.split(",")) + .clientName(clientName) + .authorizationGrantType(new AuthorizationGrantType(authorizationGrantType)) + .userInfoUri(userInfoUri) + .userNameAttributeName(userNameAttributeName) + .jwkSetUri(jwkSetUri) + .clientAuthenticationMethod(new ClientAuthenticationMethod(clientAuthenticationMethod)) + .build(); + return new InMemoryClientRegistrationRepository(Collections.singletonList(registration)); + } +} \ No newline at end of file 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 53876f4040..36490b5166 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -18,6 +18,8 @@ package org.thingsboard.server.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Required; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.context.annotation.Bean; @@ -73,12 +75,25 @@ 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; + @Value("${security.oauth2.enabled}") + private boolean oauth2Enabled; + @Value("${security.oauth2.client.loginProcessingUrl}") + private String loginProcessingUrl; + @Autowired @Qualifier("jwtHeaderTokenExtractor") private TokenExtractor jwtHeaderTokenExtractor; @@ -189,6 +204,11 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); + if (oauth2Enabled) { + http.oauth2Login() + .loginProcessingUrl(loginProcessingUrl) + .successHandler(oauth2AuthenticationSuccessHandler); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth/Oauth2AuthenticationSuccessHandler.java new file mode 100644 index 0000000000..e99e5896e1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth/Oauth2AuthenticationSuccessHandler.java @@ -0,0 +1,125 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.oauth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; +import org.thingsboard.server.service.security.model.token.JwtToken; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; +import org.thingsboard.server.service.security.system.SystemSecurityService; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Component(value="oauth2AuthenticationSuccessHandler") +@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true") +public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final ObjectMapper mapper; + private final JwtTokenFactory tokenFactory; + private final RefreshTokenRepository refreshTokenRepository; + private final SystemSecurityService systemSecurityService; + private final UserService userService; + + @Autowired + public Oauth2AuthenticationSuccessHandler(final ObjectMapper mapper, + final JwtTokenFactory tokenFactory, + final RefreshTokenRepository refreshTokenRepository, + final UserService userService, + final SystemSecurityService systemSecurityService) { + this.mapper = mapper; + this.tokenFactory = tokenFactory; + this.refreshTokenRepository = refreshTokenRepository; + this.userService = userService; + this.systemSecurityService = systemSecurityService; + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + Object object = authentication.getPrincipal(); + + System.out.println(object); + + // active user check + + UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, "tenant@thingsboard.org"); + SecurityUser securityUser = (SecurityUser) authenticateByUsernameAndPassword(principal,"tenant@thingsboard.org", "tenant").getPrincipal(); + + JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); + JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); + + Map tokenMap = new HashMap(); + tokenMap.put("token", accessToken.getToken()); + tokenMap.put("refreshToken", refreshToken.getToken()); + +// response.setStatus(HttpStatus.OK.value()); +// response.setContentType(MediaType.APPLICATION_JSON_VALUE); +// mapper.writeValue(response.getWriter(), tokenMap); + + request.setAttribute("token", accessToken.getToken()); + response.addHeader("token", accessToken.getToken()); + + getRedirectStrategy().sendRedirect(request, response, "http://localhost:4200/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); + } + + private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) { + User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, username); + if (user == null) { + throw new UsernameNotFoundException("User not found: " + username); + } + + try { + + UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, user.getId()); + if (userCredentials == null) { + throw new UsernameNotFoundException("User credentials not found"); + } + + try { + systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, username, password); + } catch (LockedException e) { + throw e; + } + + if (user.getAuthority() == null) + throw new InsufficientAuthenticationException("User has no authority assigned"); + + SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); + return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); + } catch (Exception e) { + throw e; + } + } +} \ 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..d26b02174e 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/logback.xml b/application/src/main/resources/logback.xml index e25fb72ccb..9b14ff06f5 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -26,6 +26,7 @@ + diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index bf82c586c3..97e22fa2c6 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -97,6 +97,44 @@ 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: false + # oauth2: + # enabled: true + # registrationId: A + # userNameAttributeName: email + # client: + # clientName: Thingsboard Dev Test Q + # clientId: 5f5c0998-1d9b-4679-9610-6108fb91af2a + # clientSecret: h_kXVb7Ee1LgDDinix_nkAh_owWX7YCO783NNteF9AIOqlTWu2L03YoFjv5KL8yRVyx4uYAE-r_N3tFbupE8Kw + # accessTokenUri: https://federation-q.auth.schwarz/nidp/oauth/nam/token + # authorizationUri: https://federation-q.auth.schwarz/nidp/oauth/nam/authz + # scope: openid,profile,email,siam + # redirectUriTemplate: http://localhost:8080/login/oauth2/code/ + # loginProcessingUrl: /login/oauth2/code/ + # jwkSetUri: https://federation-q.auth.schwarz/nidp/oauth/nam/keys + # authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials + # clientAuthenticationMethod: post # basic, post + # resource: + # userInfoUri: https://federation-q.auth.schwarz/nidp/oauth/nam/userinfo + oauth2: + enabled: true + registrationId: A + userNameAttributeName: email + client: + clientName: Test app + clientId: dVH9reqyqiXIG7M2wmamb0ySue8zaM4g + clientSecret: EYAfAGxwkwoeYnb2o2cDgaWZB5k97OStpZQPPvcMMD-SVH2BuughTGeBazXtF5I6 + accessTokenUri: https://dev-r9m8ht0k.auth0.com/oauth/token + authorizationUri: https://dev-r9m8ht0k.auth0.com/authorize + scope: openid,profile,email + redirectUriTemplate: http://localhost:8080/login/oauth2/code/ + loginProcessingUrl: /login/oauth2/code/ + jwkSetUri: https://dev-r9m8ht0k.auth0.com/.well-known/jwks.json + authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials + clientAuthenticationMethod: post # basic, post + resource: + userInfoUri: https://dev-r9m8ht0k.auth0.com/userinfo # Dashboard parameters dashboard: diff --git a/pom.xml b/pom.xml index 494a43cb56..f263307538 100755 --- a/pom.xml +++ b/pom.xml @@ -458,6 +458,21 @@ spring-boot-starter-security ${spring-boot.version} + + org.springframework.cloud + spring-cloud-starter-oauth2 + ${spring-boot.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/src/app/login/login.tpl.html b/ui/src/app/login/login.tpl.html index 6e9d7d8977..409f5cd4ad 100644 --- a/ui/src/app/login/login.tpl.html +++ b/ui/src/app/login/login.tpl.html @@ -47,6 +47,7 @@ {{ 'login.login' | translate }} + OAUTH2 LOGIN