Commit bc6efa5e1e339b07e8b20ac2b59d26eb2713cd4f
Committed by
GitHub
1 parent
e30ec49d
[3.3] [PROD-685] Provide user's session expiration when his auth data is changed (#4201)
* Provide user's session expiration when his auth data is changed * Provide mock TokenOutdatingService for dao tests * Increase time gap when checking if token is outdated * Add license header for TokenOutdatingTest * Refactor tokens outdating functionality to events usage * Reset tokens on front-end after changing password
Showing
20 changed files
with
427 additions
and
122 deletions
@@ -17,7 +17,7 @@ package org.thingsboard.server.config; | @@ -17,7 +17,7 @@ package org.thingsboard.server.config; | ||
17 | 17 | ||
18 | import org.springframework.boot.context.properties.ConfigurationProperties; | 18 | import org.springframework.boot.context.properties.ConfigurationProperties; |
19 | import org.springframework.context.annotation.Configuration; | 19 | import org.springframework.context.annotation.Configuration; |
20 | -import org.thingsboard.server.service.security.model.token.JwtToken; | 20 | +import org.thingsboard.server.common.data.security.model.JwtToken; |
21 | 21 | ||
22 | @Configuration | 22 | @Configuration |
23 | @ConfigurationProperties(prefix = "security.jwt") | 23 | @ConfigurationProperties(prefix = "security.jwt") |
@@ -18,8 +18,9 @@ package org.thingsboard.server.controller; | @@ -18,8 +18,9 @@ package org.thingsboard.server.controller; | ||
18 | import com.fasterxml.jackson.databind.JsonNode; | 18 | import com.fasterxml.jackson.databind.JsonNode; |
19 | import com.fasterxml.jackson.databind.ObjectMapper; | 19 | import com.fasterxml.jackson.databind.ObjectMapper; |
20 | import com.fasterxml.jackson.databind.node.ObjectNode; | 20 | import com.fasterxml.jackson.databind.node.ObjectNode; |
21 | +import lombok.RequiredArgsConstructor; | ||
21 | import lombok.extern.slf4j.Slf4j; | 22 | import lombok.extern.slf4j.Slf4j; |
22 | -import org.springframework.beans.factory.annotation.Autowired; | 23 | +import org.springframework.context.ApplicationEventPublisher; |
23 | import org.springframework.http.HttpHeaders; | 24 | import org.springframework.http.HttpHeaders; |
24 | import org.springframework.http.HttpStatus; | 25 | import org.springframework.http.HttpStatus; |
25 | import org.springframework.http.ResponseEntity; | 26 | import org.springframework.http.ResponseEntity; |
@@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.RequestParam; | @@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.RequestParam; | ||
32 | import org.springframework.web.bind.annotation.ResponseBody; | 33 | import org.springframework.web.bind.annotation.ResponseBody; |
33 | import org.springframework.web.bind.annotation.ResponseStatus; | 34 | import org.springframework.web.bind.annotation.ResponseStatus; |
34 | import org.springframework.web.bind.annotation.RestController; | 35 | import org.springframework.web.bind.annotation.RestController; |
36 | +import org.thingsboard.common.util.JacksonUtil; | ||
35 | import org.thingsboard.rule.engine.api.MailService; | 37 | import org.thingsboard.rule.engine.api.MailService; |
36 | import org.thingsboard.server.common.data.User; | 38 | import org.thingsboard.server.common.data.User; |
37 | import org.thingsboard.server.common.data.audit.ActionType; | 39 | import org.thingsboard.server.common.data.audit.ActionType; |
@@ -39,48 +41,37 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; | @@ -39,48 +41,37 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; | ||
39 | import org.thingsboard.server.common.data.exception.ThingsboardException; | 41 | import org.thingsboard.server.common.data.exception.ThingsboardException; |
40 | import org.thingsboard.server.common.data.id.TenantId; | 42 | import org.thingsboard.server.common.data.id.TenantId; |
41 | import org.thingsboard.server.common.data.security.UserCredentials; | 43 | import org.thingsboard.server.common.data.security.UserCredentials; |
44 | +import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; | ||
45 | +import org.thingsboard.server.common.data.security.model.JwtToken; | ||
46 | +import org.thingsboard.server.common.data.security.model.SecuritySettings; | ||
47 | +import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; | ||
42 | import org.thingsboard.server.dao.audit.AuditLogService; | 48 | import org.thingsboard.server.dao.audit.AuditLogService; |
43 | import org.thingsboard.server.queue.util.TbCoreComponent; | 49 | import org.thingsboard.server.queue.util.TbCoreComponent; |
44 | import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; | 50 | import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; |
45 | import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; | 51 | import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; |
46 | -import org.thingsboard.server.common.data.security.model.SecuritySettings; | ||
47 | import org.thingsboard.server.service.security.model.SecurityUser; | 52 | import org.thingsboard.server.service.security.model.SecurityUser; |
48 | -import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; | ||
49 | import org.thingsboard.server.service.security.model.UserPrincipal; | 53 | import org.thingsboard.server.service.security.model.UserPrincipal; |
50 | -import org.thingsboard.server.service.security.model.token.JwtToken; | ||
51 | import org.thingsboard.server.service.security.model.token.JwtTokenFactory; | 54 | import org.thingsboard.server.service.security.model.token.JwtTokenFactory; |
52 | import org.thingsboard.server.service.security.system.SystemSecurityService; | 55 | import org.thingsboard.server.service.security.system.SystemSecurityService; |
53 | -import org.thingsboard.server.utils.MiscUtils; | ||
54 | import ua_parser.Client; | 56 | import ua_parser.Client; |
55 | 57 | ||
56 | import javax.servlet.http.HttpServletRequest; | 58 | import javax.servlet.http.HttpServletRequest; |
57 | import java.net.URI; | 59 | import java.net.URI; |
58 | import java.net.URISyntaxException; | 60 | import java.net.URISyntaxException; |
59 | -import java.util.List; | ||
60 | 61 | ||
61 | @RestController | 62 | @RestController |
62 | @TbCoreComponent | 63 | @TbCoreComponent |
63 | @RequestMapping("/api") | 64 | @RequestMapping("/api") |
64 | @Slf4j | 65 | @Slf4j |
66 | +@RequiredArgsConstructor | ||
65 | public class AuthController extends BaseController { | 67 | public class AuthController extends BaseController { |
66 | - | ||
67 | - @Autowired | ||
68 | - private BCryptPasswordEncoder passwordEncoder; | ||
69 | - | ||
70 | - @Autowired | ||
71 | - private JwtTokenFactory tokenFactory; | ||
72 | - | ||
73 | - @Autowired | ||
74 | - private RefreshTokenRepository refreshTokenRepository; | ||
75 | - | ||
76 | - @Autowired | ||
77 | - private MailService mailService; | ||
78 | - | ||
79 | - @Autowired | ||
80 | - private SystemSecurityService systemSecurityService; | ||
81 | - | ||
82 | - @Autowired | ||
83 | - private AuditLogService auditLogService; | 68 | + private final BCryptPasswordEncoder passwordEncoder; |
69 | + private final JwtTokenFactory tokenFactory; | ||
70 | + private final RefreshTokenRepository refreshTokenRepository; | ||
71 | + private final MailService mailService; | ||
72 | + private final SystemSecurityService systemSecurityService; | ||
73 | + private final AuditLogService auditLogService; | ||
74 | + private final ApplicationEventPublisher eventPublisher; | ||
84 | 75 | ||
85 | @PreAuthorize("isAuthenticated()") | 76 | @PreAuthorize("isAuthenticated()") |
86 | @RequestMapping(value = "/auth/user", method = RequestMethod.GET) | 77 | @RequestMapping(value = "/auth/user", method = RequestMethod.GET) |
@@ -103,8 +94,7 @@ public class AuthController extends BaseController { | @@ -103,8 +94,7 @@ public class AuthController extends BaseController { | ||
103 | @PreAuthorize("isAuthenticated()") | 94 | @PreAuthorize("isAuthenticated()") |
104 | @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST) | 95 | @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST) |
105 | @ResponseStatus(value = HttpStatus.OK) | 96 | @ResponseStatus(value = HttpStatus.OK) |
106 | - public void changePassword ( | ||
107 | - @RequestBody JsonNode changePasswordRequest) throws ThingsboardException { | 97 | + public ObjectNode changePassword(@RequestBody JsonNode changePasswordRequest) throws ThingsboardException { |
108 | try { | 98 | try { |
109 | String currentPassword = changePasswordRequest.get("currentPassword").asText(); | 99 | String currentPassword = changePasswordRequest.get("currentPassword").asText(); |
110 | String newPassword = changePasswordRequest.get("newPassword").asText(); | 100 | String newPassword = changePasswordRequest.get("newPassword").asText(); |
@@ -119,6 +109,12 @@ public class AuthController extends BaseController { | @@ -119,6 +109,12 @@ public class AuthController extends BaseController { | ||
119 | } | 109 | } |
120 | userCredentials.setPassword(passwordEncoder.encode(newPassword)); | 110 | userCredentials.setPassword(passwordEncoder.encode(newPassword)); |
121 | userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials); | 111 | userService.replaceUserCredentials(securityUser.getTenantId(), userCredentials); |
112 | + | ||
113 | + eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId())); | ||
114 | + ObjectNode response = JacksonUtil.newObjectNode(); | ||
115 | + response.put("token", tokenFactory.createAccessJwtToken(securityUser).getToken()); | ||
116 | + response.put("refreshToken", tokenFactory.createRefreshToken(securityUser).getToken()); | ||
117 | + return response; | ||
122 | } catch (Exception e) { | 118 | } catch (Exception e) { |
123 | throw handleException(e); | 119 | throw handleException(e); |
124 | } | 120 | } |
@@ -135,7 +131,7 @@ public class AuthController extends BaseController { | @@ -135,7 +131,7 @@ public class AuthController extends BaseController { | ||
135 | throw handleException(e); | 131 | throw handleException(e); |
136 | } | 132 | } |
137 | } | 133 | } |
138 | - | 134 | + |
139 | @RequestMapping(value = "/noauth/activate", params = { "activateToken" }, method = RequestMethod.GET) | 135 | @RequestMapping(value = "/noauth/activate", params = { "activateToken" }, method = RequestMethod.GET) |
140 | public ResponseEntity<String> checkActivateToken( | 136 | public ResponseEntity<String> checkActivateToken( |
141 | @RequestParam(value = "activateToken") String activateToken) { | 137 | @RequestParam(value = "activateToken") String activateToken) { |
@@ -157,7 +153,7 @@ public class AuthController extends BaseController { | @@ -157,7 +153,7 @@ public class AuthController extends BaseController { | ||
157 | } | 153 | } |
158 | return new ResponseEntity<>(headers, responseStatus); | 154 | return new ResponseEntity<>(headers, responseStatus); |
159 | } | 155 | } |
160 | - | 156 | + |
161 | @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST) | 157 | @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST) |
162 | @ResponseStatus(value = HttpStatus.OK) | 158 | @ResponseStatus(value = HttpStatus.OK) |
163 | public void requestResetPasswordByEmail ( | 159 | public void requestResetPasswordByEmail ( |
@@ -170,13 +166,13 @@ public class AuthController extends BaseController { | @@ -170,13 +166,13 @@ public class AuthController extends BaseController { | ||
170 | String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request); | 166 | String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request); |
171 | String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl, | 167 | String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl, |
172 | userCredentials.getResetToken()); | 168 | userCredentials.getResetToken()); |
173 | - | 169 | + |
174 | mailService.sendResetPasswordEmail(resetUrl, email); | 170 | mailService.sendResetPasswordEmail(resetUrl, email); |
175 | } catch (Exception e) { | 171 | } catch (Exception e) { |
176 | throw handleException(e); | 172 | throw handleException(e); |
177 | } | 173 | } |
178 | } | 174 | } |
179 | - | 175 | + |
180 | @RequestMapping(value = "/noauth/resetPassword", params = { "resetToken" }, method = RequestMethod.GET) | 176 | @RequestMapping(value = "/noauth/resetPassword", params = { "resetToken" }, method = RequestMethod.GET) |
181 | public ResponseEntity<String> checkResetToken( | 177 | public ResponseEntity<String> checkResetToken( |
182 | @RequestParam(value = "resetToken") String resetToken) { | 178 | @RequestParam(value = "resetToken") String resetToken) { |
@@ -198,7 +194,7 @@ public class AuthController extends BaseController { | @@ -198,7 +194,7 @@ public class AuthController extends BaseController { | ||
198 | } | 194 | } |
199 | return new ResponseEntity<>(headers, responseStatus); | 195 | return new ResponseEntity<>(headers, responseStatus); |
200 | } | 196 | } |
201 | - | 197 | + |
202 | @RequestMapping(value = "/noauth/activate", method = RequestMethod.POST) | 198 | @RequestMapping(value = "/noauth/activate", method = RequestMethod.POST) |
203 | @ResponseStatus(value = HttpStatus.OK) | 199 | @ResponseStatus(value = HttpStatus.OK) |
204 | @ResponseBody | 200 | @ResponseBody |
@@ -240,7 +236,7 @@ public class AuthController extends BaseController { | @@ -240,7 +236,7 @@ public class AuthController extends BaseController { | ||
240 | throw handleException(e); | 236 | throw handleException(e); |
241 | } | 237 | } |
242 | } | 238 | } |
243 | - | 239 | + |
244 | @RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST) | 240 | @RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST) |
245 | @ResponseStatus(value = HttpStatus.OK) | 241 | @ResponseStatus(value = HttpStatus.OK) |
246 | @ResponseBody | 242 | @ResponseBody |
@@ -268,6 +264,7 @@ public class AuthController extends BaseController { | @@ -268,6 +264,7 @@ public class AuthController extends BaseController { | ||
268 | String email = user.getEmail(); | 264 | String email = user.getEmail(); |
269 | mailService.sendPasswordWasResetEmail(loginUrl, email); | 265 | mailService.sendPasswordWasResetEmail(loginUrl, email); |
270 | 266 | ||
267 | + eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId())); | ||
271 | JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); | 268 | JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); |
272 | JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); | 269 | JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); |
273 | 270 |
@@ -19,8 +19,9 @@ import com.fasterxml.jackson.databind.JsonNode; | @@ -19,8 +19,9 @@ import com.fasterxml.jackson.databind.JsonNode; | ||
19 | import com.fasterxml.jackson.databind.ObjectMapper; | 19 | import com.fasterxml.jackson.databind.ObjectMapper; |
20 | import com.fasterxml.jackson.databind.node.ObjectNode; | 20 | import com.fasterxml.jackson.databind.node.ObjectNode; |
21 | import lombok.Getter; | 21 | import lombok.Getter; |
22 | -import org.springframework.beans.factory.annotation.Autowired; | 22 | +import lombok.RequiredArgsConstructor; |
23 | import org.springframework.beans.factory.annotation.Value; | 23 | import org.springframework.beans.factory.annotation.Value; |
24 | +import org.springframework.context.ApplicationEventPublisher; | ||
24 | import org.springframework.http.HttpStatus; | 25 | import org.springframework.http.HttpStatus; |
25 | import org.springframework.security.access.prepost.PreAuthorize; | 26 | import org.springframework.security.access.prepost.PreAuthorize; |
26 | import org.springframework.web.bind.annotation.PathVariable; | 27 | import org.springframework.web.bind.annotation.PathVariable; |
@@ -44,11 +45,12 @@ import org.thingsboard.server.common.data.page.PageData; | @@ -44,11 +45,12 @@ import org.thingsboard.server.common.data.page.PageData; | ||
44 | import org.thingsboard.server.common.data.page.PageLink; | 45 | import org.thingsboard.server.common.data.page.PageLink; |
45 | import org.thingsboard.server.common.data.security.Authority; | 46 | import org.thingsboard.server.common.data.security.Authority; |
46 | import org.thingsboard.server.common.data.security.UserCredentials; | 47 | import org.thingsboard.server.common.data.security.UserCredentials; |
48 | +import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; | ||
49 | +import org.thingsboard.server.common.data.security.model.JwtToken; | ||
47 | import org.thingsboard.server.queue.util.TbCoreComponent; | 50 | import org.thingsboard.server.queue.util.TbCoreComponent; |
48 | import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; | 51 | import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; |
49 | import org.thingsboard.server.service.security.model.SecurityUser; | 52 | import org.thingsboard.server.service.security.model.SecurityUser; |
50 | import org.thingsboard.server.service.security.model.UserPrincipal; | 53 | import org.thingsboard.server.service.security.model.UserPrincipal; |
51 | -import org.thingsboard.server.service.security.model.token.JwtToken; | ||
52 | import org.thingsboard.server.service.security.model.token.JwtTokenFactory; | 54 | import org.thingsboard.server.service.security.model.token.JwtTokenFactory; |
53 | import org.thingsboard.server.service.security.permission.Operation; | 55 | import org.thingsboard.server.service.security.permission.Operation; |
54 | import org.thingsboard.server.service.security.permission.Resource; | 56 | import org.thingsboard.server.service.security.permission.Resource; |
@@ -56,6 +58,7 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; | @@ -56,6 +58,7 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; | ||
56 | 58 | ||
57 | import javax.servlet.http.HttpServletRequest; | 59 | import javax.servlet.http.HttpServletRequest; |
58 | 60 | ||
61 | +@RequiredArgsConstructor | ||
59 | @RestController | 62 | @RestController |
60 | @TbCoreComponent | 63 | @TbCoreComponent |
61 | @RequestMapping("/api") | 64 | @RequestMapping("/api") |
@@ -69,18 +72,11 @@ public class UserController extends BaseController { | @@ -69,18 +72,11 @@ public class UserController extends BaseController { | ||
69 | @Getter | 72 | @Getter |
70 | private boolean userTokenAccessEnabled; | 73 | private boolean userTokenAccessEnabled; |
71 | 74 | ||
72 | - @Autowired | ||
73 | - private MailService mailService; | ||
74 | - | ||
75 | - @Autowired | ||
76 | - private JwtTokenFactory tokenFactory; | ||
77 | - | ||
78 | - @Autowired | ||
79 | - private RefreshTokenRepository refreshTokenRepository; | ||
80 | - | ||
81 | - @Autowired | ||
82 | - private SystemSecurityService systemSecurityService; | ||
83 | - | 75 | + private final MailService mailService; |
76 | + private final JwtTokenFactory tokenFactory; | ||
77 | + private final RefreshTokenRepository refreshTokenRepository; | ||
78 | + private final SystemSecurityService systemSecurityService; | ||
79 | + private final ApplicationEventPublisher eventPublisher; | ||
84 | 80 | ||
85 | @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") | 81 | @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
86 | @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET) | 82 | @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET) |
@@ -341,6 +337,10 @@ public class UserController extends BaseController { | @@ -341,6 +337,10 @@ public class UserController extends BaseController { | ||
341 | User user = checkUserId(userId, Operation.WRITE); | 337 | User user = checkUserId(userId, Operation.WRITE); |
342 | TenantId tenantId = getCurrentUser().getTenantId(); | 338 | TenantId tenantId = getCurrentUser().getTenantId(); |
343 | userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled); | 339 | userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled); |
340 | + | ||
341 | + if (!userCredentialsEnabled) { | ||
342 | + eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId)); | ||
343 | + } | ||
344 | } catch (Exception e) { | 344 | } catch (Exception e) { |
345 | throw handleException(e); | 345 | throw handleException(e); |
346 | } | 346 | } |
application/src/main/java/org/thingsboard/server/service/security/auth/TokenOutdatingService.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2021 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; | ||
17 | + | ||
18 | +import io.jsonwebtoken.Claims; | ||
19 | +import lombok.RequiredArgsConstructor; | ||
20 | +import org.springframework.cache.Cache; | ||
21 | +import org.springframework.cache.CacheManager; | ||
22 | +import org.springframework.context.event.EventListener; | ||
23 | +import org.springframework.stereotype.Service; | ||
24 | +import org.thingsboard.server.common.data.CacheConstants; | ||
25 | +import org.thingsboard.server.common.data.id.UserId; | ||
26 | +import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; | ||
27 | +import org.thingsboard.server.common.data.security.model.JwtToken; | ||
28 | +import org.thingsboard.server.config.JwtSettings; | ||
29 | +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; | ||
30 | + | ||
31 | +import javax.annotation.PostConstruct; | ||
32 | +import java.util.Optional; | ||
33 | + | ||
34 | +import static java.util.concurrent.TimeUnit.MILLISECONDS; | ||
35 | +import static java.util.concurrent.TimeUnit.SECONDS; | ||
36 | + | ||
37 | +@Service | ||
38 | +@RequiredArgsConstructor | ||
39 | +public class TokenOutdatingService { | ||
40 | + private final CacheManager cacheManager; | ||
41 | + private final JwtTokenFactory tokenFactory; | ||
42 | + private final JwtSettings jwtSettings; | ||
43 | + private Cache tokenOutdatageTimeCache; | ||
44 | + | ||
45 | + @PostConstruct | ||
46 | + protected void initCache() { | ||
47 | + tokenOutdatageTimeCache = cacheManager.getCache(CacheConstants.TOKEN_OUTDATAGE_TIME_CACHE); | ||
48 | + } | ||
49 | + | ||
50 | + @EventListener(classes = UserAuthDataChangedEvent.class) | ||
51 | + public void onUserAuthDataChanged(UserAuthDataChangedEvent userAuthDataChangedEvent) { | ||
52 | + outdateOldUserTokens(userAuthDataChangedEvent.getUserId()); | ||
53 | + } | ||
54 | + | ||
55 | + public boolean isOutdated(JwtToken token, UserId userId) { | ||
56 | + Claims claims = tokenFactory.parseTokenClaims(token).getBody(); | ||
57 | + long issueTime = claims.getIssuedAt().getTime(); | ||
58 | + | ||
59 | + return Optional.ofNullable(tokenOutdatageTimeCache.get(toKey(userId), Long.class)) | ||
60 | + .map(outdatageTime -> { | ||
61 | + if (System.currentTimeMillis() - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) { | ||
62 | + return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); | ||
63 | + } else { | ||
64 | + /* | ||
65 | + * Means that since the outdating has passed more than | ||
66 | + * the lifetime of refresh token (the longest lived) | ||
67 | + * and there is no need to store outdatage time anymore | ||
68 | + * as all the tokens issued before the outdatage time | ||
69 | + * are now expired by themselves | ||
70 | + * */ | ||
71 | + tokenOutdatageTimeCache.evict(toKey(userId)); | ||
72 | + return false; | ||
73 | + } | ||
74 | + }) | ||
75 | + .orElse(false); | ||
76 | + } | ||
77 | + | ||
78 | + public void outdateOldUserTokens(UserId userId) { | ||
79 | + tokenOutdatageTimeCache.put(toKey(userId), System.currentTimeMillis()); | ||
80 | + } | ||
81 | + | ||
82 | + private String toKey(UserId userId) { | ||
83 | + return userId.getId().toString(); | ||
84 | + } | ||
85 | +} |
@@ -15,31 +15,34 @@ | @@ -15,31 +15,34 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.server.service.security.auth.jwt; | 16 | package org.thingsboard.server.service.security.auth.jwt; |
17 | 17 | ||
18 | -import org.springframework.beans.factory.annotation.Autowired; | 18 | +import lombok.RequiredArgsConstructor; |
19 | import org.springframework.security.authentication.AuthenticationProvider; | 19 | import org.springframework.security.authentication.AuthenticationProvider; |
20 | import org.springframework.security.core.Authentication; | 20 | import org.springframework.security.core.Authentication; |
21 | import org.springframework.security.core.AuthenticationException; | 21 | import org.springframework.security.core.AuthenticationException; |
22 | import org.springframework.stereotype.Component; | 22 | import org.springframework.stereotype.Component; |
23 | +import org.thingsboard.server.service.security.auth.TokenOutdatingService; | ||
23 | import org.thingsboard.server.service.security.auth.JwtAuthenticationToken; | 24 | import org.thingsboard.server.service.security.auth.JwtAuthenticationToken; |
25 | +import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; | ||
24 | import org.thingsboard.server.service.security.model.SecurityUser; | 26 | import org.thingsboard.server.service.security.model.SecurityUser; |
25 | import org.thingsboard.server.service.security.model.token.JwtTokenFactory; | 27 | import org.thingsboard.server.service.security.model.token.JwtTokenFactory; |
26 | import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; | 28 | import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; |
27 | 29 | ||
28 | @Component | 30 | @Component |
29 | -@SuppressWarnings("unchecked") | 31 | +@RequiredArgsConstructor |
30 | public class JwtAuthenticationProvider implements AuthenticationProvider { | 32 | public class JwtAuthenticationProvider implements AuthenticationProvider { |
31 | 33 | ||
32 | private final JwtTokenFactory tokenFactory; | 34 | private final JwtTokenFactory tokenFactory; |
33 | - | ||
34 | - @Autowired | ||
35 | - public JwtAuthenticationProvider(JwtTokenFactory tokenFactory) { | ||
36 | - this.tokenFactory = tokenFactory; | ||
37 | - } | 35 | + private final TokenOutdatingService tokenOutdatingService; |
38 | 36 | ||
39 | @Override | 37 | @Override |
40 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { | 38 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
41 | RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); | 39 | RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); |
42 | SecurityUser securityUser = tokenFactory.parseAccessJwtToken(rawAccessToken); | 40 | SecurityUser securityUser = tokenFactory.parseAccessJwtToken(rawAccessToken); |
41 | + | ||
42 | + if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) { | ||
43 | + throw new JwtExpiredTokenException("Token is outdated"); | ||
44 | + } | ||
45 | + | ||
43 | return new JwtAuthenticationToken(securityUser); | 46 | return new JwtAuthenticationToken(securityUser); |
44 | } | 47 | } |
45 | 48 |
@@ -15,9 +15,10 @@ | @@ -15,9 +15,10 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.server.service.security.auth.jwt; | 16 | package org.thingsboard.server.service.security.auth.jwt; |
17 | 17 | ||
18 | -import org.springframework.beans.factory.annotation.Autowired; | 18 | +import lombok.RequiredArgsConstructor; |
19 | import org.springframework.security.authentication.AuthenticationProvider; | 19 | import org.springframework.security.authentication.AuthenticationProvider; |
20 | import org.springframework.security.authentication.BadCredentialsException; | 20 | import org.springframework.security.authentication.BadCredentialsException; |
21 | +import org.springframework.security.authentication.CredentialsExpiredException; | ||
21 | import org.springframework.security.authentication.DisabledException; | 22 | import org.springframework.security.authentication.DisabledException; |
22 | import org.springframework.security.authentication.InsufficientAuthenticationException; | 23 | import org.springframework.security.authentication.InsufficientAuthenticationException; |
23 | import org.springframework.security.core.Authentication; | 24 | import org.springframework.security.core.Authentication; |
@@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.id.EntityId; | @@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.id.EntityId; | ||
32 | import org.thingsboard.server.common.data.id.TenantId; | 33 | import org.thingsboard.server.common.data.id.TenantId; |
33 | import org.thingsboard.server.common.data.id.UserId; | 34 | import org.thingsboard.server.common.data.id.UserId; |
34 | import org.thingsboard.server.common.data.security.Authority; | 35 | import org.thingsboard.server.common.data.security.Authority; |
36 | +import org.thingsboard.server.service.security.auth.TokenOutdatingService; | ||
35 | import org.thingsboard.server.common.data.security.UserCredentials; | 37 | import org.thingsboard.server.common.data.security.UserCredentials; |
36 | import org.thingsboard.server.dao.customer.CustomerService; | 38 | import org.thingsboard.server.dao.customer.CustomerService; |
37 | import org.thingsboard.server.dao.user.UserService; | 39 | import org.thingsboard.server.dao.user.UserService; |
@@ -44,18 +46,12 @@ import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; | @@ -44,18 +46,12 @@ import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; | ||
44 | import java.util.UUID; | 46 | import java.util.UUID; |
45 | 47 | ||
46 | @Component | 48 | @Component |
49 | +@RequiredArgsConstructor | ||
47 | public class RefreshTokenAuthenticationProvider implements AuthenticationProvider { | 50 | public class RefreshTokenAuthenticationProvider implements AuthenticationProvider { |
48 | - | ||
49 | private final JwtTokenFactory tokenFactory; | 51 | private final JwtTokenFactory tokenFactory; |
50 | private final UserService userService; | 52 | private final UserService userService; |
51 | private final CustomerService customerService; | 53 | private final CustomerService customerService; |
52 | - | ||
53 | - @Autowired | ||
54 | - public RefreshTokenAuthenticationProvider(final UserService userService, final CustomerService customerService, final JwtTokenFactory tokenFactory) { | ||
55 | - this.userService = userService; | ||
56 | - this.customerService = customerService; | ||
57 | - this.tokenFactory = tokenFactory; | ||
58 | - } | 54 | + private final TokenOutdatingService tokenOutdatingService; |
59 | 55 | ||
60 | @Override | 56 | @Override |
61 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { | 57 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
@@ -63,12 +59,18 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide | @@ -63,12 +59,18 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide | ||
63 | RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); | 59 | RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials(); |
64 | SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken); | 60 | SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken); |
65 | UserPrincipal principal = unsafeUser.getUserPrincipal(); | 61 | UserPrincipal principal = unsafeUser.getUserPrincipal(); |
62 | + | ||
66 | SecurityUser securityUser; | 63 | SecurityUser securityUser; |
67 | if (principal.getType() == UserPrincipal.Type.USER_NAME) { | 64 | if (principal.getType() == UserPrincipal.Type.USER_NAME) { |
68 | securityUser = authenticateByUserId(unsafeUser.getId()); | 65 | securityUser = authenticateByUserId(unsafeUser.getId()); |
69 | } else { | 66 | } else { |
70 | securityUser = authenticateByPublicId(principal.getValue()); | 67 | securityUser = authenticateByPublicId(principal.getValue()); |
71 | } | 68 | } |
69 | + | ||
70 | + if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) { | ||
71 | + throw new CredentialsExpiredException("Token is outdated"); | ||
72 | + } | ||
73 | + | ||
72 | return new RefreshAuthenticationToken(securityUser); | 74 | return new RefreshAuthenticationToken(securityUser); |
73 | } | 75 | } |
74 | 76 | ||
@@ -91,7 +93,6 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide | @@ -91,7 +93,6 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide | ||
91 | if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); | 93 | if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); |
92 | 94 | ||
93 | UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); | 95 | UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); |
94 | - | ||
95 | SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); | 96 | SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); |
96 | 97 | ||
97 | return securityUser; | 98 | return securityUser; |
@@ -17,8 +17,8 @@ package org.thingsboard.server.service.security.auth.jwt; | @@ -17,8 +17,8 @@ package org.thingsboard.server.service.security.auth.jwt; | ||
17 | 17 | ||
18 | import org.springframework.beans.factory.annotation.Autowired; | 18 | import org.springframework.beans.factory.annotation.Autowired; |
19 | import org.springframework.stereotype.Component; | 19 | import org.springframework.stereotype.Component; |
20 | +import org.thingsboard.server.common.data.security.model.JwtToken; | ||
20 | import org.thingsboard.server.service.security.model.SecurityUser; | 21 | import org.thingsboard.server.service.security.model.SecurityUser; |
21 | -import org.thingsboard.server.service.security.model.token.JwtToken; | ||
22 | import org.thingsboard.server.service.security.model.token.JwtTokenFactory; | 22 | import org.thingsboard.server.service.security.model.token.JwtTokenFactory; |
23 | 23 | ||
24 | @Component | 24 | @Component |
@@ -26,13 +26,12 @@ import org.thingsboard.server.common.data.id.CustomerId; | @@ -26,13 +26,12 @@ import org.thingsboard.server.common.data.id.CustomerId; | ||
26 | import org.thingsboard.server.common.data.id.EntityId; | 26 | import org.thingsboard.server.common.data.id.EntityId; |
27 | import org.thingsboard.server.common.data.id.TenantId; | 27 | import org.thingsboard.server.common.data.id.TenantId; |
28 | import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; | 28 | import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo; |
29 | +import org.thingsboard.server.common.data.security.model.JwtToken; | ||
29 | import org.thingsboard.server.dao.oauth2.OAuth2Service; | 30 | import org.thingsboard.server.dao.oauth2.OAuth2Service; |
30 | import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; | 31 | import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; |
31 | import org.thingsboard.server.service.security.model.SecurityUser; | 32 | import org.thingsboard.server.service.security.model.SecurityUser; |
32 | -import org.thingsboard.server.service.security.model.token.JwtToken; | ||
33 | import org.thingsboard.server.service.security.model.token.JwtTokenFactory; | 33 | import org.thingsboard.server.service.security.model.token.JwtTokenFactory; |
34 | import org.thingsboard.server.service.security.system.SystemSecurityService; | 34 | import org.thingsboard.server.service.security.system.SystemSecurityService; |
35 | -import org.thingsboard.server.utils.MiscUtils; | ||
36 | 35 | ||
37 | import javax.servlet.http.HttpServletRequest; | 36 | import javax.servlet.http.HttpServletRequest; |
38 | import javax.servlet.http.HttpServletResponse; | 37 | import javax.servlet.http.HttpServletResponse; |
@@ -23,9 +23,9 @@ import org.springframework.security.core.Authentication; | @@ -23,9 +23,9 @@ import org.springframework.security.core.Authentication; | ||
23 | import org.springframework.security.web.WebAttributes; | 23 | import org.springframework.security.web.WebAttributes; |
24 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; | 24 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; |
25 | import org.springframework.stereotype.Component; | 25 | import org.springframework.stereotype.Component; |
26 | +import org.thingsboard.server.common.data.security.model.JwtToken; | ||
26 | import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; | 27 | 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.SecurityUser; |
28 | -import org.thingsboard.server.service.security.model.token.JwtToken; | ||
29 | import org.thingsboard.server.service.security.model.token.JwtTokenFactory; | 29 | import org.thingsboard.server.service.security.model.token.JwtTokenFactory; |
30 | 30 | ||
31 | import javax.servlet.ServletException; | 31 | import javax.servlet.ServletException; |
@@ -16,7 +16,7 @@ | @@ -16,7 +16,7 @@ | ||
16 | package org.thingsboard.server.service.security.exception; | 16 | package org.thingsboard.server.service.security.exception; |
17 | 17 | ||
18 | import org.springframework.security.core.AuthenticationException; | 18 | import org.springframework.security.core.AuthenticationException; |
19 | -import org.thingsboard.server.service.security.model.token.JwtToken; | 19 | +import org.thingsboard.server.common.data.security.model.JwtToken; |
20 | 20 | ||
21 | public class JwtExpiredTokenException extends AuthenticationException { | 21 | public class JwtExpiredTokenException extends AuthenticationException { |
22 | private static final long serialVersionUID = -5959543783324224864L; | 22 | private static final long serialVersionUID = -5959543783324224864L; |
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.model.token; | @@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.model.token; | ||
17 | 17 | ||
18 | import com.fasterxml.jackson.annotation.JsonIgnore; | 18 | import com.fasterxml.jackson.annotation.JsonIgnore; |
19 | import io.jsonwebtoken.Claims; | 19 | import io.jsonwebtoken.Claims; |
20 | +import org.thingsboard.server.common.data.security.model.JwtToken; | ||
20 | 21 | ||
21 | public final class AccessJwtToken implements JwtToken { | 22 | public final class AccessJwtToken implements JwtToken { |
22 | private final String rawToken; | 23 | private final String rawToken; |
@@ -16,18 +16,26 @@ | @@ -16,18 +16,26 @@ | ||
16 | package org.thingsboard.server.service.security.model.token; | 16 | package org.thingsboard.server.service.security.model.token; |
17 | 17 | ||
18 | import io.jsonwebtoken.Claims; | 18 | import io.jsonwebtoken.Claims; |
19 | +import io.jsonwebtoken.ExpiredJwtException; | ||
19 | import io.jsonwebtoken.Jws; | 20 | import io.jsonwebtoken.Jws; |
20 | import io.jsonwebtoken.Jwts; | 21 | import io.jsonwebtoken.Jwts; |
22 | +import io.jsonwebtoken.MalformedJwtException; | ||
21 | import io.jsonwebtoken.SignatureAlgorithm; | 23 | import io.jsonwebtoken.SignatureAlgorithm; |
24 | +import io.jsonwebtoken.SignatureException; | ||
25 | +import io.jsonwebtoken.UnsupportedJwtException; | ||
26 | +import lombok.extern.slf4j.Slf4j; | ||
22 | import org.apache.commons.lang3.StringUtils; | 27 | import org.apache.commons.lang3.StringUtils; |
23 | import org.springframework.beans.factory.annotation.Autowired; | 28 | import org.springframework.beans.factory.annotation.Autowired; |
29 | +import org.springframework.security.authentication.BadCredentialsException; | ||
24 | import org.springframework.security.core.GrantedAuthority; | 30 | import org.springframework.security.core.GrantedAuthority; |
25 | import org.springframework.stereotype.Component; | 31 | import org.springframework.stereotype.Component; |
26 | import org.thingsboard.server.common.data.id.CustomerId; | 32 | import org.thingsboard.server.common.data.id.CustomerId; |
27 | import org.thingsboard.server.common.data.id.TenantId; | 33 | import org.thingsboard.server.common.data.id.TenantId; |
28 | import org.thingsboard.server.common.data.id.UserId; | 34 | import org.thingsboard.server.common.data.id.UserId; |
29 | import org.thingsboard.server.common.data.security.Authority; | 35 | import org.thingsboard.server.common.data.security.Authority; |
36 | +import org.thingsboard.server.common.data.security.model.JwtToken; | ||
30 | import org.thingsboard.server.config.JwtSettings; | 37 | import org.thingsboard.server.config.JwtSettings; |
38 | +import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; | ||
31 | import org.thingsboard.server.service.security.model.SecurityUser; | 39 | import org.thingsboard.server.service.security.model.SecurityUser; |
32 | import org.thingsboard.server.service.security.model.UserPrincipal; | 40 | import org.thingsboard.server.service.security.model.UserPrincipal; |
33 | 41 | ||
@@ -39,6 +47,7 @@ import java.util.UUID; | @@ -39,6 +47,7 @@ import java.util.UUID; | ||
39 | import java.util.stream.Collectors; | 47 | import java.util.stream.Collectors; |
40 | 48 | ||
41 | @Component | 49 | @Component |
50 | +@Slf4j | ||
42 | public class JwtTokenFactory { | 51 | public class JwtTokenFactory { |
43 | 52 | ||
44 | private static final String SCOPES = "scopes"; | 53 | private static final String SCOPES = "scopes"; |
@@ -97,7 +106,7 @@ public class JwtTokenFactory { | @@ -97,7 +106,7 @@ public class JwtTokenFactory { | ||
97 | } | 106 | } |
98 | 107 | ||
99 | public SecurityUser parseAccessJwtToken(RawAccessJwtToken rawAccessToken) { | 108 | public SecurityUser parseAccessJwtToken(RawAccessJwtToken rawAccessToken) { |
100 | - Jws<Claims> jwsClaims = rawAccessToken.parseClaims(settings.getTokenSigningKey()); | 109 | + Jws<Claims> jwsClaims = parseTokenClaims(rawAccessToken); |
101 | Claims claims = jwsClaims.getBody(); | 110 | Claims claims = jwsClaims.getBody(); |
102 | String subject = claims.getSubject(); | 111 | String subject = claims.getSubject(); |
103 | @SuppressWarnings("unchecked") | 112 | @SuppressWarnings("unchecked") |
@@ -153,7 +162,7 @@ public class JwtTokenFactory { | @@ -153,7 +162,7 @@ public class JwtTokenFactory { | ||
153 | } | 162 | } |
154 | 163 | ||
155 | public SecurityUser parseRefreshToken(RawAccessJwtToken rawAccessToken) { | 164 | public SecurityUser parseRefreshToken(RawAccessJwtToken rawAccessToken) { |
156 | - Jws<Claims> jwsClaims = rawAccessToken.parseClaims(settings.getTokenSigningKey()); | 165 | + Jws<Claims> jwsClaims = parseTokenClaims(rawAccessToken); |
157 | Claims claims = jwsClaims.getBody(); | 166 | Claims claims = jwsClaims.getBody(); |
158 | String subject = claims.getSubject(); | 167 | String subject = claims.getSubject(); |
159 | @SuppressWarnings("unchecked") | 168 | @SuppressWarnings("unchecked") |
@@ -171,4 +180,17 @@ public class JwtTokenFactory { | @@ -171,4 +180,17 @@ public class JwtTokenFactory { | ||
171 | return securityUser; | 180 | return securityUser; |
172 | } | 181 | } |
173 | 182 | ||
183 | + public Jws<Claims> parseTokenClaims(JwtToken token) { | ||
184 | + try { | ||
185 | + return Jwts.parser() | ||
186 | + .setSigningKey(settings.getTokenSigningKey()) | ||
187 | + .parseClaimsJws(token.getToken()); | ||
188 | + } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { | ||
189 | + log.debug("Invalid JWT Token", ex); | ||
190 | + throw new BadCredentialsException("Invalid JWT token: ", ex); | ||
191 | + } catch (ExpiredJwtException expiredEx) { | ||
192 | + log.debug("JWT Token is expired", expiredEx); | ||
193 | + throw new JwtExpiredTokenException(token, "JWT Token expired", expiredEx); | ||
194 | + } | ||
195 | + } | ||
174 | } | 196 | } |
application/src/main/java/org/thingsboard/server/service/security/model/token/RawAccessJwtToken.java
@@ -15,22 +15,10 @@ | @@ -15,22 +15,10 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.server.service.security.model.token; | 16 | package org.thingsboard.server.service.security.model.token; |
17 | 17 | ||
18 | -import io.jsonwebtoken.Claims; | ||
19 | -import io.jsonwebtoken.ExpiredJwtException; | ||
20 | -import io.jsonwebtoken.Jws; | ||
21 | -import io.jsonwebtoken.Jwts; | ||
22 | -import io.jsonwebtoken.MalformedJwtException; | ||
23 | -import io.jsonwebtoken.SignatureException; | ||
24 | -import io.jsonwebtoken.UnsupportedJwtException; | ||
25 | -import lombok.extern.slf4j.Slf4j; | ||
26 | -import org.slf4j.Logger; | ||
27 | -import org.slf4j.LoggerFactory; | ||
28 | -import org.springframework.security.authentication.BadCredentialsException; | ||
29 | -import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; | 18 | +import org.thingsboard.server.common.data.security.model.JwtToken; |
30 | 19 | ||
31 | import java.io.Serializable; | 20 | import java.io.Serializable; |
32 | 21 | ||
33 | -@Slf4j | ||
34 | public class RawAccessJwtToken implements JwtToken, Serializable { | 22 | public class RawAccessJwtToken implements JwtToken, Serializable { |
35 | 23 | ||
36 | private static final long serialVersionUID = -797397445703066079L; | 24 | private static final long serialVersionUID = -797397445703066079L; |
@@ -41,25 +29,6 @@ public class RawAccessJwtToken implements JwtToken, Serializable { | @@ -41,25 +29,6 @@ public class RawAccessJwtToken implements JwtToken, Serializable { | ||
41 | this.token = token; | 29 | this.token = token; |
42 | } | 30 | } |
43 | 31 | ||
44 | - /** | ||
45 | - * Parses and validates JWT Token signature. | ||
46 | - * | ||
47 | - * @throws BadCredentialsException | ||
48 | - * @throws JwtExpiredTokenException | ||
49 | - * | ||
50 | - */ | ||
51 | - public Jws<Claims> parseClaims(String signingKey) { | ||
52 | - try { | ||
53 | - return Jwts.parser().setSigningKey(signingKey).parseClaimsJws(this.token); | ||
54 | - } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { | ||
55 | - log.debug("Invalid JWT Token", ex); | ||
56 | - throw new BadCredentialsException("Invalid JWT token: ", ex); | ||
57 | - } catch (ExpiredJwtException expiredEx) { | ||
58 | - log.debug("JWT Token is expired", expiredEx); | ||
59 | - throw new JwtExpiredTokenException(this, "JWT Token expired", expiredEx); | ||
60 | - } | ||
61 | - } | ||
62 | - | ||
63 | @Override | 32 | @Override |
64 | public String getToken() { | 33 | public String getToken() { |
65 | return token; | 34 | return token; |
@@ -362,6 +362,9 @@ caffeine: | @@ -362,6 +362,9 @@ caffeine: | ||
362 | attributes: | 362 | attributes: |
363 | timeToLiveInMinutes: 1440 | 363 | timeToLiveInMinutes: 1440 |
364 | maxSize: 100000 | 364 | maxSize: 100000 |
365 | + tokensOutdatageTime: | ||
366 | + timeToLiveInMinutes: 20000 | ||
367 | + maxSize: 10000 | ||
365 | 368 | ||
366 | redis: | 369 | redis: |
367 | # standalone or cluster | 370 | # standalone or cluster |
application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016-2021 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; | ||
17 | + | ||
18 | +import org.junit.jupiter.api.BeforeEach; | ||
19 | +import org.junit.jupiter.api.Test; | ||
20 | +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; | ||
21 | +import org.springframework.security.authentication.CredentialsExpiredException; | ||
22 | +import org.thingsboard.server.common.data.CacheConstants; | ||
23 | +import org.thingsboard.server.common.data.User; | ||
24 | +import org.thingsboard.server.common.data.id.UserId; | ||
25 | +import org.thingsboard.server.common.data.security.Authority; | ||
26 | +import org.thingsboard.server.common.data.security.UserCredentials; | ||
27 | +import org.thingsboard.server.common.data.security.model.JwtToken; | ||
28 | +import org.thingsboard.server.config.JwtSettings; | ||
29 | +import org.thingsboard.server.dao.customer.CustomerService; | ||
30 | +import org.thingsboard.server.dao.user.UserService; | ||
31 | +import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; | ||
32 | +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider; | ||
33 | +import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; | ||
34 | +import org.thingsboard.server.service.security.model.SecurityUser; | ||
35 | +import org.thingsboard.server.service.security.model.UserPrincipal; | ||
36 | +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; | ||
37 | +import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; | ||
38 | + | ||
39 | +import java.util.UUID; | ||
40 | + | ||
41 | +import static java.util.concurrent.TimeUnit.DAYS; | ||
42 | +import static java.util.concurrent.TimeUnit.MINUTES; | ||
43 | +import static java.util.concurrent.TimeUnit.SECONDS; | ||
44 | +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; | ||
45 | +import static org.junit.jupiter.api.Assertions.assertFalse; | ||
46 | +import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
47 | +import static org.junit.jupiter.api.Assertions.assertNull; | ||
48 | +import static org.junit.jupiter.api.Assertions.assertThrows; | ||
49 | +import static org.junit.jupiter.api.Assertions.assertTrue; | ||
50 | +import static org.mockito.ArgumentMatchers.any; | ||
51 | +import static org.mockito.ArgumentMatchers.eq; | ||
52 | +import static org.mockito.Mockito.mock; | ||
53 | +import static org.mockito.Mockito.when; | ||
54 | + | ||
55 | +public class TokenOutdatingTest { | ||
56 | + private JwtAuthenticationProvider accessTokenAuthenticationProvider; | ||
57 | + private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider; | ||
58 | + | ||
59 | + private TokenOutdatingService tokenOutdatingService; | ||
60 | + private ConcurrentMapCacheManager cacheManager; | ||
61 | + private JwtTokenFactory tokenFactory; | ||
62 | + private JwtSettings jwtSettings; | ||
63 | + | ||
64 | + private UserId userId; | ||
65 | + | ||
66 | + @BeforeEach | ||
67 | + public void setUp() { | ||
68 | + jwtSettings = new JwtSettings(); | ||
69 | + jwtSettings.setTokenIssuer("test.io"); | ||
70 | + jwtSettings.setTokenExpirationTime((int) MINUTES.toSeconds(10)); | ||
71 | + jwtSettings.setRefreshTokenExpTime((int) DAYS.toSeconds(7)); | ||
72 | + jwtSettings.setTokenSigningKey("secret"); | ||
73 | + tokenFactory = new JwtTokenFactory(jwtSettings); | ||
74 | + | ||
75 | + cacheManager = new ConcurrentMapCacheManager(); | ||
76 | + tokenOutdatingService = new TokenOutdatingService(cacheManager, tokenFactory, jwtSettings); | ||
77 | + tokenOutdatingService.initCache(); | ||
78 | + | ||
79 | + userId = new UserId(UUID.randomUUID()); | ||
80 | + | ||
81 | + UserService userService = mock(UserService.class); | ||
82 | + | ||
83 | + User user = new User(); | ||
84 | + user.setId(userId); | ||
85 | + user.setAuthority(Authority.TENANT_ADMIN); | ||
86 | + user.setEmail("email"); | ||
87 | + when(userService.findUserById(any(), eq(userId))).thenReturn(user); | ||
88 | + | ||
89 | + UserCredentials userCredentials = new UserCredentials(); | ||
90 | + userCredentials.setEnabled(true); | ||
91 | + when(userService.findUserCredentialsByUserId(any(), eq(userId))).thenReturn(userCredentials); | ||
92 | + | ||
93 | + accessTokenAuthenticationProvider = new JwtAuthenticationProvider(tokenFactory, tokenOutdatingService); | ||
94 | + refreshTokenAuthenticationProvider = new RefreshTokenAuthenticationProvider(tokenFactory, userService, mock(CustomerService.class), tokenOutdatingService); | ||
95 | + } | ||
96 | + | ||
97 | + @Test | ||
98 | + public void testOutdateOldUserTokens() throws Exception { | ||
99 | + JwtToken jwtToken = createAccessJwtToken(userId); | ||
100 | + | ||
101 | + SECONDS.sleep(1); // need to wait before outdating so that outdatage time is strictly after token issue time | ||
102 | + tokenOutdatingService.outdateOldUserTokens(userId); | ||
103 | + assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId)); | ||
104 | + | ||
105 | + SECONDS.sleep(1); | ||
106 | + | ||
107 | + JwtToken newJwtToken = tokenFactory.createAccessJwtToken(createMockSecurityUser(userId)); | ||
108 | + assertFalse(tokenOutdatingService.isOutdated(newJwtToken, userId)); | ||
109 | + } | ||
110 | + | ||
111 | + @Test | ||
112 | + public void testAuthenticateWithOutdatedAccessToken() throws InterruptedException { | ||
113 | + RawAccessJwtToken accessJwtToken = getRawJwtToken(createAccessJwtToken(userId)); | ||
114 | + | ||
115 | + assertDoesNotThrow(() -> { | ||
116 | + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); | ||
117 | + }); | ||
118 | + | ||
119 | + SECONDS.sleep(1); | ||
120 | + tokenOutdatingService.outdateOldUserTokens(userId); | ||
121 | + | ||
122 | + assertThrows(JwtExpiredTokenException.class, () -> { | ||
123 | + accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); | ||
124 | + }); | ||
125 | + } | ||
126 | + | ||
127 | + @Test | ||
128 | + public void testAuthenticateWithOutdatedRefreshToken() throws InterruptedException { | ||
129 | + RawAccessJwtToken refreshJwtToken = getRawJwtToken(createRefreshJwtToken(userId)); | ||
130 | + | ||
131 | + assertDoesNotThrow(() -> { | ||
132 | + refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); | ||
133 | + }); | ||
134 | + | ||
135 | + SECONDS.sleep(1); | ||
136 | + tokenOutdatingService.outdateOldUserTokens(userId); | ||
137 | + | ||
138 | + assertThrows(CredentialsExpiredException.class, () -> { | ||
139 | + refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); | ||
140 | + }); | ||
141 | + } | ||
142 | + | ||
143 | + @Test | ||
144 | + public void testTokensOutdatageTimeRemovalFromCache() throws Exception { | ||
145 | + JwtToken jwtToken = createAccessJwtToken(userId); | ||
146 | + | ||
147 | + SECONDS.sleep(1); | ||
148 | + tokenOutdatingService.outdateOldUserTokens(userId); | ||
149 | + | ||
150 | + int refreshTokenExpirationTime = 5; | ||
151 | + jwtSettings.setRefreshTokenExpTime(refreshTokenExpirationTime); | ||
152 | + | ||
153 | + SECONDS.sleep(refreshTokenExpirationTime - 2); | ||
154 | + | ||
155 | + assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId)); | ||
156 | + assertNotNull(cacheManager.getCache(CacheConstants.TOKEN_OUTDATAGE_TIME_CACHE).get(userId.getId().toString())); | ||
157 | + | ||
158 | + SECONDS.sleep(3); | ||
159 | + | ||
160 | + assertFalse(tokenOutdatingService.isOutdated(jwtToken, userId)); | ||
161 | + assertNull(cacheManager.getCache(CacheConstants.TOKEN_OUTDATAGE_TIME_CACHE).get(userId.getId().toString())); | ||
162 | + } | ||
163 | + | ||
164 | + private JwtToken createAccessJwtToken(UserId userId) { | ||
165 | + return tokenFactory.createAccessJwtToken(createMockSecurityUser(userId)); | ||
166 | + } | ||
167 | + | ||
168 | + private JwtToken createRefreshJwtToken(UserId userId) { | ||
169 | + return tokenFactory.createRefreshToken(createMockSecurityUser(userId)); | ||
170 | + } | ||
171 | + | ||
172 | + private RawAccessJwtToken getRawJwtToken(JwtToken token) { | ||
173 | + return new RawAccessJwtToken(token.getToken()); | ||
174 | + } | ||
175 | + | ||
176 | + private SecurityUser createMockSecurityUser(UserId userId) { | ||
177 | + SecurityUser securityUser = new SecurityUser(); | ||
178 | + securityUser.setEmail("email"); | ||
179 | + securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail())); | ||
180 | + securityUser.setAuthority(Authority.CUSTOMER_USER); | ||
181 | + securityUser.setId(userId); | ||
182 | + return securityUser; | ||
183 | + } | ||
184 | +} |
@@ -27,4 +27,5 @@ public class CacheConstants { | @@ -27,4 +27,5 @@ public class CacheConstants { | ||
27 | public static final String TENANT_PROFILE_CACHE = "tenantProfiles"; | 27 | public static final String TENANT_PROFILE_CACHE = "tenantProfiles"; |
28 | public static final String DEVICE_PROFILE_CACHE = "deviceProfiles"; | 28 | public static final String DEVICE_PROFILE_CACHE = "deviceProfiles"; |
29 | public static final String ATTRIBUTES_CACHE = "attributes"; | 29 | public static final String ATTRIBUTES_CACHE = "attributes"; |
30 | + public static final String TOKEN_OUTDATAGE_TIME_CACHE = "tokensOutdatageTime"; | ||
30 | } | 31 | } |
1 | +/** | ||
2 | + * Copyright © 2016-2021 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.security.event; | ||
17 | + | ||
18 | +import org.thingsboard.server.common.data.id.UserId; | ||
19 | + | ||
20 | +public class UserAuthDataChangedEvent { | ||
21 | + private final UserId userId; | ||
22 | + | ||
23 | + public UserAuthDataChangedEvent(UserId userId) { | ||
24 | + this.userId = userId; | ||
25 | + } | ||
26 | + | ||
27 | + public UserId getUserId() { | ||
28 | + return userId; | ||
29 | + } | ||
30 | +} |
common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtToken.java
renamed from
application/src/main/java/org/thingsboard/server/service/security/model/token/JwtToken.java
@@ -13,7 +13,7 @@ | @@ -13,7 +13,7 @@ | ||
13 | * See the License for the specific language governing permissions and | 13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. | 14 | * limitations under the License. |
15 | */ | 15 | */ |
16 | -package org.thingsboard.server.service.security.model.token; | 16 | +package org.thingsboard.server.common.data.security.model; |
17 | 17 | ||
18 | import java.io.Serializable; | 18 | import java.io.Serializable; |
19 | 19 |
@@ -22,8 +22,8 @@ import com.google.common.util.concurrent.ListenableFuture; | @@ -22,8 +22,8 @@ import com.google.common.util.concurrent.ListenableFuture; | ||
22 | import lombok.extern.slf4j.Slf4j; | 22 | import lombok.extern.slf4j.Slf4j; |
23 | import org.apache.commons.lang3.RandomStringUtils; | 23 | import org.apache.commons.lang3.RandomStringUtils; |
24 | import org.apache.commons.lang3.StringUtils; | 24 | import org.apache.commons.lang3.StringUtils; |
25 | -import org.springframework.beans.factory.annotation.Autowired; | ||
26 | import org.springframework.beans.factory.annotation.Value; | 25 | import org.springframework.beans.factory.annotation.Value; |
26 | +import org.springframework.context.ApplicationEventPublisher; | ||
27 | import org.springframework.context.annotation.Lazy; | 27 | import org.springframework.context.annotation.Lazy; |
28 | import org.springframework.stereotype.Service; | 28 | import org.springframework.stereotype.Service; |
29 | import org.thingsboard.server.common.data.Customer; | 29 | import org.thingsboard.server.common.data.Customer; |
@@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageData; | @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageData; | ||
38 | import org.thingsboard.server.common.data.page.PageLink; | 38 | import org.thingsboard.server.common.data.page.PageLink; |
39 | import org.thingsboard.server.common.data.security.Authority; | 39 | import org.thingsboard.server.common.data.security.Authority; |
40 | import org.thingsboard.server.common.data.security.UserCredentials; | 40 | import org.thingsboard.server.common.data.security.UserCredentials; |
41 | +import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; | ||
41 | import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; | 42 | import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; |
42 | import org.thingsboard.server.dao.customer.CustomerDao; | 43 | import org.thingsboard.server.dao.customer.CustomerDao; |
43 | import org.thingsboard.server.dao.entity.AbstractEntityService; | 44 | import org.thingsboard.server.dao.entity.AbstractEntityService; |
@@ -75,21 +76,26 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic | @@ -75,21 +76,26 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic | ||
75 | @Value("${security.user_login_case_sensitive:true}") | 76 | @Value("${security.user_login_case_sensitive:true}") |
76 | private boolean userLoginCaseSensitive; | 77 | private boolean userLoginCaseSensitive; |
77 | 78 | ||
78 | - @Autowired | ||
79 | - private UserDao userDao; | ||
80 | - | ||
81 | - @Autowired | ||
82 | - private UserCredentialsDao userCredentialsDao; | ||
83 | - | ||
84 | - @Autowired | ||
85 | - private TenantDao tenantDao; | ||
86 | - | ||
87 | - @Autowired | ||
88 | - private CustomerDao customerDao; | ||
89 | - | ||
90 | - @Autowired | ||
91 | - @Lazy | ||
92 | - private TbTenantProfileCache tenantProfileCache; | 79 | + private final UserDao userDao; |
80 | + private final UserCredentialsDao userCredentialsDao; | ||
81 | + private final TenantDao tenantDao; | ||
82 | + private final CustomerDao customerDao; | ||
83 | + private final TbTenantProfileCache tenantProfileCache; | ||
84 | + private final ApplicationEventPublisher eventPublisher; | ||
85 | + | ||
86 | + public UserServiceImpl(UserDao userDao, | ||
87 | + UserCredentialsDao userCredentialsDao, | ||
88 | + TenantDao tenantDao, | ||
89 | + CustomerDao customerDao, | ||
90 | + @Lazy TbTenantProfileCache tenantProfileCache, | ||
91 | + ApplicationEventPublisher eventPublisher) { | ||
92 | + this.userDao = userDao; | ||
93 | + this.userCredentialsDao = userCredentialsDao; | ||
94 | + this.tenantDao = tenantDao; | ||
95 | + this.customerDao = customerDao; | ||
96 | + this.tenantProfileCache = tenantProfileCache; | ||
97 | + this.eventPublisher = eventPublisher; | ||
98 | + } | ||
93 | 99 | ||
94 | @Override | 100 | @Override |
95 | public User findUserByEmail(TenantId tenantId, String email) { | 101 | public User findUserByEmail(TenantId tenantId, String email) { |
@@ -225,6 +231,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic | @@ -225,6 +231,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic | ||
225 | userCredentialsDao.removeById(tenantId, userCredentials.getUuidId()); | 231 | userCredentialsDao.removeById(tenantId, userCredentials.getUuidId()); |
226 | deleteEntityRelations(tenantId, userId); | 232 | deleteEntityRelations(tenantId, userId); |
227 | userDao.removeById(tenantId, userId.getId()); | 233 | userDao.removeById(tenantId, userId.getId()); |
234 | + eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId)); | ||
228 | } | 235 | } |
229 | 236 | ||
230 | @Override | 237 | @Override |
@@ -149,8 +149,11 @@ export class AuthService { | @@ -149,8 +149,11 @@ export class AuthService { | ||
149 | } | 149 | } |
150 | 150 | ||
151 | public changePassword(currentPassword: string, newPassword: string) { | 151 | public changePassword(currentPassword: string, newPassword: string) { |
152 | - return this.http.post('/api/auth/changePassword', | ||
153 | - {currentPassword, newPassword}, defaultHttpOptions()); | 152 | + return this.http.post('/api/auth/changePassword', {currentPassword, newPassword}, defaultHttpOptions()).pipe( |
153 | + tap((loginResponse: LoginResponse) => { | ||
154 | + this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, false); | ||
155 | + } | ||
156 | + )); | ||
154 | } | 157 | } |
155 | 158 | ||
156 | public activateByEmailCode(emailCode: string): Observable<LoginResponse> { | 159 | public activateByEmailCode(emailCode: string): Observable<LoginResponse> { |