/yamcs-core/src/main/java/org/yamcs/http/auth/TokenStore.java

https://github.com/yamcs/yamcs · Java · 211 lines · 163 code · 29 blank · 19 comment · 10 complexity · 88a5ce4a7e54c3ce226dfcaefe70a974 MD5 · raw file

  1. package org.yamcs.http.auth;
  2. import java.util.Arrays;
  3. import java.util.HashMap;
  4. import java.util.Map;
  5. import java.util.UUID;
  6. import java.util.concurrent.ConcurrentHashMap;
  7. import java.util.concurrent.TimeUnit;
  8. import org.yamcs.InitException;
  9. import org.yamcs.YamcsServer;
  10. import org.yamcs.http.AbstractHttpService;
  11. import org.yamcs.http.HttpServer;
  12. import org.yamcs.http.UnauthorizedException;
  13. import org.yamcs.http.auth.JwtHelper.JwtDecodeException;
  14. import org.yamcs.security.AuthenticationInfo;
  15. import org.yamcs.security.CryptoUtils;
  16. import org.yamcs.security.SecurityStore;
  17. import org.yamcs.security.SessionExpiredException;
  18. import org.yamcs.security.SessionManager;
  19. import org.yamcs.security.UserSession;
  20. import com.google.common.cache.Cache;
  21. import com.google.common.cache.CacheBuilder;
  22. /**
  23. * Store capable of generating a chain of refresh tokens. When a token is exchanged for a new token, the old token
  24. * remains valid for a limited lifetime. This property is useful do deal with a burst of identical refresh requests.
  25. * <p>
  26. * This class maintains a cache from a JWT bearer token to the original authentication info. This allows skipping the
  27. * login process as long as the bearer is valid.
  28. */
  29. public class TokenStore extends AbstractHttpService {
  30. private final ConcurrentHashMap<String, AuthenticationInfo> accessTokens = new ConcurrentHashMap<>();
  31. private int cleaningCounter = 0;
  32. private Map<Hmac, RefreshState> refreshTokens = new HashMap<>();
  33. private Cache<Hmac, RefreshResult> refreshCache = CacheBuilder.newBuilder()
  34. .expireAfterWrite(5, TimeUnit.SECONDS)
  35. .build();
  36. @Override
  37. public void init(HttpServer httpServer) throws InitException {
  38. }
  39. @Override
  40. protected void doStart() {
  41. notifyStarted();
  42. }
  43. @Override
  44. protected void doStop() {
  45. accessTokens.clear();
  46. refreshTokens.clear();
  47. refreshCache.invalidateAll();
  48. cleaningCounter = 0;
  49. notifyStopped();
  50. }
  51. public void registerAccessToken(String accessToken, AuthenticationInfo authenticationInfo) {
  52. accessTokens.put(accessToken, authenticationInfo);
  53. }
  54. public void revokeAccessToken(String accessToken) {
  55. accessTokens.remove(accessToken);
  56. }
  57. public AuthenticationInfo verifyAccessToken(String accessToken) throws UnauthorizedException {
  58. cleaningCounter++;
  59. if (cleaningCounter > 1000) {
  60. cleaningCounter = 0;
  61. forgetExpiredAccessTokens();
  62. }
  63. try {
  64. JwtToken jwtToken = new JwtToken(accessToken, YamcsServer.getServer().getSecretKey());
  65. if (jwtToken.isExpired()) {
  66. accessTokens.remove(accessToken);
  67. throw new UnauthorizedException("Token expired");
  68. }
  69. AuthenticationInfo authenticationInfo = accessTokens.get(accessToken);
  70. if (authenticationInfo == null) {
  71. log.warn("Got an invalid access token");
  72. throw new UnauthorizedException("Invalid access token");
  73. }
  74. return authenticationInfo;
  75. } catch (JwtDecodeException e) {
  76. throw new UnauthorizedException("Failed to decode JWT: " + e.getMessage());
  77. }
  78. }
  79. private void forgetExpiredAccessTokens() {
  80. accessTokens.entrySet().removeIf(entry -> {
  81. try {
  82. JwtToken jwtToken = new JwtToken(entry.getKey(), YamcsServer.getServer().getSecretKey());
  83. return jwtToken.isExpired();
  84. } catch (JwtDecodeException e) {
  85. return true;
  86. }
  87. });
  88. }
  89. public synchronized void forgetUser(String username) {
  90. refreshTokens.entrySet().removeIf(entry -> {
  91. return username.equals(entry.getValue().authenticationInfo.getUsername());
  92. });
  93. accessTokens.entrySet().removeIf(entry -> {
  94. return username.equals(entry.getValue().getUsername());
  95. });
  96. }
  97. public synchronized String generateRefreshToken(AuthenticationInfo authenticationInfo, UserSession session) {
  98. String refreshToken = UUID.randomUUID().toString();
  99. Hmac hmac = new Hmac(refreshToken);
  100. refreshTokens.put(hmac, new RefreshState(authenticationInfo, session));
  101. return refreshToken;
  102. }
  103. /**
  104. * Validate the provided refresh token, and exchange it for a new one. The provided refresh token is invalidated,
  105. * and will stop working after a certain time.
  106. * <p>
  107. * Attempts to exchange a previously exchanged token will always return the same result, as long as it has not
  108. * expired yet.
  109. *
  110. * @return a new refresh token, or null if the token could not be exchanged.
  111. */
  112. public synchronized RefreshResult verifyRefreshToken(String refreshToken) {
  113. Hmac hmac = new Hmac(refreshToken);
  114. RefreshState state = refreshTokens.get(hmac);
  115. if (state != null) { // Token valid, generate new token (once only)
  116. String nextToken = generateRefreshToken(state.authenticationInfo, state.userSession);
  117. try {
  118. renewSession(state.userSession);
  119. } catch (SessionExpiredException e) {
  120. throw new UnauthorizedException("Token expired");
  121. }
  122. RefreshResult result = new RefreshResult(state.authenticationInfo, nextToken);
  123. refreshCache.put(hmac, result);
  124. refreshTokens.remove(hmac);
  125. return result;
  126. } else { // Maybe an old token, attempt to upgrade it based on previous token exchanges
  127. RefreshResult result = null;
  128. RefreshResult candidate = refreshCache.getIfPresent(hmac);
  129. while (candidate != null) {
  130. result = candidate;
  131. candidate = refreshCache.getIfPresent(new Hmac(candidate.refreshToken));
  132. }
  133. return result;
  134. }
  135. }
  136. private void renewSession(UserSession userSession) throws SessionExpiredException {
  137. SecurityStore securityStore = YamcsServer.getServer().getSecurityStore();
  138. SessionManager sessionManager = securityStore.getSessionManager();
  139. sessionManager.renewSession(userSession.getId());
  140. }
  141. public synchronized void revokeRefreshToken(String refreshToken) {
  142. Hmac hmac = new Hmac(refreshToken);
  143. refreshTokens.remove(hmac);
  144. refreshCache.invalidate(hmac);
  145. }
  146. private static final class RefreshState {
  147. final AuthenticationInfo authenticationInfo;
  148. final UserSession userSession;
  149. RefreshState(AuthenticationInfo authenticationInfo, UserSession userSession) {
  150. this.authenticationInfo = authenticationInfo;
  151. this.userSession = userSession;
  152. }
  153. }
  154. /**
  155. * byte[] wrapper that allows value comparison in HashMap
  156. */
  157. private static final class Hmac {
  158. private byte[] hmac;
  159. Hmac(String refreshToken) {
  160. hmac = CryptoUtils.calculateHmac(refreshToken, YamcsServer.getServer().getSecretKey());
  161. }
  162. @Override
  163. public boolean equals(Object obj) {
  164. if (!(obj instanceof Hmac)) {
  165. return false;
  166. }
  167. return Arrays.equals(hmac, ((Hmac) obj).hmac);
  168. }
  169. @Override
  170. public int hashCode() {
  171. return Arrays.hashCode(hmac);
  172. }
  173. }
  174. static final class RefreshResult {
  175. AuthenticationInfo authenticationInfo;
  176. String refreshToken;
  177. RefreshResult(AuthenticationInfo authenticationInfo, String refreshToken) {
  178. this.authenticationInfo = authenticationInfo;
  179. this.refreshToken = refreshToken;
  180. }
  181. }
  182. }