Browse Source
* 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 passwordpull/4277/head
committed by
GitHub
20 changed files with 427 additions and 122 deletions
@ -0,0 +1,85 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth; |
|||
|
|||
import io.jsonwebtoken.Claims; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.cache.Cache; |
|||
import org.springframework.cache.CacheManager; |
|||
import org.springframework.context.event.EventListener; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.CacheConstants; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEvent; |
|||
import org.thingsboard.server.common.data.security.model.JwtToken; |
|||
import org.thingsboard.server.config.JwtSettings; |
|||
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; |
|||
|
|||
import javax.annotation.PostConstruct; |
|||
import java.util.Optional; |
|||
|
|||
import static java.util.concurrent.TimeUnit.MILLISECONDS; |
|||
import static java.util.concurrent.TimeUnit.SECONDS; |
|||
|
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class TokenOutdatingService { |
|||
private final CacheManager cacheManager; |
|||
private final JwtTokenFactory tokenFactory; |
|||
private final JwtSettings jwtSettings; |
|||
private Cache tokenOutdatageTimeCache; |
|||
|
|||
@PostConstruct |
|||
protected void initCache() { |
|||
tokenOutdatageTimeCache = cacheManager.getCache(CacheConstants.TOKEN_OUTDATAGE_TIME_CACHE); |
|||
} |
|||
|
|||
@EventListener(classes = UserAuthDataChangedEvent.class) |
|||
public void onUserAuthDataChanged(UserAuthDataChangedEvent userAuthDataChangedEvent) { |
|||
outdateOldUserTokens(userAuthDataChangedEvent.getUserId()); |
|||
} |
|||
|
|||
public boolean isOutdated(JwtToken token, UserId userId) { |
|||
Claims claims = tokenFactory.parseTokenClaims(token).getBody(); |
|||
long issueTime = claims.getIssuedAt().getTime(); |
|||
|
|||
return Optional.ofNullable(tokenOutdatageTimeCache.get(toKey(userId), Long.class)) |
|||
.map(outdatageTime -> { |
|||
if (System.currentTimeMillis() - outdatageTime <= SECONDS.toMillis(jwtSettings.getRefreshTokenExpTime())) { |
|||
return MILLISECONDS.toSeconds(issueTime) < MILLISECONDS.toSeconds(outdatageTime); |
|||
} else { |
|||
/* |
|||
* Means that since the outdating has passed more than |
|||
* the lifetime of refresh token (the longest lived) |
|||
* and there is no need to store outdatage time anymore |
|||
* as all the tokens issued before the outdatage time |
|||
* are now expired by themselves |
|||
* */ |
|||
tokenOutdatageTimeCache.evict(toKey(userId)); |
|||
return false; |
|||
} |
|||
}) |
|||
.orElse(false); |
|||
} |
|||
|
|||
public void outdateOldUserTokens(UserId userId) { |
|||
tokenOutdatageTimeCache.put(toKey(userId), System.currentTimeMillis()); |
|||
} |
|||
|
|||
private String toKey(UserId userId) { |
|||
return userId.getId().toString(); |
|||
} |
|||
} |
|||
@ -0,0 +1,184 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth; |
|||
|
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager; |
|||
import org.springframework.security.authentication.CredentialsExpiredException; |
|||
import org.thingsboard.server.common.data.CacheConstants; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.security.Authority; |
|||
import org.thingsboard.server.common.data.security.UserCredentials; |
|||
import org.thingsboard.server.common.data.security.model.JwtToken; |
|||
import org.thingsboard.server.config.JwtSettings; |
|||
import org.thingsboard.server.dao.customer.CustomerService; |
|||
import org.thingsboard.server.dao.user.UserService; |
|||
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; |
|||
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider; |
|||
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.model.UserPrincipal; |
|||
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; |
|||
import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
import static java.util.concurrent.TimeUnit.DAYS; |
|||
import static java.util.concurrent.TimeUnit.MINUTES; |
|||
import static java.util.concurrent.TimeUnit.SECONDS; |
|||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; |
|||
import static org.junit.jupiter.api.Assertions.assertFalse; |
|||
import static org.junit.jupiter.api.Assertions.assertNotNull; |
|||
import static org.junit.jupiter.api.Assertions.assertNull; |
|||
import static org.junit.jupiter.api.Assertions.assertThrows; |
|||
import static org.junit.jupiter.api.Assertions.assertTrue; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.Mockito.mock; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
public class TokenOutdatingTest { |
|||
private JwtAuthenticationProvider accessTokenAuthenticationProvider; |
|||
private RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider; |
|||
|
|||
private TokenOutdatingService tokenOutdatingService; |
|||
private ConcurrentMapCacheManager cacheManager; |
|||
private JwtTokenFactory tokenFactory; |
|||
private JwtSettings jwtSettings; |
|||
|
|||
private UserId userId; |
|||
|
|||
@BeforeEach |
|||
public void setUp() { |
|||
jwtSettings = new JwtSettings(); |
|||
jwtSettings.setTokenIssuer("test.io"); |
|||
jwtSettings.setTokenExpirationTime((int) MINUTES.toSeconds(10)); |
|||
jwtSettings.setRefreshTokenExpTime((int) DAYS.toSeconds(7)); |
|||
jwtSettings.setTokenSigningKey("secret"); |
|||
tokenFactory = new JwtTokenFactory(jwtSettings); |
|||
|
|||
cacheManager = new ConcurrentMapCacheManager(); |
|||
tokenOutdatingService = new TokenOutdatingService(cacheManager, tokenFactory, jwtSettings); |
|||
tokenOutdatingService.initCache(); |
|||
|
|||
userId = new UserId(UUID.randomUUID()); |
|||
|
|||
UserService userService = mock(UserService.class); |
|||
|
|||
User user = new User(); |
|||
user.setId(userId); |
|||
user.setAuthority(Authority.TENANT_ADMIN); |
|||
user.setEmail("email"); |
|||
when(userService.findUserById(any(), eq(userId))).thenReturn(user); |
|||
|
|||
UserCredentials userCredentials = new UserCredentials(); |
|||
userCredentials.setEnabled(true); |
|||
when(userService.findUserCredentialsByUserId(any(), eq(userId))).thenReturn(userCredentials); |
|||
|
|||
accessTokenAuthenticationProvider = new JwtAuthenticationProvider(tokenFactory, tokenOutdatingService); |
|||
refreshTokenAuthenticationProvider = new RefreshTokenAuthenticationProvider(tokenFactory, userService, mock(CustomerService.class), tokenOutdatingService); |
|||
} |
|||
|
|||
@Test |
|||
public void testOutdateOldUserTokens() throws Exception { |
|||
JwtToken jwtToken = createAccessJwtToken(userId); |
|||
|
|||
SECONDS.sleep(1); // need to wait before outdating so that outdatage time is strictly after token issue time
|
|||
tokenOutdatingService.outdateOldUserTokens(userId); |
|||
assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId)); |
|||
|
|||
SECONDS.sleep(1); |
|||
|
|||
JwtToken newJwtToken = tokenFactory.createAccessJwtToken(createMockSecurityUser(userId)); |
|||
assertFalse(tokenOutdatingService.isOutdated(newJwtToken, userId)); |
|||
} |
|||
|
|||
@Test |
|||
public void testAuthenticateWithOutdatedAccessToken() throws InterruptedException { |
|||
RawAccessJwtToken accessJwtToken = getRawJwtToken(createAccessJwtToken(userId)); |
|||
|
|||
assertDoesNotThrow(() -> { |
|||
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); |
|||
}); |
|||
|
|||
SECONDS.sleep(1); |
|||
tokenOutdatingService.outdateOldUserTokens(userId); |
|||
|
|||
assertThrows(JwtExpiredTokenException.class, () -> { |
|||
accessTokenAuthenticationProvider.authenticate(new JwtAuthenticationToken(accessJwtToken)); |
|||
}); |
|||
} |
|||
|
|||
@Test |
|||
public void testAuthenticateWithOutdatedRefreshToken() throws InterruptedException { |
|||
RawAccessJwtToken refreshJwtToken = getRawJwtToken(createRefreshJwtToken(userId)); |
|||
|
|||
assertDoesNotThrow(() -> { |
|||
refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); |
|||
}); |
|||
|
|||
SECONDS.sleep(1); |
|||
tokenOutdatingService.outdateOldUserTokens(userId); |
|||
|
|||
assertThrows(CredentialsExpiredException.class, () -> { |
|||
refreshTokenAuthenticationProvider.authenticate(new RefreshAuthenticationToken(refreshJwtToken)); |
|||
}); |
|||
} |
|||
|
|||
@Test |
|||
public void testTokensOutdatageTimeRemovalFromCache() throws Exception { |
|||
JwtToken jwtToken = createAccessJwtToken(userId); |
|||
|
|||
SECONDS.sleep(1); |
|||
tokenOutdatingService.outdateOldUserTokens(userId); |
|||
|
|||
int refreshTokenExpirationTime = 5; |
|||
jwtSettings.setRefreshTokenExpTime(refreshTokenExpirationTime); |
|||
|
|||
SECONDS.sleep(refreshTokenExpirationTime - 2); |
|||
|
|||
assertTrue(tokenOutdatingService.isOutdated(jwtToken, userId)); |
|||
assertNotNull(cacheManager.getCache(CacheConstants.TOKEN_OUTDATAGE_TIME_CACHE).get(userId.getId().toString())); |
|||
|
|||
SECONDS.sleep(3); |
|||
|
|||
assertFalse(tokenOutdatingService.isOutdated(jwtToken, userId)); |
|||
assertNull(cacheManager.getCache(CacheConstants.TOKEN_OUTDATAGE_TIME_CACHE).get(userId.getId().toString())); |
|||
} |
|||
|
|||
private JwtToken createAccessJwtToken(UserId userId) { |
|||
return tokenFactory.createAccessJwtToken(createMockSecurityUser(userId)); |
|||
} |
|||
|
|||
private JwtToken createRefreshJwtToken(UserId userId) { |
|||
return tokenFactory.createRefreshToken(createMockSecurityUser(userId)); |
|||
} |
|||
|
|||
private RawAccessJwtToken getRawJwtToken(JwtToken token) { |
|||
return new RawAccessJwtToken(token.getToken()); |
|||
} |
|||
|
|||
private SecurityUser createMockSecurityUser(UserId userId) { |
|||
SecurityUser securityUser = new SecurityUser(); |
|||
securityUser.setEmail("email"); |
|||
securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail())); |
|||
securityUser.setAuthority(Authority.CUSTOMER_USER); |
|||
securityUser.setId(userId); |
|||
return securityUser; |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
/** |
|||
* Copyright © 2016-2021 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.security.event; |
|||
|
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
|
|||
public class UserAuthDataChangedEvent { |
|||
private final UserId userId; |
|||
|
|||
public UserAuthDataChangedEvent(UserId userId) { |
|||
this.userId = userId; |
|||
} |
|||
|
|||
public UserId getUserId() { |
|||
return userId; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue