Commit 7fc46010b790f0453a14a70ffba77383351e8734
Committed by
Andrew Shvayka
1 parent
08ab9752
Added base impl for OAuth-2
Showing
9 changed files
with
295 additions
and
2 deletions
... | ... | @@ -133,6 +133,18 @@ |
133 | 133 | <artifactId>spring-boot-starter-websocket</artifactId> |
134 | 134 | </dependency> |
135 | 135 | <dependency> |
136 | + <groupId>org.springframework.cloud</groupId> | |
137 | + <artifactId>spring-cloud-starter-oauth2</artifactId> | |
138 | + </dependency> | |
139 | + <dependency> | |
140 | + <groupId>org.springframework.security</groupId> | |
141 | + <artifactId>spring-security-oauth2-client</artifactId> | |
142 | + </dependency> | |
143 | + <dependency> | |
144 | + <groupId>org.springframework.security</groupId> | |
145 | + <artifactId>spring-security-oauth2-jose</artifactId> | |
146 | + </dependency> | |
147 | + <dependency> | |
136 | 148 | <groupId>io.jsonwebtoken</groupId> |
137 | 149 | <artifactId>jjwt</artifactId> |
138 | 150 | </dependency> | ... | ... |
application/src/main/java/org/thingsboard/server/config/ThingsboardOAuth2Configuration.java
0 → 100644
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 | +} | |
\ No newline at end of file | ... | ... |
... | ... | @@ -18,6 +18,8 @@ package org.thingsboard.server.config; |
18 | 18 | import com.fasterxml.jackson.databind.ObjectMapper; |
19 | 19 | import org.springframework.beans.factory.annotation.Autowired; |
20 | 20 | import org.springframework.beans.factory.annotation.Qualifier; |
21 | +import org.springframework.beans.factory.annotation.Required; | |
22 | +import org.springframework.beans.factory.annotation.Value; | |
21 | 23 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
22 | 24 | import org.springframework.boot.autoconfigure.security.SecurityProperties; |
23 | 25 | import org.springframework.context.annotation.Bean; |
... | ... | @@ -73,12 +75,25 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt |
73 | 75 | public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**"; |
74 | 76 | |
75 | 77 | @Autowired private ThingsboardErrorResponseHandler restAccessDeniedHandler; |
76 | - @Autowired private AuthenticationSuccessHandler successHandler; | |
78 | + | |
79 | + @Autowired(required = false) | |
80 | + @Qualifier("oauth2AuthenticationSuccessHandler") | |
81 | + private AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler; | |
82 | + | |
83 | + @Autowired | |
84 | + @Qualifier("defaultAuthenticationSuccessHandler") | |
85 | + private AuthenticationSuccessHandler successHandler; | |
86 | + | |
77 | 87 | @Autowired private AuthenticationFailureHandler failureHandler; |
78 | 88 | @Autowired private RestAuthenticationProvider restAuthenticationProvider; |
79 | 89 | @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; |
80 | 90 | @Autowired private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider; |
81 | 91 | |
92 | + @Value("${security.oauth2.enabled}") | |
93 | + private boolean oauth2Enabled; | |
94 | + @Value("${security.oauth2.client.loginProcessingUrl}") | |
95 | + private String loginProcessingUrl; | |
96 | + | |
82 | 97 | @Autowired |
83 | 98 | @Qualifier("jwtHeaderTokenExtractor") |
84 | 99 | private TokenExtractor jwtHeaderTokenExtractor; |
... | ... | @@ -189,6 +204,11 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt |
189 | 204 | .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) |
190 | 205 | .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) |
191 | 206 | .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); |
207 | + if (oauth2Enabled) { | |
208 | + http.oauth2Login() | |
209 | + .loginProcessingUrl(loginProcessingUrl) | |
210 | + .successHandler(oauth2AuthenticationSuccessHandler); | |
211 | + } | |
192 | 212 | } |
193 | 213 | |
194 | 214 | ... | ... |
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 | +} | |
\ No newline at end of file | ... | ... |
... | ... | @@ -36,7 +36,7 @@ import java.io.IOException; |
36 | 36 | import java.util.HashMap; |
37 | 37 | import java.util.Map; |
38 | 38 | |
39 | -@Component | |
39 | +@Component(value="defaultAuthenticationSuccessHandler") | |
40 | 40 | public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler { |
41 | 41 | private final ObjectMapper mapper; |
42 | 42 | private final JwtTokenFactory tokenFactory; | ... | ... |
... | ... | @@ -26,6 +26,7 @@ |
26 | 26 | </appender> |
27 | 27 | |
28 | 28 | <logger name="org.thingsboard.server" level="INFO" /> |
29 | + <logger name="org.springframework.security" level="DEBUG" /> | |
29 | 30 | <logger name="akka" level="INFO" /> |
30 | 31 | |
31 | 32 | <!-- <logger name="org.thingsboard.server.service.queue" level="TRACE" />--> | ... | ... |
... | ... | @@ -97,6 +97,44 @@ security: |
97 | 97 | allowClaimingByDefault: "${SECURITY_CLAIM_ALLOW_CLAIMING_BY_DEFAULT:true}" |
98 | 98 | # Time allowed to claim the device in milliseconds |
99 | 99 | duration: "${SECURITY_CLAIM_DURATION:60000}" # 1 minute, note this value must equal claimDevices.timeToLiveInMinutes value |
100 | + basic: | |
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: | |
125 | + clientName: Test app | |
126 | + clientId: dVH9reqyqiXIG7M2wmamb0ySue8zaM4g | |
127 | + clientSecret: EYAfAGxwkwoeYnb2o2cDgaWZB5k97OStpZQPPvcMMD-SVH2BuughTGeBazXtF5I6 | |
128 | + accessTokenUri: https://dev-r9m8ht0k.auth0.com/oauth/token | |
129 | + authorizationUri: https://dev-r9m8ht0k.auth0.com/authorize | |
130 | + scope: openid,profile,email | |
131 | + redirectUriTemplate: http://localhost:8080/login/oauth2/code/ | |
132 | + loginProcessingUrl: /login/oauth2/code/ | |
133 | + jwkSetUri: https://dev-r9m8ht0k.auth0.com/.well-known/jwks.json | |
134 | + authorizationGrantType: authorization_code # authorization_code, implicit, refresh_token, client_credentials | |
135 | + clientAuthenticationMethod: post # basic, post | |
136 | + resource: | |
137 | + userInfoUri: https://dev-r9m8ht0k.auth0.com/userinfo | |
100 | 138 | |
101 | 139 | # Dashboard parameters |
102 | 140 | dashboard: | ... | ... |
... | ... | @@ -459,6 +459,21 @@ |
459 | 459 | <version>${spring-boot.version}</version> |
460 | 460 | </dependency> |
461 | 461 | <dependency> |
462 | + <groupId>org.springframework.cloud</groupId> | |
463 | + <artifactId>spring-cloud-starter-oauth2</artifactId> | |
464 | + <version>${spring-boot.version}</version> | |
465 | + </dependency> | |
466 | + <dependency> | |
467 | + <groupId>org.springframework.security</groupId> | |
468 | + <artifactId>spring-security-oauth2-client</artifactId> | |
469 | + <version>${spring.version}</version> | |
470 | + </dependency> | |
471 | + <dependency> | |
472 | + <groupId>org.springframework.security</groupId> | |
473 | + <artifactId>spring-security-oauth2-jose</artifactId> | |
474 | + <version>${spring.version}</version> | |
475 | + </dependency> | |
476 | + <dependency> | |
462 | 477 | <groupId>org.springframework.boot</groupId> |
463 | 478 | <artifactId>spring-boot-starter-web</artifactId> |
464 | 479 | <version>${spring-boot.version}</version> | ... | ... |