Commit bc6efa5e1e339b07e8b20ac2b59d26eb2713cd4f

Authored by Viacheslav Klimov
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 17
18 18 import org.springframework.boot.context.properties.ConfigurationProperties;
19 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 22 @Configuration
23 23 @ConfigurationProperties(prefix = "security.jwt")
... ...
... ... @@ -18,8 +18,9 @@ package org.thingsboard.server.controller;
18 18 import com.fasterxml.jackson.databind.JsonNode;
19 19 import com.fasterxml.jackson.databind.ObjectMapper;
20 20 import com.fasterxml.jackson.databind.node.ObjectNode;
  21 +import lombok.RequiredArgsConstructor;
21 22 import lombok.extern.slf4j.Slf4j;
22   -import org.springframework.beans.factory.annotation.Autowired;
  23 +import org.springframework.context.ApplicationEventPublisher;
23 24 import org.springframework.http.HttpHeaders;
24 25 import org.springframework.http.HttpStatus;
25 26 import org.springframework.http.ResponseEntity;
... ... @@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.RequestParam;
32 33 import org.springframework.web.bind.annotation.ResponseBody;
33 34 import org.springframework.web.bind.annotation.ResponseStatus;
34 35 import org.springframework.web.bind.annotation.RestController;
  36 +import org.thingsboard.common.util.JacksonUtil;
35 37 import org.thingsboard.rule.engine.api.MailService;
36 38 import org.thingsboard.server.common.data.User;
37 39 import org.thingsboard.server.common.data.audit.ActionType;
... ... @@ -39,48 +41,37 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
39 41 import org.thingsboard.server.common.data.exception.ThingsboardException;
40 42 import org.thingsboard.server.common.data.id.TenantId;
41 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 48 import org.thingsboard.server.dao.audit.AuditLogService;
43 49 import org.thingsboard.server.queue.util.TbCoreComponent;
44 50 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
45 51 import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
46   -import org.thingsboard.server.common.data.security.model.SecuritySettings;
47 52 import org.thingsboard.server.service.security.model.SecurityUser;
48   -import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
49 53 import org.thingsboard.server.service.security.model.UserPrincipal;
50   -import org.thingsboard.server.service.security.model.token.JwtToken;
51 54 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
52 55 import org.thingsboard.server.service.security.system.SystemSecurityService;
53   -import org.thingsboard.server.utils.MiscUtils;
54 56 import ua_parser.Client;
55 57
56 58 import javax.servlet.http.HttpServletRequest;
57 59 import java.net.URI;
58 60 import java.net.URISyntaxException;
59   -import java.util.List;
60 61
61 62 @RestController
62 63 @TbCoreComponent
63 64 @RequestMapping("/api")
64 65 @Slf4j
  66 +@RequiredArgsConstructor
65 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 76 @PreAuthorize("isAuthenticated()")
86 77 @RequestMapping(value = "/auth/user", method = RequestMethod.GET)
... ... @@ -103,8 +94,7 @@ public class AuthController extends BaseController {
103 94 @PreAuthorize("isAuthenticated()")
104 95 @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
105 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 98 try {
109 99 String currentPassword = changePasswordRequest.get("currentPassword").asText();
110 100 String newPassword = changePasswordRequest.get("newPassword").asText();
... ... @@ -119,6 +109,12 @@ public class AuthController extends BaseController {
119 109 }
120 110 userCredentials.setPassword(passwordEncoder.encode(newPassword));
121 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 118 } catch (Exception e) {
123 119 throw handleException(e);
124 120 }
... ... @@ -135,7 +131,7 @@ public class AuthController extends BaseController {
135 131 throw handleException(e);
136 132 }
137 133 }
138   -
  134 +
139 135 @RequestMapping(value = "/noauth/activate", params = { "activateToken" }, method = RequestMethod.GET)
140 136 public ResponseEntity<String> checkActivateToken(
141 137 @RequestParam(value = "activateToken") String activateToken) {
... ... @@ -157,7 +153,7 @@ public class AuthController extends BaseController {
157 153 }
158 154 return new ResponseEntity<>(headers, responseStatus);
159 155 }
160   -
  156 +
161 157 @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
162 158 @ResponseStatus(value = HttpStatus.OK)
163 159 public void requestResetPasswordByEmail (
... ... @@ -170,13 +166,13 @@ public class AuthController extends BaseController {
170 166 String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
171 167 String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
172 168 userCredentials.getResetToken());
173   -
  169 +
174 170 mailService.sendResetPasswordEmail(resetUrl, email);
175 171 } catch (Exception e) {
176 172 throw handleException(e);
177 173 }
178 174 }
179   -
  175 +
180 176 @RequestMapping(value = "/noauth/resetPassword", params = { "resetToken" }, method = RequestMethod.GET)
181 177 public ResponseEntity<String> checkResetToken(
182 178 @RequestParam(value = "resetToken") String resetToken) {
... ... @@ -198,7 +194,7 @@ public class AuthController extends BaseController {
198 194 }
199 195 return new ResponseEntity<>(headers, responseStatus);
200 196 }
201   -
  197 +
202 198 @RequestMapping(value = "/noauth/activate", method = RequestMethod.POST)
203 199 @ResponseStatus(value = HttpStatus.OK)
204 200 @ResponseBody
... ... @@ -240,7 +236,7 @@ public class AuthController extends BaseController {
240 236 throw handleException(e);
241 237 }
242 238 }
243   -
  239 +
244 240 @RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST)
245 241 @ResponseStatus(value = HttpStatus.OK)
246 242 @ResponseBody
... ... @@ -268,6 +264,7 @@ public class AuthController extends BaseController {
268 264 String email = user.getEmail();
269 265 mailService.sendPasswordWasResetEmail(loginUrl, email);
270 266
  267 + eventPublisher.publishEvent(new UserAuthDataChangedEvent(securityUser.getId()));
271 268 JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
272 269 JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
273 270
... ...
... ... @@ -19,8 +19,9 @@ import com.fasterxml.jackson.databind.JsonNode;
19 19 import com.fasterxml.jackson.databind.ObjectMapper;
20 20 import com.fasterxml.jackson.databind.node.ObjectNode;
21 21 import lombok.Getter;
22   -import org.springframework.beans.factory.annotation.Autowired;
  22 +import lombok.RequiredArgsConstructor;
23 23 import org.springframework.beans.factory.annotation.Value;
  24 +import org.springframework.context.ApplicationEventPublisher;
24 25 import org.springframework.http.HttpStatus;
25 26 import org.springframework.security.access.prepost.PreAuthorize;
26 27 import org.springframework.web.bind.annotation.PathVariable;
... ... @@ -44,11 +45,12 @@ import org.thingsboard.server.common.data.page.PageData;
44 45 import org.thingsboard.server.common.data.page.PageLink;
45 46 import org.thingsboard.server.common.data.security.Authority;
46 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 50 import org.thingsboard.server.queue.util.TbCoreComponent;
48 51 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
49 52 import org.thingsboard.server.service.security.model.SecurityUser;
50 53 import org.thingsboard.server.service.security.model.UserPrincipal;
51   -import org.thingsboard.server.service.security.model.token.JwtToken;
52 54 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
53 55 import org.thingsboard.server.service.security.permission.Operation;
54 56 import org.thingsboard.server.service.security.permission.Resource;
... ... @@ -56,6 +58,7 @@ import org.thingsboard.server.service.security.system.SystemSecurityService;
56 58
57 59 import javax.servlet.http.HttpServletRequest;
58 60
  61 +@RequiredArgsConstructor
59 62 @RestController
60 63 @TbCoreComponent
61 64 @RequestMapping("/api")
... ... @@ -69,18 +72,11 @@ public class UserController extends BaseController {
69 72 @Getter
70 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 81 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
86 82 @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET)
... ... @@ -341,6 +337,10 @@ public class UserController extends BaseController {
341 337 User user = checkUserId(userId, Operation.WRITE);
342 338 TenantId tenantId = getCurrentUser().getTenantId();
343 339 userService.setUserCredentialsEnabled(tenantId, userId, userCredentialsEnabled);
  340 +
  341 + if (!userCredentialsEnabled) {
  342 + eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId));
  343 + }
344 344 } catch (Exception e) {
345 345 throw handleException(e);
346 346 }
... ...
  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 15 */
16 16 package org.thingsboard.server.service.security.auth.jwt;
17 17
18   -import org.springframework.beans.factory.annotation.Autowired;
  18 +import lombok.RequiredArgsConstructor;
19 19 import org.springframework.security.authentication.AuthenticationProvider;
20 20 import org.springframework.security.core.Authentication;
21 21 import org.springframework.security.core.AuthenticationException;
22 22 import org.springframework.stereotype.Component;
  23 +import org.thingsboard.server.service.security.auth.TokenOutdatingService;
23 24 import org.thingsboard.server.service.security.auth.JwtAuthenticationToken;
  25 +import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
24 26 import org.thingsboard.server.service.security.model.SecurityUser;
25 27 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
26 28 import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
27 29
28 30 @Component
29   -@SuppressWarnings("unchecked")
  31 +@RequiredArgsConstructor
30 32 public class JwtAuthenticationProvider implements AuthenticationProvider {
31 33
32 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 37 @Override
40 38 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
41 39 RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
42 40 SecurityUser securityUser = tokenFactory.parseAccessJwtToken(rawAccessToken);
  41 +
  42 + if (tokenOutdatingService.isOutdated(rawAccessToken, securityUser.getId())) {
  43 + throw new JwtExpiredTokenException("Token is outdated");
  44 + }
  45 +
43 46 return new JwtAuthenticationToken(securityUser);
44 47 }
45 48
... ...
... ... @@ -15,9 +15,10 @@
15 15 */
16 16 package org.thingsboard.server.service.security.auth.jwt;
17 17
18   -import org.springframework.beans.factory.annotation.Autowired;
  18 +import lombok.RequiredArgsConstructor;
19 19 import org.springframework.security.authentication.AuthenticationProvider;
20 20 import org.springframework.security.authentication.BadCredentialsException;
  21 +import org.springframework.security.authentication.CredentialsExpiredException;
21 22 import org.springframework.security.authentication.DisabledException;
22 23 import org.springframework.security.authentication.InsufficientAuthenticationException;
23 24 import org.springframework.security.core.Authentication;
... ... @@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.id.EntityId;
32 33 import org.thingsboard.server.common.data.id.TenantId;
33 34 import org.thingsboard.server.common.data.id.UserId;
34 35 import org.thingsboard.server.common.data.security.Authority;
  36 +import org.thingsboard.server.service.security.auth.TokenOutdatingService;
35 37 import org.thingsboard.server.common.data.security.UserCredentials;
36 38 import org.thingsboard.server.dao.customer.CustomerService;
37 39 import org.thingsboard.server.dao.user.UserService;
... ... @@ -44,18 +46,12 @@ import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
44 46 import java.util.UUID;
45 47
46 48 @Component
  49 +@RequiredArgsConstructor
47 50 public class RefreshTokenAuthenticationProvider implements AuthenticationProvider {
48   -
49 51 private final JwtTokenFactory tokenFactory;
50 52 private final UserService userService;
51 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 56 @Override
61 57 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
... ... @@ -63,12 +59,18 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
63 59 RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
64 60 SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken);
65 61 UserPrincipal principal = unsafeUser.getUserPrincipal();
  62 +
66 63 SecurityUser securityUser;
67 64 if (principal.getType() == UserPrincipal.Type.USER_NAME) {
68 65 securityUser = authenticateByUserId(unsafeUser.getId());
69 66 } else {
70 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 74 return new RefreshAuthenticationToken(securityUser);
73 75 }
74 76
... ... @@ -91,7 +93,6 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
91 93 if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned");
92 94
93 95 UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
94   -
95 96 SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
96 97
97 98 return securityUser;
... ...
... ... @@ -17,8 +17,8 @@ package org.thingsboard.server.service.security.auth.jwt;
17 17
18 18 import org.springframework.beans.factory.annotation.Autowired;
19 19 import org.springframework.stereotype.Component;
  20 +import org.thingsboard.server.common.data.security.model.JwtToken;
20 21 import org.thingsboard.server.service.security.model.SecurityUser;
21   -import org.thingsboard.server.service.security.model.token.JwtToken;
22 22 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
23 23
24 24 @Component
... ...
... ... @@ -26,13 +26,12 @@ import org.thingsboard.server.common.data.id.CustomerId;
26 26 import org.thingsboard.server.common.data.id.EntityId;
27 27 import org.thingsboard.server.common.data.id.TenantId;
28 28 import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationInfo;
  29 +import org.thingsboard.server.common.data.security.model.JwtToken;
29 30 import org.thingsboard.server.dao.oauth2.OAuth2Service;
30 31 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
31 32 import org.thingsboard.server.service.security.model.SecurityUser;
32   -import org.thingsboard.server.service.security.model.token.JwtToken;
33 33 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
34 34 import org.thingsboard.server.service.security.system.SystemSecurityService;
35   -import org.thingsboard.server.utils.MiscUtils;
36 35
37 36 import javax.servlet.http.HttpServletRequest;
38 37 import javax.servlet.http.HttpServletResponse;
... ...
... ... @@ -23,9 +23,9 @@ import org.springframework.security.core.Authentication;
23 23 import org.springframework.security.web.WebAttributes;
24 24 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
25 25 import org.springframework.stereotype.Component;
  26 +import org.thingsboard.server.common.data.security.model.JwtToken;
26 27 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
27 28 import org.thingsboard.server.service.security.model.SecurityUser;
28   -import org.thingsboard.server.service.security.model.token.JwtToken;
29 29 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
30 30
31 31 import javax.servlet.ServletException;
... ...
... ... @@ -16,7 +16,7 @@
16 16 package org.thingsboard.server.service.security.exception;
17 17
18 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 21 public class JwtExpiredTokenException extends AuthenticationException {
22 22 private static final long serialVersionUID = -5959543783324224864L;
... ...
... ... @@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.model.token;
17 17
18 18 import com.fasterxml.jackson.annotation.JsonIgnore;
19 19 import io.jsonwebtoken.Claims;
  20 +import org.thingsboard.server.common.data.security.model.JwtToken;
20 21
21 22 public final class AccessJwtToken implements JwtToken {
22 23 private final String rawToken;
... ...
... ... @@ -16,18 +16,26 @@
16 16 package org.thingsboard.server.service.security.model.token;
17 17
18 18 import io.jsonwebtoken.Claims;
  19 +import io.jsonwebtoken.ExpiredJwtException;
19 20 import io.jsonwebtoken.Jws;
20 21 import io.jsonwebtoken.Jwts;
  22 +import io.jsonwebtoken.MalformedJwtException;
21 23 import io.jsonwebtoken.SignatureAlgorithm;
  24 +import io.jsonwebtoken.SignatureException;
  25 +import io.jsonwebtoken.UnsupportedJwtException;
  26 +import lombok.extern.slf4j.Slf4j;
22 27 import org.apache.commons.lang3.StringUtils;
23 28 import org.springframework.beans.factory.annotation.Autowired;
  29 +import org.springframework.security.authentication.BadCredentialsException;
24 30 import org.springframework.security.core.GrantedAuthority;
25 31 import org.springframework.stereotype.Component;
26 32 import org.thingsboard.server.common.data.id.CustomerId;
27 33 import org.thingsboard.server.common.data.id.TenantId;
28 34 import org.thingsboard.server.common.data.id.UserId;
29 35 import org.thingsboard.server.common.data.security.Authority;
  36 +import org.thingsboard.server.common.data.security.model.JwtToken;
30 37 import org.thingsboard.server.config.JwtSettings;
  38 +import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
31 39 import org.thingsboard.server.service.security.model.SecurityUser;
32 40 import org.thingsboard.server.service.security.model.UserPrincipal;
33 41
... ... @@ -39,6 +47,7 @@ import java.util.UUID;
39 47 import java.util.stream.Collectors;
40 48
41 49 @Component
  50 +@Slf4j
42 51 public class JwtTokenFactory {
43 52
44 53 private static final String SCOPES = "scopes";
... ... @@ -97,7 +106,7 @@ public class JwtTokenFactory {
97 106 }
98 107
99 108 public SecurityUser parseAccessJwtToken(RawAccessJwtToken rawAccessToken) {
100   - Jws<Claims> jwsClaims = rawAccessToken.parseClaims(settings.getTokenSigningKey());
  109 + Jws<Claims> jwsClaims = parseTokenClaims(rawAccessToken);
101 110 Claims claims = jwsClaims.getBody();
102 111 String subject = claims.getSubject();
103 112 @SuppressWarnings("unchecked")
... ... @@ -153,7 +162,7 @@ public class JwtTokenFactory {
153 162 }
154 163
155 164 public SecurityUser parseRefreshToken(RawAccessJwtToken rawAccessToken) {
156   - Jws<Claims> jwsClaims = rawAccessToken.parseClaims(settings.getTokenSigningKey());
  165 + Jws<Claims> jwsClaims = parseTokenClaims(rawAccessToken);
157 166 Claims claims = jwsClaims.getBody();
158 167 String subject = claims.getSubject();
159 168 @SuppressWarnings("unchecked")
... ... @@ -171,4 +180,17 @@ public class JwtTokenFactory {
171 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 }
... ...
... ... @@ -15,22 +15,10 @@
15 15 */
16 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 20 import java.io.Serializable;
32 21
33   -@Slf4j
34 22 public class RawAccessJwtToken implements JwtToken, Serializable {
35 23
36 24 private static final long serialVersionUID = -797397445703066079L;
... ... @@ -41,25 +29,6 @@ public class RawAccessJwtToken implements JwtToken, Serializable {
41 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 32 @Override
64 33 public String getToken() {
65 34 return token;
... ...
... ... @@ -362,6 +362,9 @@ caffeine:
362 362 attributes:
363 363 timeToLiveInMinutes: 1440
364 364 maxSize: 100000
  365 + tokensOutdatageTime:
  366 + timeToLiveInMinutes: 20000
  367 + maxSize: 10000
365 368
366 369 redis:
367 370 # standalone or cluster
... ...
  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 27 public static final String TENANT_PROFILE_CACHE = "tenantProfiles";
28 28 public static final String DEVICE_PROFILE_CACHE = "deviceProfiles";
29 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 13 * See the License for the specific language governing permissions and
14 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 18 import java.io.Serializable;
19 19
... ...
... ... @@ -22,8 +22,8 @@ import com.google.common.util.concurrent.ListenableFuture;
22 22 import lombok.extern.slf4j.Slf4j;
23 23 import org.apache.commons.lang3.RandomStringUtils;
24 24 import org.apache.commons.lang3.StringUtils;
25   -import org.springframework.beans.factory.annotation.Autowired;
26 25 import org.springframework.beans.factory.annotation.Value;
  26 +import org.springframework.context.ApplicationEventPublisher;
27 27 import org.springframework.context.annotation.Lazy;
28 28 import org.springframework.stereotype.Service;
29 29 import org.thingsboard.server.common.data.Customer;
... ... @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageData;
38 38 import org.thingsboard.server.common.data.page.PageLink;
39 39 import org.thingsboard.server.common.data.security.Authority;
40 40 import org.thingsboard.server.common.data.security.UserCredentials;
  41 +import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent;
41 42 import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
42 43 import org.thingsboard.server.dao.customer.CustomerDao;
43 44 import org.thingsboard.server.dao.entity.AbstractEntityService;
... ... @@ -75,21 +76,26 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
75 76 @Value("${security.user_login_case_sensitive:true}")
76 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 100 @Override
95 101 public User findUserByEmail(TenantId tenantId, String email) {
... ... @@ -225,6 +231,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
225 231 userCredentialsDao.removeById(tenantId, userCredentials.getUuidId());
226 232 deleteEntityRelations(tenantId, userId);
227 233 userDao.removeById(tenantId, userId.getId());
  234 + eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId));
228 235 }
229 236
230 237 @Override
... ...
... ... @@ -149,8 +149,11 @@ export class AuthService {
149 149 }
150 150
151 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 159 public activateByEmailCode(emailCode: string): Observable<LoginResponse> {
... ...