v2.1.x; added APS API support
This commit is contained in:
parent
84d1b4ea2f
commit
faba551a2d
3
pom.xml
3
pom.xml
@ -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>
|
||||
@ -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 \
|
||||
|
@ -1,5 +1,7 @@
|
||||
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;
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
@ -33,6 +40,9 @@ public class IdentityServiceConfigurationOverride {
|
||||
@Autowired
|
||||
private ApplicationContext appContext;
|
||||
|
||||
@Autowired
|
||||
private JwtAuthenticationProvider jwtAuthenticationProvider;
|
||||
|
||||
@Bean("inteligr8.clientRegistrationRepository")
|
||||
@Primary
|
||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
@ -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());
|
||||
|
@ -0,0 +1,11 @@
|
||||
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();
|
||||
|
||||
}
|
50
src/main/java/com/inteligr8/activiti/auth/service/SyncingJwtAuthenticationConverter.java
Normal file
50
src/main/java/com/inteligr8/activiti/auth/service/SyncingJwtAuthenticationConverter.java
Normal file
@ -0,0 +1,50 @@
|
||||
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()));
|
||||
}
|
||||
|
||||
}
|
27
src/main/java/com/inteligr8/activiti/auth/service/SyncingJwtAuthenticationProvider.java
Normal file
27
src/main/java/com/inteligr8/activiti/auth/service/SyncingJwtAuthenticationProvider.java
Normal file
@ -0,0 +1,27 @@
|
||||
package com.inteligr8.activiti.auth.service;
|
||||
|
||||
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.UserDetailsService;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SyncingJwtAuthenticationProvider implements JwtAuthenticationProvider {
|
||||
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
@Autowired
|
||||
private UserSyncService userSyncService;
|
||||
|
||||
@Autowired
|
||||
private GroupSyncService groupSyncService;
|
||||
|
||||
@Override
|
||||
public Converter<Jwt, AbstractAuthenticationToken> create() {
|
||||
return new SyncingJwtAuthenticationConverter(this.userDetailsService, this.userSyncService, this.groupSyncService);
|
||||
}
|
||||
|
||||
}
|
@ -20,12 +20,16 @@ import com.activiti.security.identity.service.config.IdentityServiceKeycloakProp
|
||||
* Activiti Identity Service configuration is enabled. When it isn't
|
||||
* enabled, it will still serve as the default OIDC user service for
|
||||
* Spring Security.
|
||||
*
|
||||
* This is only executed with non-API authentication and authorization use
|
||||
* cases. API authentication/authorization uses the
|
||||
* `SyncingJwtAuthenitcationConverter`.
|
||||
*/
|
||||
@Component
|
||||
@Primary
|
||||
public class OIDCUserService extends OidcUserService {
|
||||
public class SyncingUserService extends OidcUserService {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(OIDCUserService.class);
|
||||
private final Logger logger = LoggerFactory.getLogger(SyncingUserService.class);
|
||||
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
@ -9,7 +9,10 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
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;
|
||||
@ -49,7 +52,7 @@ public class UserSyncService {
|
||||
protected boolean clearNewUserGroups;
|
||||
|
||||
public void sync(OidcUser oidcUser) {
|
||||
UserDetails springUser = this.loadSpringUser(oidcUser);
|
||||
UserDetails springUser = this.loadSpringUser(oidcUser.getEmail(), oidcUser.getGivenName(), oidcUser.getFamilyName(), oidcUser);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Loaded Spring Security user: {}: {}", springUser.getUsername(), springUser.getAuthorities());
|
||||
} else {
|
||||
@ -57,42 +60,58 @@ public class UserSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
private UserDetails loadSpringUser(OidcUser oidcUser) throws UsernameNotFoundException {
|
||||
public void sync(Jwt jwt) {
|
||||
String email = jwt.getClaim(StandardClaimNames.EMAIL);
|
||||
if (email == null)
|
||||
throw new IllegalArgumentException("An '" + StandardClaimNames.EMAIL + "' claim is required");
|
||||
|
||||
String givenName = jwt.getClaim(StandardClaimNames.GIVEN_NAME);
|
||||
String familyName = jwt.getClaim(StandardClaimNames.FAMILY_NAME);
|
||||
|
||||
UserDetails springUser = this.loadSpringUser(email, givenName, familyName, jwt);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Loaded Spring Security user: {}: {}", springUser.getUsername(), springUser.getAuthorities());
|
||||
} else {
|
||||
this.logger.debug("Loaded Spring Security user: {}", springUser.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
private UserDetails loadSpringUser(String email, String givenName, String familyName, ClaimAccessor claims) throws UsernameNotFoundException {
|
||||
try {
|
||||
UserDetails springUser = this.userDetailsService.loadUserByUsername(oidcUser.getEmail());
|
||||
this.logger.debug("Loaded APS user: {} => {}", oidcUser.getEmail(), springUser.getUsername());
|
||||
UserDetails springUser = this.userDetailsService.loadUserByUsername(email);
|
||||
this.logger.debug("Loaded APS user: {} => {}", email, springUser.getUsername());
|
||||
return springUser;
|
||||
} catch (UsernameNotFoundException unfe) {
|
||||
this.logger.debug("User does not exist: {}", unfe.getMessage());
|
||||
if (!this.createMissingUser)
|
||||
throw unfe;
|
||||
|
||||
if (this.requiredGroup != null && (!oidcUser.hasClaim("groups") || !oidcUser.getClaimAsStringList("groups").contains(this.requiredGroup))) {
|
||||
this.logger.info("User does not exist and does not have the required OIDC group to be created: {} ", oidcUser.getEmail(), this.requiredGroup);
|
||||
if (this.requiredGroup != null && (!claims.hasClaim("groups") || !claims.getClaimAsStringList("groups").contains(this.requiredGroup))) {
|
||||
this.logger.info("User does not exist and does not have the required OIDC group to be created: {} ", email, this.requiredGroup);
|
||||
throw unfe;
|
||||
}
|
||||
|
||||
this.logger.debug("User does not exist; will attempt to create: {}", oidcUser.getEmail());
|
||||
User apsUser = this.createApsUser(oidcUser);
|
||||
this.logger.debug("User does not exist; will attempt to create: {}", email);
|
||||
User apsUser = this.createApsUser(email, givenName, familyName);
|
||||
if (this.clearNewUserGroups) {
|
||||
apsUser = this.userService.getUser(apsUser.getId(), true);
|
||||
if (this.logger.isDebugEnabled())
|
||||
this.logger.debug("User is new; clearing default groups: {}: {}", oidcUser.getEmail(), apsUser.getGroups().stream().map(group -> group.getName()).toList());
|
||||
this.logger.debug("User is new; clearing default groups: {}: {}", email, apsUser.getGroups().stream().map(group -> group.getName()).toList());
|
||||
this.deleteApsUserGroups(apsUser);
|
||||
}
|
||||
return this.userDetailsService.loadByUserId(apsUser.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private User createApsUser(OidcUser oidcUser) {
|
||||
private User createApsUser(String email, String givenName, String familyName) {
|
||||
long tenantId = this.tenantFinderService.findTenantId();
|
||||
|
||||
User user = this.userService.createNewUserFromExternalStore(
|
||||
oidcUser.getEmail(),
|
||||
oidcUser.getGivenName(),
|
||||
oidcUser.getFamilyName(),
|
||||
email,
|
||||
givenName,
|
||||
familyName,
|
||||
tenantId,
|
||||
oidcUser.getEmail(),
|
||||
email,
|
||||
this.externalIdmSource,
|
||||
new Date());
|
||||
this.logger.info("Created user: {} => {}", user.getId(), user.getEmail());
|
||||
|
Loading…
x
Reference in New Issue
Block a user