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