Loading pom.xml +2 −1 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ <groupId>com.inteligr8.activiti</groupId> <artifactId>auth-activiti-app-ext</artifactId> <version>2.0-SNAPSHOT</version> <version>2.1-SNAPSHOT</version> <name>Authentication & Authorization for APS</name> <description>An Alfresco Process Service App extension providing improved authentication and authorization support.</description> Loading Loading @@ -45,6 +45,7 @@ <!-- for RAD --> <tomcat-rad.version>10-2.2</tomcat-rad.version> <aps.hotswap.enabled>false</aps.hotswap.enabled> <aps.tomcat.opts.base>-Dspring.main.allow-circular-references=true \ -Dhibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ -Dauth-ext.external.id=keycloak \ Loading src/main/java/com/inteligr8/activiti/auth/oauth/IdentityServiceConfigurationOverride.java +34 −1 Original line number Diff line number Diff line package com.inteligr8.activiti.auth.oauth; import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; Loading @@ -10,11 +12,16 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.web.SecurityFilterChain; import com.activiti.security.ProtectedPaths; import com.activiti.security.identity.service.config.IdentityServiceEnabledCondition; import com.inteligr8.activiti.auth.service.JwtAuthenticationProvider; import com.nimbusds.oauth2.sdk.ParseException; /** Loading @@ -33,6 +40,9 @@ public class IdentityServiceConfigurationOverride { @Autowired private ApplicationContext appContext; @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; @Bean("inteligr8.clientRegistrationRepository") @Primary public ClientRegistrationRepository clientRegistrationRepository() { Loading @@ -51,7 +61,7 @@ public class IdentityServiceConfigurationOverride { @Bean(OVERRIDE_CLIENT_REGISTRATION_BEANNAME) @Primary public ClientRegistration clientRegistration1() throws ParseException, InterruptedException { public ClientRegistration clientRegistration() throws ParseException, InterruptedException { this.logger.trace("clientRegistration()"); ClientRegistration clientRegistration = this.appContext.getBean(OOTB_CLIENT_REGISTRATION_BEANNAME, ClientRegistration.class); Loading @@ -62,4 +72,27 @@ public class IdentityServiceConfigurationOverride { .build(); } /** * Slightly lower priority than the one provided OOTB. This * allows for the bean injection of the JwtAuthenticationConverter. * * A lower priority means it is applied last. This means it replaces the * JwtAuthenticationConverter provided by Alfresco OOTB. * * @see com.activiti.security.identity.service.config.IdentityServiceConfigurationApi#identityServiceApiWebSecurity */ @Bean("inteligr8.identityServiceApiWebSecurity") @Order(-5) public SecurityFilterChain identityServiceApiWebSecurity(HttpSecurity http) throws Exception { http .securityMatcher(antMatcher(ProtectedPaths.API_URL_PATH + "/**")) .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwtConfigurer -> { jwtConfigurer.jwtAuthenticationConverter(this.jwtAuthenticationProvider.create()); }) ); return http.build(); } } src/main/java/com/inteligr8/activiti/auth/service/GroupSyncService.java +20 −9 Original line number Diff line number Diff line Loading @@ -30,7 +30,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.data.util.Pair; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.oauth2.core.ClaimAccessor; import org.springframework.security.oauth2.core.oidc.StandardClaimNames; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Component; import com.activiti.domain.idm.Group; Loading Loading @@ -129,28 +132,36 @@ public class GroupSyncService { } public void sync(OidcUser oidcUser) { if (!oidcUser.hasClaim("groups")) { this.logger.warn("There is no 'groups' claim to synchronize: {}", oidcUser.getEmail()); this.logger.debug("The claims available: {}", oidcUser.getClaims().keySet()); this.sync(oidcUser.getEmail(), oidcUser); } public void sync(Jwt jwt) { this.sync(jwt.getClaim(StandardClaimNames.EMAIL), jwt); } public void sync(String email, ClaimAccessor claims) { if (!claims.hasClaim("groups")) { this.logger.warn("There is no 'groups' claim to synchronize: {}", email); this.logger.debug("The claims available: {}", claims.getClaims().keySet()); return; } Set<String> oidcGroups = new HashSet<>(oidcUser.getClaimAsStringList("groups")); this.logger.trace("Incoming OIDC groups: {}: {}", oidcUser.getEmail(), oidcGroups); Set<String> oidcGroups = new HashSet<>(claims.getClaimAsStringList("groups")); this.logger.trace("Incoming OIDC groups: {}: {}", email, oidcGroups); oidcGroups = this.filterGroups(oidcGroups); Set<String> translatedGroups = this.translateGroups(oidcGroups); this.logger.debug("Filtered/translated OIDC groups: {}: {}", oidcUser.getEmail(), translatedGroups); this.logger.debug("Filtered/translated OIDC groups: {}: {}", email, translatedGroups); long tenantId = this.tenantFinderService.findTenantId(); // check Activiti groups User user = this.userService.findUserByEmailAndTenantId(oidcUser.getEmail(), tenantId); User user = this.userService.findUserByEmailAndTenantId(email, tenantId); if (user == null) { user = this.userService.findUserByEmail(oidcUser.getEmail()); user = this.userService.findUserByEmail(email); if (user == null) throw new UsernameNotFoundException("The user could not be found: " + oidcUser.getEmail()); throw new UsernameNotFoundException("The user could not be found: " + email); } User userWithGroups = this.userService.getUser(user.getId(), true); this.logger.debug("Discovered user belongs to {} APS groups: {}", userWithGroups.getGroups().size(), user.getExternalId()); Loading src/main/java/com/inteligr8/activiti/auth/service/JwtAuthenticationProvider.java 0 → 100644 +11 −0 Original line number Diff line number Diff line package com.inteligr8.activiti.auth.service; import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.oauth2.jwt.Jwt; public interface JwtAuthenticationProvider { Converter<Jwt, AbstractAuthenticationToken> create(); } src/main/java/com/inteligr8/activiti/auth/service/SyncingJwtAuthenticationConverter.java 0 → 100644 +50 −0 Original line number Diff line number Diff line package com.inteligr8.activiti.auth.service; import java.util.ArrayList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.jwt.Jwt; import com.activiti.security.identity.service.config.JwtAuthenticationToken; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class SyncingJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final UserDetailsService userDetailsService; private final UserSyncService userSyncService; private final GroupSyncService groupSyncService; public SyncingJwtAuthenticationConverter(UserDetailsService userDetailsService, UserSyncService userSyncService, GroupSyncService groupSyncService) { this.userDetailsService = userDetailsService; this.userSyncService = userSyncService; this.groupSyncService = groupSyncService; } @Override public AbstractAuthenticationToken convert(Jwt source) { this.logger.trace("convert({}, {})", source.getId(), source.getClaim("email")); try { this.logger.debug("jwt: {}", new ObjectMapper().registerModule(new JavaTimeModule()).writeValueAsString(source)); } catch (JsonProcessingException jpe) { this.logger.error("error", jpe); } this.userSyncService.sync(source); this.groupSyncService.sync(source); UserDetails springUser = this.userDetailsService.loadUserByUsername(source.getClaim("email")); return new JwtAuthenticationToken( springUser, new ArrayList<>(springUser.getAuthorities())); } } Loading
pom.xml +2 −1 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ <groupId>com.inteligr8.activiti</groupId> <artifactId>auth-activiti-app-ext</artifactId> <version>2.0-SNAPSHOT</version> <version>2.1-SNAPSHOT</version> <name>Authentication & Authorization for APS</name> <description>An Alfresco Process Service App extension providing improved authentication and authorization support.</description> Loading Loading @@ -45,6 +45,7 @@ <!-- for RAD --> <tomcat-rad.version>10-2.2</tomcat-rad.version> <aps.hotswap.enabled>false</aps.hotswap.enabled> <aps.tomcat.opts.base>-Dspring.main.allow-circular-references=true \ -Dhibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ -Dauth-ext.external.id=keycloak \ Loading
src/main/java/com/inteligr8/activiti/auth/oauth/IdentityServiceConfigurationOverride.java +34 −1 Original line number Diff line number Diff line package com.inteligr8.activiti.auth.oauth; import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; Loading @@ -10,11 +12,16 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.web.SecurityFilterChain; import com.activiti.security.ProtectedPaths; import com.activiti.security.identity.service.config.IdentityServiceEnabledCondition; import com.inteligr8.activiti.auth.service.JwtAuthenticationProvider; import com.nimbusds.oauth2.sdk.ParseException; /** Loading @@ -33,6 +40,9 @@ public class IdentityServiceConfigurationOverride { @Autowired private ApplicationContext appContext; @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; @Bean("inteligr8.clientRegistrationRepository") @Primary public ClientRegistrationRepository clientRegistrationRepository() { Loading @@ -51,7 +61,7 @@ public class IdentityServiceConfigurationOverride { @Bean(OVERRIDE_CLIENT_REGISTRATION_BEANNAME) @Primary public ClientRegistration clientRegistration1() throws ParseException, InterruptedException { public ClientRegistration clientRegistration() throws ParseException, InterruptedException { this.logger.trace("clientRegistration()"); ClientRegistration clientRegistration = this.appContext.getBean(OOTB_CLIENT_REGISTRATION_BEANNAME, ClientRegistration.class); Loading @@ -62,4 +72,27 @@ public class IdentityServiceConfigurationOverride { .build(); } /** * Slightly lower priority than the one provided OOTB. This * allows for the bean injection of the JwtAuthenticationConverter. * * A lower priority means it is applied last. This means it replaces the * JwtAuthenticationConverter provided by Alfresco OOTB. * * @see com.activiti.security.identity.service.config.IdentityServiceConfigurationApi#identityServiceApiWebSecurity */ @Bean("inteligr8.identityServiceApiWebSecurity") @Order(-5) public SecurityFilterChain identityServiceApiWebSecurity(HttpSecurity http) throws Exception { http .securityMatcher(antMatcher(ProtectedPaths.API_URL_PATH + "/**")) .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwtConfigurer -> { jwtConfigurer.jwtAuthenticationConverter(this.jwtAuthenticationProvider.create()); }) ); return http.build(); } }
src/main/java/com/inteligr8/activiti/auth/service/GroupSyncService.java +20 −9 Original line number Diff line number Diff line Loading @@ -30,7 +30,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.data.util.Pair; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.oauth2.core.ClaimAccessor; import org.springframework.security.oauth2.core.oidc.StandardClaimNames; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Component; import com.activiti.domain.idm.Group; Loading Loading @@ -129,28 +132,36 @@ public class GroupSyncService { } public void sync(OidcUser oidcUser) { if (!oidcUser.hasClaim("groups")) { this.logger.warn("There is no 'groups' claim to synchronize: {}", oidcUser.getEmail()); this.logger.debug("The claims available: {}", oidcUser.getClaims().keySet()); this.sync(oidcUser.getEmail(), oidcUser); } public void sync(Jwt jwt) { this.sync(jwt.getClaim(StandardClaimNames.EMAIL), jwt); } public void sync(String email, ClaimAccessor claims) { if (!claims.hasClaim("groups")) { this.logger.warn("There is no 'groups' claim to synchronize: {}", email); this.logger.debug("The claims available: {}", claims.getClaims().keySet()); return; } Set<String> oidcGroups = new HashSet<>(oidcUser.getClaimAsStringList("groups")); this.logger.trace("Incoming OIDC groups: {}: {}", oidcUser.getEmail(), oidcGroups); Set<String> oidcGroups = new HashSet<>(claims.getClaimAsStringList("groups")); this.logger.trace("Incoming OIDC groups: {}: {}", email, oidcGroups); oidcGroups = this.filterGroups(oidcGroups); Set<String> translatedGroups = this.translateGroups(oidcGroups); this.logger.debug("Filtered/translated OIDC groups: {}: {}", oidcUser.getEmail(), translatedGroups); this.logger.debug("Filtered/translated OIDC groups: {}: {}", email, translatedGroups); long tenantId = this.tenantFinderService.findTenantId(); // check Activiti groups User user = this.userService.findUserByEmailAndTenantId(oidcUser.getEmail(), tenantId); User user = this.userService.findUserByEmailAndTenantId(email, tenantId); if (user == null) { user = this.userService.findUserByEmail(oidcUser.getEmail()); user = this.userService.findUserByEmail(email); if (user == null) throw new UsernameNotFoundException("The user could not be found: " + oidcUser.getEmail()); throw new UsernameNotFoundException("The user could not be found: " + email); } User userWithGroups = this.userService.getUser(user.getId(), true); this.logger.debug("Discovered user belongs to {} APS groups: {}", userWithGroups.getGroups().size(), user.getExternalId()); Loading
src/main/java/com/inteligr8/activiti/auth/service/JwtAuthenticationProvider.java 0 → 100644 +11 −0 Original line number Diff line number Diff line package com.inteligr8.activiti.auth.service; import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.oauth2.jwt.Jwt; public interface JwtAuthenticationProvider { Converter<Jwt, AbstractAuthenticationToken> create(); }
src/main/java/com/inteligr8/activiti/auth/service/SyncingJwtAuthenticationConverter.java 0 → 100644 +50 −0 Original line number Diff line number Diff line package com.inteligr8.activiti.auth.service; import java.util.ArrayList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.jwt.Jwt; import com.activiti.security.identity.service.config.JwtAuthenticationToken; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public class SyncingJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final UserDetailsService userDetailsService; private final UserSyncService userSyncService; private final GroupSyncService groupSyncService; public SyncingJwtAuthenticationConverter(UserDetailsService userDetailsService, UserSyncService userSyncService, GroupSyncService groupSyncService) { this.userDetailsService = userDetailsService; this.userSyncService = userSyncService; this.groupSyncService = groupSyncService; } @Override public AbstractAuthenticationToken convert(Jwt source) { this.logger.trace("convert({}, {})", source.getId(), source.getClaim("email")); try { this.logger.debug("jwt: {}", new ObjectMapper().registerModule(new JavaTimeModule()).writeValueAsString(source)); } catch (JsonProcessingException jpe) { this.logger.error("error", jpe); } this.userSyncService.sync(source); this.groupSyncService.sync(source); UserDetails springUser = this.userDetailsService.loadUserByUsername(source.getClaim("email")); return new JwtAuthenticationToken( springUser, new ArrayList<>(springUser.getAuthorities())); } }