diff --git a/README.md b/README.md index ba576c0..b897200 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ The following properties are used across the functionalities of this extension. | --------------------------------- | --------- | ----------- | | `auth-ext.externalId` | `oauth` | This will serve as the external ID for users and as the prefix for the external ID of groups created or searched by this extension. Anything without an external ID is considered internal. So mismatched external IDs are never considered for anything by this extension. | | `auth-ext.tenant` | | A preselected tenant for all operations in this extension. Only required if there are multiple tenants. | +| `auth-ext.sync.resyncInMillis` | `300000` | To prevent too many sync checks, how long between seeing the EXACT same token should we wait before doing another sync. The only time this matters is if the user is manually added to or removed from groups in APS by other means. Or those groups are deleted. | ### OAuth Authentication/Authorization diff --git a/src/main/java/com/inteligr8/activiti/auth/service/SyncingJwtAuthenticationConverter.java b/src/main/java/com/inteligr8/activiti/auth/service/SyncingJwtAuthenticationConverter.java index 13a3742..29535fa 100644 --- a/src/main/java/com/inteligr8/activiti/auth/service/SyncingJwtAuthenticationConverter.java +++ b/src/main/java/com/inteligr8/activiti/auth/service/SyncingJwtAuthenticationConverter.java @@ -4,10 +4,12 @@ import java.util.ArrayList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.core.oidc.StandardClaimNames; import org.springframework.security.oauth2.jwt.Jwt; import com.activiti.security.identity.service.config.JwtAuthenticationToken; @@ -22,6 +24,9 @@ public class SyncingJwtAuthenticationConverter implements Converter(springUser.getAuthorities())); diff --git a/src/main/java/com/inteligr8/activiti/auth/service/SyncingUserService.java b/src/main/java/com/inteligr8/activiti/auth/service/SyncingUserService.java index adefce7..f8d6f7b 100644 --- a/src/main/java/com/inteligr8/activiti/auth/service/SyncingUserService.java +++ b/src/main/java/com/inteligr8/activiti/auth/service/SyncingUserService.java @@ -29,7 +29,7 @@ import com.activiti.security.identity.service.config.IdentityServiceKeycloakProp @Primary public class SyncingUserService extends OidcUserService { - private final Logger logger = LoggerFactory.getLogger(SyncingUserService.class); + private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private UserDetailsService userDetailsService; @@ -43,17 +43,24 @@ public class SyncingUserService extends OidcUserService { @Autowired private IdentityServiceKeycloakProperties identityServiceKeycloakProperties; + @Autowired + private TokenRecaller tokenRecaller; + @Override public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { this.logger.trace("loadUser({}, {})", userRequest.getIdToken().getEmail(), userRequest.getAccessToken().getScopes()); - + OidcUser oidcUser = super.loadUser(userRequest); this.logger.debug("Loaded OIDC user: {}", oidcUser.getEmail()); - this.userSyncService.sync(oidcUser); - this.groupSyncService.sync(oidcUser); + if (this.tokenRecaller.recall(userRequest.getAccessToken().getTokenValue())) { + this.logger.trace("Skipping sync for '{}' as the same token was already recently sync'd", userRequest.getIdToken().getEmail()); + } else { + this.userSyncService.sync(oidcUser); + this.groupSyncService.sync(oidcUser); + this.tokenRecaller.add(userRequest.getAccessToken().getTokenValue()); + } - // reload for sync'd group changes UserDetails springUser = this.userDetailsService.loadUserByUsername(oidcUser.getEmail()); CustomOAuth2User customOAuth2User = new CustomOAuth2User( diff --git a/src/main/java/com/inteligr8/activiti/auth/service/TokenRecaller.java b/src/main/java/com/inteligr8/activiti/auth/service/TokenRecaller.java new file mode 100755 index 0000000..f175f01 --- /dev/null +++ b/src/main/java/com/inteligr8/activiti/auth/service/TokenRecaller.java @@ -0,0 +1,59 @@ +package com.inteligr8.activiti.auth.service; + +import java.util.concurrent.TimeUnit; + +import org.apache.commons.collections4.map.PassiveExpiringMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; + +@Component +public class TokenRecaller { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Value("${auth-ext.sync.resyncInMillis:300000}") + private long resyncTimeInMillis; + + private PassiveExpiringMap tokenCache; + + @PostConstruct + private void init() { + this.tokenCache = new PassiveExpiringMap<>(this.resyncTimeInMillis, TimeUnit.MILLISECONDS); + } + + @Scheduled(timeUnit = TimeUnit.MINUTES, initialDelay = 10L, fixedDelay = 5L) + private void reap() { + int tokens = this.tokenCache.size(); + this.logger.trace("Reaping token cache of size: {}", tokens); + + synchronized (this.tokenCache) { + // clear expired keys + this.tokenCache.entrySet(); + } + + this.logger.debug("Reaped {} expired tokens from cache", this.tokenCache.size() - tokens); + } + + public void add(String token) { + synchronized (this.tokenCache) { + this.tokenCache.put(token, System.currentTimeMillis() + this.resyncTimeInMillis); + } + } + + public boolean recall(String token) { + Long expirationTimeMillis = this.tokenCache.get(token); + if (expirationTimeMillis == null) { + return false; + } else if (expirationTimeMillis < System.currentTimeMillis()) { + return false; + } else { + return true; + } + } + +}