diff --git a/backend/build.gradle b/backend/build.gradle index e2e080dbc..6c63a7d14 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -31,7 +31,7 @@ repositories { } tasks.withType(War) { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE + duplicatesStrategy = DuplicatesStrategy.INCLUDE } configurations.all { @@ -231,9 +231,11 @@ dependencies { // CSV file support implementation "com.opencsv:opencsv:${project.'opencsvVersion'}", { exclude group: 'commons-collections' + exclude group: 'commons-lang3' } implementation "org.apache.commons:commons-collections4:${project.'commonsCollections4Version'}" + implementation "org.apache.commons:commons-lang3:${project.'commonsLang3Version'}" // Envers for persistent entities versioning implementation "org.hibernate:hibernate-envers:${project.'hibernateVersion'}" diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java index e706f1b45..e8a6042da 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java @@ -32,7 +32,7 @@ @EnableJpaAuditing @EnableScheduling @EnableAsync -@OpenAPIDefinition(info=@Info(description = "The Shibboleth UI is specifically designed to help manage and edit metadata-driven configuration support for Shibboleth", title = "Shibboleth UI API", version = "1.0")) +@OpenAPIDefinition(info=@Info(description = "The SAML Metadata Configuration Manager is specifically designed to help manage and edit metadata-driven configuration support", title = "SAML Metadata Configuration Manager API", version = "2.0")) public class ShibbolethUiApplication extends SpringBootServletInitializer { private static final Logger logger = LoggerFactory.getLogger(ShibbolethUiApplication.class); diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index c2845b54e..32b4833f0 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -128,7 +128,7 @@ shibui.pac4j-enabled=false #environment variables must be set for beacon publisher to be used (the ones that are set when running shib-ui in #docker container shibui.beacon.enabled=true -shibui.beacon.productName=ShibUi +shibui.beacon.productName=SAML Metadata Configuration Manager shibui.beacon.installationID=UNICON-SHIBUI-TESTING shibui.beacon.url=http://collector.testbed.tier.internet2.edu:5001 #shibui.beacon.send.cron=0 59 3 * * ? diff --git a/gradle.properties b/gradle.properties index ac75875b0..772fae0fe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,11 @@ name=shibui group=edu.internet2.tier.shibboleth.admin.ui -version=2.0.0-BETA-R2 +version=2.0.0-BETA-RC5 ### library versions ### ## As of 2-23-23 commonsCollections4Version=4.4 +commonsLang3Version=3.14.0 comsunjaxbVersion=4.0.2 cryptacularVersion=1.2.5 groovyVersion=4.0.9 @@ -15,8 +16,8 @@ lombokVersion=6.6.2 nashornVersion=15.4 opencsvVersion=5.7.1 opensamlVersion=5.0.0 -pac4JVersion=5.7.0 -pac4jSpringSecurityVersion=9.0.0 +pac4JVersion=6.0.0 +pac4jSpringSecurityVersion=10.0.0 seleneseRunnerVersion=4.3.0 shedlockVersion=5.2.0 shibbolethVersion=5.0.0 diff --git a/pac4j-module/build.gradle b/pac4j-module/build.gradle index 70e420eb8..e6193d229 100644 --- a/pac4j-module/build.gradle +++ b/pac4j-module/build.gradle @@ -47,6 +47,7 @@ dependencies { implementation "org.pac4j:spring-security-pac4j:${project.'pac4jSpringSecurityVersion'}" implementation "org.pac4j:pac4j-core:${project.'pac4JVersion'}" implementation "org.pac4j:pac4j-http:${project.'pac4JVersion'}" + implementation "org.pac4j:pac4j-jakartaee:${project.'pac4JVersion'}" implementation "org.pac4j:pac4j-saml:${project.'pac4JVersion'}", { // opensaml libraries are provided exclude group: 'org.opensaml' @@ -57,7 +58,7 @@ dependencies { implementation "org.opensaml:opensaml-storage-impl:${project.'opensamlVersion'}" implementation "org.apache.commons:commons-collections4:${project.'commonsCollections4Version'}" // As listed by - https://github.com/pac4j/spring-security-pac4j/wiki/Dependencies - implementation "org.pac4j:jakartaee-pac4j:7.1.0" + //implementation "org.pac4j:jakartaee-pac4j:7.1.0" changed to pac4j-jakartaee and released with core numbering... testImplementation "jakarta.servlet:jakarta.servlet-api:6.0.0" testImplementation project(':backend') diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java index fb757313f..45cf31906 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java @@ -20,6 +20,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; +import org.pac4j.core.context.CallContext; import org.pac4j.core.context.WebContext; import org.pac4j.core.context.session.SessionStore; import org.pac4j.core.matching.matcher.Matcher; @@ -106,9 +107,9 @@ public void destroy() { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { WebContext context = new JEEContext((HttpServletRequest)request, (HttpServletResponse)response); - Optional optionalSession = JEESessionStore.INSTANCE.buildFromTrackableSession(context, ((HttpServletRequest) request).getSession()); + Optional optionalSession = new JEESessionStore().buildFromTrackableSession(context, ((HttpServletRequest) request).getSession()); SessionStore session = optionalSession.isPresent() ? (SessionStore) optionalSession.get() : null; - if (!matcher.matches(context, session)) { + if (!matcher.matches(new CallContext(context, session))) { return; } Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/LocalUserProfileAuthorizationGenerator.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/LocalUserProfileAuthorizationGenerator.java index 4ec901998..06d3e5f26 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/LocalUserProfileAuthorizationGenerator.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/LocalUserProfileAuthorizationGenerator.java @@ -3,6 +3,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.User; import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; import org.pac4j.core.authorization.generator.AuthorizationGenerator; +import org.pac4j.core.context.CallContext; import org.pac4j.core.context.WebContext; import org.pac4j.core.context.session.SessionStore; import org.pac4j.core.profile.UserProfile; @@ -17,7 +18,7 @@ public LocalUserProfileAuthorizationGenerator(UserRepository userRepository) { } @Override - public Optional generate(WebContext context, SessionStore sessionStore, UserProfile profile) { + public Optional generate(CallContext callContext, UserProfile profile) { Optional user = userRepository.findByUsername(profile.getUsername()); user.ifPresent(u -> profile.addRole(u.getRole())); return Optional.of(profile); diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4JHttpServletRequestWrapper.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4JHttpServletRequestWrapper.java new file mode 100644 index 000000000..d7ac6ad54 --- /dev/null +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4JHttpServletRequestWrapper.java @@ -0,0 +1,45 @@ +package net.unicon.shibui.pac4j; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import org.pac4j.core.profile.ProfileHelper; +import org.pac4j.core.profile.UserProfile; + +import java.security.Principal; +import java.util.Collection; +import java.util.Optional; + +/** + * FROM: https://github.com/pac4j/jee-pac4j/blob/master/jakartaee-pac4j/src/main/java/org/pac4j/jee/util/Pac4JHttpServletRequestWrapper.java + */ +public class Pac4JHttpServletRequestWrapper extends HttpServletRequestWrapper { + private Collection profiles; + + public Pac4JHttpServletRequestWrapper(final HttpServletRequest request, final Collection profiles) { + super(request); + this.profiles = profiles; + } + + @Override + public String getRemoteUser() { + return getPrincipal().map(p -> p.getName()).orElse(null); + } + + private Optional getProfile() { + return ProfileHelper.flatIntoOneProfile(profiles); + } + + private Optional getPrincipal() { + return getProfile().map(UserProfile::asPrincipal); + } + + @Override + public Principal getUserPrincipal() { + return getPrincipal().orElse(null); + } + + @Override + public boolean isUserInRole(String role) { + return this.profiles.stream().anyMatch(p -> p.getRoles().contains(role)); + } +} \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java index c31a0d201..d04b468ab 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java @@ -5,19 +5,14 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import lombok.extern.slf4j.Slf4j; import net.unicon.shibui.pac4j.authenticator.ShibuiPac4JHeaderClientAuthenticator; -import net.unicon.shibui.pac4j.authenticator.ShibuiSAML2Authenticator; import org.pac4j.core.client.Clients; import org.pac4j.core.config.Config; import org.pac4j.core.context.WebContext; import org.pac4j.core.context.session.SessionStore; import org.pac4j.core.matching.matcher.PathMatcher; import org.pac4j.core.profile.ProfileManager; -import org.pac4j.core.profile.definition.CommonProfileDefinition; import org.pac4j.core.profile.factory.ProfileManagerFactory; import org.pac4j.http.client.direct.HeaderClient; -import org.pac4j.saml.client.SAML2Client; -import org.pac4j.saml.config.SAML2Configuration; -import org.pac4j.saml.credentials.authenticator.SAML2Authenticator; import org.pac4j.springframework.security.profile.SpringSecurityProfileManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; @@ -53,8 +48,7 @@ public LocalUserProfileAuthorizationGenerator saml2ModelAuthorizationGenerator(U } @Bean(name = "pac4j-config") - public Config config(final Pac4jConfigurationProperties pac4jConfigProps, - final LocalUserProfileAuthorizationGenerator saml2ModelAuthorizationGenerator) { + public Config pac4jConfig(final Pac4jConfigurationProperties pac4jConfigProps, final LocalUserProfileAuthorizationGenerator saml2ModelAuthorizationGenerator) { log.info("**** Configuring PAC4J "); final Config config = new Config(); final Clients clients = new Clients(pac4jConfigProps.getCallbackUrl()); @@ -63,7 +57,7 @@ public Config config(final Pac4jConfigurationProperties pac4jConfigProps, PathMatcher pm = new PathMatcher(); pm.setExcludedPaths(Lists.newArrayList("/favicon.ico", "/unsecured/**/*", "/assets/**/*.png", "/static/**/*")); config.addMatcher("exclude-paths-matcher", pm); - config.defaultProfileManagerFactory(new ProfileManagerFactory() { + config.setProfileManagerFactory(new ProfileManagerFactory() { @Override public ProfileManager apply(WebContext webContext, SessionStore sessionStore) { return new SpringSecurityProfileManager(webContext, sessionStore); @@ -82,15 +76,9 @@ public ProfileManager apply(WebContext webContext, SessionStore sessionStore) { case "SAML2": default: log.info("**** Configuring PAC4J SAML2"); - final SAML2Configuration saml2Config = buildSaml2ConfigFromPac4JConfiguration(pac4jConfigProps); - - - final SAML2Client saml2Client = new SAML2Client(saml2Config); + final ShibuiSAML2Client saml2Client = new ShibuiSAML2Client(pac4jConfigProps, userService); saml2Client.setName(PAC4J_CLIENT_NAME); saml2Client.addAuthorizationGenerator(saml2ModelAuthorizationGenerator); - SAML2Authenticator saml2Authenticator = new ShibuiSAML2Authenticator(saml2Config.getAttributeAsId(), saml2Config.getMappedAttributes(), userService); - saml2Authenticator.setProfileDefinition(new CommonProfileDefinition(p -> new BetterSAML2Profile(pac4jConfigProps.getSimpleProfileMapping()))); - saml2Client.setAuthenticator(saml2Authenticator); saml2Client.setCallbackUrl(pac4jConfigProps.getCallbackUrl()); saml2Client.init(); @@ -101,24 +89,6 @@ public ProfileManager apply(WebContext webContext, SessionStore sessionStore) { return config; } - private SAML2Configuration buildSaml2ConfigFromPac4JConfiguration(Pac4jConfigurationProperties pac4jConfigProps) { - SAML2Configuration saml2Config = new SAML2Configuration(); - saml2Config.setKeystorePath(pac4jConfigProps.getKeystorePath()); - saml2Config.setKeystorePassword(pac4jConfigProps.getKeystorePassword()); - saml2Config.setPrivateKeyPassword(pac4jConfigProps.getPrivateKeyPassword()); - saml2Config.setIdentityProviderMetadataPath(pac4jConfigProps.getIdentityProviderMetadataPath()); - saml2Config.setMaximumAuthenticationLifetime(pac4jConfigProps.getMaximumAuthenticationLifetime()); - saml2Config.setServiceProviderEntityId(pac4jConfigProps.getServiceProviderEntityId()); - saml2Config.setServiceProviderMetadataPath(pac4jConfigProps.getServiceProviderMetadataPath()); - saml2Config.setForceServiceProviderMetadataGeneration(pac4jConfigProps.isForceServiceProviderMetadataGeneration()); - saml2Config.setWantsAssertionsSigned(pac4jConfigProps.isWantAssertionsSigned()); - saml2Config.setAttributeAsId(pac4jConfigProps.getSimpleProfileMapping().getUsername()); - saml2Config.setPostLogoutURL(pac4jConfigProps.getPostLogoutURL()); - saml2Config.setAuthnRequestBindingType("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); - - return saml2Config; - } - @Bean public ErrorPageRegistrar errorPageRegistrar() { return this::registerErrorPages; diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jSpringSecurityConfig.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jSpringSecurityConfig.java index 2d725a83a..4f60c9925 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jSpringSecurityConfig.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jSpringSecurityConfig.java @@ -7,11 +7,11 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.IRolesService; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.ui.service.EmailService; +import jakarta.servlet.Filter; import org.apache.commons.lang3.StringUtils; import org.pac4j.core.authorization.authorizer.DefaultAuthorizers; import org.pac4j.core.config.Config; import org.pac4j.core.matching.matcher.Matcher; -import org.pac4j.jee.filter.SecurityFilter; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureOrder; @@ -86,7 +86,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { logoutFilter.setCentralLogout(Boolean.TRUE); logoutFilter.setDefaultUrl(pac4jConfigurationProperties.getPostLogoutURL()); logoutFilter.setDestroySession(true); - http.securityMatcher("/login*", "/logout").addFilterBefore(logoutFilter, BasicAuthenticationFilter.class); + http.securityMatcher("/login").addFilterBefore((Filter) logoutFilter, BasicAuthenticationFilter.class); } // add correct auth filter diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/SecurityFilter.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/SecurityFilter.java new file mode 100644 index 000000000..b0b72caf4 --- /dev/null +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/SecurityFilter.java @@ -0,0 +1,78 @@ +package net.unicon.shibui.pac4j; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import lombok.Getter; +import lombok.Setter; +import org.pac4j.core.adapter.FrameworkAdapter; +import org.pac4j.core.config.Config; +import org.pac4j.core.profile.ProfileHelper; +import org.pac4j.core.profile.UserProfile; +import org.pac4j.core.util.Pac4jConstants; +import org.pac4j.core.util.security.SecurityEndpoint; +import org.pac4j.core.util.security.SecurityEndpointBuilder; +import org.pac4j.jee.config.AbstractConfigFilter; +import org.pac4j.jee.context.JEEFrameworkParameters; + +import java.io.IOException; +import java.security.Principal; +import java.util.Collection; +import java.util.Optional; + +@Getter +@Setter +public class SecurityFilter extends AbstractConfigFilter implements SecurityEndpoint { + private String clients; + private String authorizers; + private String matchers; + public SecurityFilter() {} + + public SecurityFilter(final Config config) { + setConfig(config); + } + + public SecurityFilter(final Config config, final String clients) { + this(config); + this.clients = clients; + } + + public SecurityFilter(final Config config, final String clients, final String authorizers) { + this(config, clients); + this.authorizers = authorizers; + } + + public SecurityFilter(final Config config, final String clients, final String authorizers, final String matchers) { + this(config, clients, authorizers); + this.matchers = matchers; + } + + public static SecurityFilter build(final Object... parameters) { + final SecurityFilter securityFilter = new SecurityFilter(); + SecurityEndpointBuilder.buildConfig(securityFilter, parameters); + return securityFilter; + } + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + super.init(filterConfig); + + this.clients = getStringParam(filterConfig, Pac4jConstants.CLIENTS, this.clients); + this.authorizers = getStringParam(filterConfig, Pac4jConstants.AUTHORIZERS, this.authorizers); + this.matchers = getStringParam(filterConfig, Pac4jConstants.MATCHERS, this.matchers); + } + + @Override + protected final void internalFilter(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws IOException, ServletException { + Config config = getSharedConfig(); + FrameworkAdapter.INSTANCE.applyDefaultSettingsIfUndefined(config); + config.getSecurityLogic().perform(config, (ctx, session, profiles) -> { + // if no profiles are loaded, pac4j is not concerned with this request + filterChain.doFilter(profiles.isEmpty() ? request : new Pac4JHttpServletRequestWrapper(request, profiles), response); + return null; + }, clients, authorizers, matchers, new JEEFrameworkParameters(request, response)); + } +} \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/ShibuiCallbackFilter.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/ShibuiCallbackFilter.java index 88ec6d822..1963de981 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/ShibuiCallbackFilter.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/ShibuiCallbackFilter.java @@ -1,45 +1,85 @@ package net.unicon.shibui.pac4j; import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.pac4j.core.adapter.FrameworkAdapter; import org.pac4j.core.config.Config; +import org.pac4j.core.context.CallContext; import org.pac4j.core.context.WebContext; -import org.pac4j.core.util.FindBest; +import org.pac4j.core.context.WebContextFactory; +import org.pac4j.core.engine.DefaultCallbackLogic; +import org.pac4j.core.exception.http.FoundAction; +import org.pac4j.core.exception.http.HttpAction; +import org.pac4j.core.exception.http.SeeOtherAction; +import org.pac4j.core.util.CommonHelper; +import org.pac4j.core.util.Pac4jConstants; +import org.pac4j.jee.config.AbstractConfigFilter; import org.pac4j.jee.context.JEEContextFactory; -import org.pac4j.jee.filter.CallbackFilter; +import org.pac4j.jee.context.JEEFrameworkParameters; import java.io.IOException; import static org.pac4j.core.util.CommonHelper.isBlank; /** - * CallbackFilter changes between Pac4j versions would not emulate previous behaviors, so roughly the previous logic was incorporated - * in order to get the behaviors we were seeing before. + * CallbackFilter was part of the jakartee-pac4j stuff - there were a number of changes when Pac4J shifted to v6 (the j2ee stuff is now core + * as pac4j-jakartee and its not at all confusing). * * Essentially, we check to see if the filter matches the right pattern - this should be done by the Spring mechanisms, but the configured filters - * were still being called in the filter chain, so this logic was re-introduced here. + * were still being called in the filter chain, so this logic was re-introduced here. This is essentially a copy/expansion of - + * https://github.com/pac4j/jee-pac4j/blob/master/jakartaee-pac4j/src/main/java/org/pac4j/jee/filter/CallbackFilter.java re-worked a little to match + * the v6+ pac4j stuff... */ -public class ShibuiCallbackFilter extends CallbackFilter { +public class ShibuiCallbackFilter extends AbstractConfigFilter { private String suffix = "/callback"; + private String defaultUrl = "/dashboard"; + private Boolean renewSession; + private String defaultClient; public ShibuiCallbackFilter(Config config) { - super(config); + // Added this because we were seeing odd behavior where the favicon request was getting in the mix and the return to the + // dashboard url was getting lost. + config.setCallbackLogicIfUndefined(new DefaultCallbackLogic() { + @Override + protected HttpAction redirectToOriginallyRequestedUrl(CallContext ctx, String defaultUrl) { + HttpAction action = super.redirectToOriginallyRequestedUrl(ctx, defaultUrl); + if (action instanceof SeeOtherAction && ((SeeOtherAction) action).getLocation().contains("favicon")) { + return new FoundAction(defaultUrl); + } + return action; + } + }); + setConfig(config); + } + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + super.init(filterConfig); + this.renewSession = getBooleanParam(filterConfig, Pac4jConstants.RENEW_SESSION, this.renewSession); + this.defaultClient = getStringParam(filterConfig, Pac4jConstants.DEFAULT_CLIENT, this.defaultClient); } @Override protected void internalFilter(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException { final Config config = getSharedConfig(); - final WebContext context = FindBest.webContextFactory(null, config, JEEContextFactory.INSTANCE).newContext(request, response); + final WebContext context = findBestWebContextFactory(null, config, JEEContextFactory.INSTANCE).newContext(new JEEFrameworkParameters(request, response)); final boolean mustApply = mustApply(context); if (mustApply) { - super.internalFilter(request, response, chain); + mustApplyInternalFilter(request, response, chain); } else { chain.doFilter(request, response); } } + private void mustApplyInternalFilter(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException { + Config config = getSharedConfig(); + FrameworkAdapter.INSTANCE.applyDefaultSettingsIfUndefined(getSharedConfig()); + config.getCallbackLogic().perform(config, defaultUrl, renewSession, defaultClient, new JEEFrameworkParameters(request, response)); + } + protected boolean mustApply(final WebContext context) { final String path = context.getPath(); logger.debug("path: {} | suffix: {}", path, suffix); @@ -50,4 +90,15 @@ protected boolean mustApply(final WebContext context) { return path != null && path.endsWith(suffix); } } + + private WebContextFactory findBestWebContextFactory(final WebContextFactory localFactory, final Config config, final WebContextFactory defaultFactory) { + if (localFactory != null) { + return localFactory; + } else if (config != null && config.getWebContextFactory() != null) { + return config.getWebContextFactory(); + } else { + CommonHelper.assertNotNull("defaultFactory", defaultFactory); + return defaultFactory; + } + } } \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/ShibuiLogoutFilter.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/ShibuiLogoutFilter.java index c075b91f6..4ad6e3ac4 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/ShibuiLogoutFilter.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/ShibuiLogoutFilter.java @@ -1,19 +1,26 @@ package net.unicon.shibui.pac4j; import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.Getter; +import lombok.Setter; +import org.pac4j.core.adapter.FrameworkAdapter; import org.pac4j.core.config.Config; import org.pac4j.core.context.WebContext; +import org.pac4j.core.context.WebContextFactory; import org.pac4j.core.context.session.SessionStore; import org.pac4j.core.engine.DefaultLogoutLogic; import org.pac4j.core.engine.LogoutLogic; import org.pac4j.core.http.adapter.HttpActionAdapter; -import org.pac4j.core.util.FindBest; +import org.pac4j.core.util.CommonHelper; +import org.pac4j.core.util.Pac4jConstants; +import org.pac4j.jee.config.AbstractConfigFilter; import org.pac4j.jee.context.JEEContextFactory; +import org.pac4j.jee.context.JEEFrameworkParameters; import org.pac4j.jee.context.session.JEESessionStoreFactory; -import org.pac4j.jee.filter.LogoutFilter; import org.pac4j.jee.http.adapter.JEEHttpActionAdapter; import java.io.IOException; @@ -21,40 +28,46 @@ import static org.pac4j.core.util.CommonHelper.isBlank; /** - * LogoutFilter changes between Pac4j versions would not emulate previous behaviors, so roughly the previous logic was incorporated - * in order to get the behaviors we were seeing before. + * LogoutFilter was part of the jakartee-pac4j stuff - there were a number of changes when Pac4J shifted to v6 (the j2ee stuff is now core + * as pac4j-jakartee and its not at all confusing). * - * Essentially, we check to see if the filter matches the right pattern - this should be done by the Spring mechanisms, but the configured filters - * were still being called in the filter chain, so this logic was re-introduced here. + * Essentially, we check to see if the filter matches the right pattern - because of how we re-rout "/logout" before it even gets to + * the filters, we have this filter in place to check for the "/login/logout" which will then do logout behaviors. + * This class is essentially a modification of - + * https://github.com/pac4j/jee-pac4j/blob/master/jakartaee-pac4j/src/main/java/org/pac4j/jee/filter/LogoutFilter.java */ -public class ShibuiLogoutFilter extends LogoutFilter { - private final static String SUFFIX = "login"; // "logout" is redirected before we ever hit the filters - sent to /login?logout; +@Getter +@Setter +public class ShibuiLogoutFilter extends AbstractConfigFilter { + private String defaultUrl; + private String logoutUrlPattern; + private Boolean localLogout; + private Boolean destroySession; + private Boolean centralLogout; public ShibuiLogoutFilter(Config config) { - super(config); + setConfig(config); } - private boolean mustApply(final WebContext context) { - final String path = context.getPath(); - logger.debug("path: {} | suffix: {}", path, SUFFIX); + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + super.init(filterConfig); - if (isBlank(SUFFIX)) { - return true; - } else { - return path != null && path.endsWith(SUFFIX); - } + this.defaultUrl = getStringParam(filterConfig, Pac4jConstants.DEFAULT_URL, this.defaultUrl); + this.logoutUrlPattern = getStringParam(filterConfig, Pac4jConstants.LOGOUT_URL_PATTERN, this.logoutUrlPattern); + this.localLogout = getBooleanParam(filterConfig, Pac4jConstants.LOCAL_LOGOUT, this.localLogout); + this.destroySession = getBooleanParam(filterConfig, Pac4jConstants.DESTROY_SESSION, this.destroySession); + this.centralLogout = getBooleanParam(filterConfig, Pac4jConstants.CENTRAL_LOGOUT, this.centralLogout); } @Override protected void internalFilter(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException { - final Config config = getSharedConfig(); - final SessionStore bestSessionStore = FindBest.sessionStoreFactory(null, config, JEESessionStoreFactory.INSTANCE).newSessionStore(request, response); - final HttpActionAdapter bestAdapter = FindBest.httpActionAdapter(null, config, JEEHttpActionAdapter.INSTANCE); - final LogoutLogic bestLogic = FindBest.logoutLogic(getLogoutLogic(), config, DefaultLogoutLogic.INSTANCE); - - final WebContext context = FindBest.webContextFactory(null, config, JEEContextFactory.INSTANCE).newContext(request, response); - if (mustApply(context)) { - bestLogic.perform(context, bestSessionStore, config, bestAdapter, getDefaultUrl(), getLogoutUrlPattern(), getLocalLogout(), getDestroySession(), getCentralLogout()); + // the actual "/logout url is redirected before the filters every get anything. It hits /login?logout - so this filter should only + // act when the QUERY STRING is "logout" + if (request.getQueryString() != null && request.getQueryString().endsWith("logout")) { + Config config = getSharedConfig(); + FrameworkAdapter.INSTANCE.applyDefaultSettingsIfUndefined(config); + config.getLogoutLogic().perform(config, defaultUrl, logoutUrlPattern, localLogout, destroySession, centralLogout, new JEEFrameworkParameters(request, response)); } else { chain.doFilter(request, response); } diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/ShibuiSAML2Client.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/ShibuiSAML2Client.java new file mode 100644 index 000000000..bbdcd4544 --- /dev/null +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/ShibuiSAML2Client.java @@ -0,0 +1,48 @@ +package net.unicon.shibui.pac4j; + +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import net.unicon.shibui.pac4j.authenticator.ShibuiSAML2Authenticator; +import org.pac4j.core.credentials.authenticator.Authenticator; +import org.pac4j.core.profile.definition.CommonProfileDefinition; +import org.pac4j.saml.client.SAML2Client; +import org.pac4j.saml.config.SAML2Configuration; + +/** + * Wrapper/extention of the SAML2Client in order to make creating the SAML2Authenticator, that we need, easier. + */ +public class ShibuiSAML2Client extends SAML2Client { + private Pac4jConfigurationProperties.SimpleProfileMapping simpleProfileMapping; + private UserService userService; + + public ShibuiSAML2Client(Pac4jConfigurationProperties pac4jConfigProps, UserService userService) { + setConfiguration(buildSaml2ConfigFromPac4JConfiguration(pac4jConfigProps)); + this.userService = userService; + this.simpleProfileMapping = pac4jConfigProps.getSimpleProfileMapping(); + } + + @Override + protected void setAuthenticatorIfUndefined(Authenticator authenticator) { + ShibuiSAML2Authenticator authenticatorToUse = new ShibuiSAML2Authenticator(this.authnResponseValidator, this.logoutValidator, this.configuration.getAttributeAsId(), this.configuration.getMappedAttributes(), userService); + authenticatorToUse.setProfileDefinition(new CommonProfileDefinition(p -> new BetterSAML2Profile(simpleProfileMapping))); + setAuthenticator(authenticatorToUse); + } + + private SAML2Configuration buildSaml2ConfigFromPac4JConfiguration(Pac4jConfigurationProperties pac4jConfigProps) { + SAML2Configuration saml2Config = new SAML2Configuration(); + saml2Config.setKeystorePath(pac4jConfigProps.getKeystorePath()); + saml2Config.setKeystorePassword(pac4jConfigProps.getKeystorePassword()); + saml2Config.setPrivateKeyPassword(pac4jConfigProps.getPrivateKeyPassword()); + saml2Config.setIdentityProviderMetadataPath(pac4jConfigProps.getIdentityProviderMetadataPath()); + saml2Config.setMaximumAuthenticationLifetime(pac4jConfigProps.getMaximumAuthenticationLifetime()); + saml2Config.setServiceProviderEntityId(pac4jConfigProps.getServiceProviderEntityId()); + saml2Config.setServiceProviderMetadataPath(pac4jConfigProps.getServiceProviderMetadataPath()); + saml2Config.setForceServiceProviderMetadataGeneration(pac4jConfigProps.isForceServiceProviderMetadataGeneration()); + saml2Config.setWantsAssertionsSigned(pac4jConfigProps.isWantAssertionsSigned()); + saml2Config.setAttributeAsId(pac4jConfigProps.getSimpleProfileMapping().getUsername()); + saml2Config.setPostLogoutURL(pac4jConfigProps.getPostLogoutURL()); + saml2Config.setAuthnRequestBindingType("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); + saml2Config.setCallbackUrl(pac4jConfigProps.getCallbackUrl()); + + return saml2Config; + } +} \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/authenticator/ShibuiPac4JHeaderClientAuthenticator.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/authenticator/ShibuiPac4JHeaderClientAuthenticator.java index 0c1a9e084..6657d068f 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/authenticator/ShibuiPac4JHeaderClientAuthenticator.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/authenticator/ShibuiPac4JHeaderClientAuthenticator.java @@ -3,14 +3,15 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; -import org.pac4j.core.context.WebContext; -import org.pac4j.core.context.session.SessionStore; +import org.pac4j.core.context.CallContext; import org.pac4j.core.credentials.Credentials; import org.pac4j.core.credentials.TokenCredentials; import org.pac4j.core.credentials.authenticator.Authenticator; import org.pac4j.core.exception.CredentialsException; import org.pac4j.core.profile.CommonProfile; +import java.util.Optional; + /** * Handles parsing the header tokens when using the Pac4J Header client */ @@ -19,23 +20,22 @@ public class ShibuiPac4JHeaderClientAuthenticator implements Authenticator { private UserService userService; @Override - public void validate(Credentials credentials, WebContext context, SessionStore sessionStore) { - { - if (credentials instanceof TokenCredentials) { - TokenCredentials creds = (TokenCredentials) credentials; - String token = creds.getToken(); - if (StringUtils.isAllBlank(token)) { - throw new CredentialsException("Supplied token value in header was missing or blank"); - } - } else { - throw new CredentialsException("Invalid Credentials object generated by HeaderClient"); + public Optional validate(CallContext context, Credentials credentials) { + if (credentials instanceof TokenCredentials) { + TokenCredentials creds = (TokenCredentials) credentials; + String token = creds.getToken(); + if (StringUtils.isAllBlank(token)) { + throw new CredentialsException("Supplied token value in header was missing or blank"); } - final CommonProfile profile = new CommonProfile(); - String token = ((TokenCredentials) credentials).getToken(); - profile.setId(token); - profile.addAttribute("username", token); - profile.setRoles(userService.getUserRoles(token)); - credentials.setUserProfile(profile); + } else { + throw new CredentialsException("Invalid Credentials object generated by HeaderClient"); } + final CommonProfile profile = new CommonProfile(); + String token = ((TokenCredentials) credentials).getToken(); + profile.setId(token); + profile.addAttribute("username", token); + profile.setRoles(userService.getUserRoles(token)); + credentials.setUserProfile(profile); + return Optional.of(credentials); } } \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/authenticator/ShibuiSAML2Authenticator.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/authenticator/ShibuiSAML2Authenticator.java index 110611f2d..4219112be 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/authenticator/ShibuiSAML2Authenticator.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/authenticator/ShibuiSAML2Authenticator.java @@ -1,33 +1,36 @@ package net.unicon.shibui.pac4j.authenticator; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; -import org.pac4j.core.context.WebContext; -import org.pac4j.core.context.session.SessionStore; +import org.pac4j.core.context.CallContext; import org.pac4j.core.credentials.Credentials; import org.pac4j.core.profile.CommonProfile; import org.pac4j.saml.credentials.authenticator.SAML2Authenticator; +import org.pac4j.saml.logout.impl.SAML2LogoutValidator; +import org.pac4j.saml.profile.api.SAML2ResponseValidator; import java.util.Map; +import java.util.Optional; public class ShibuiSAML2Authenticator extends SAML2Authenticator { private final UserService userService; - public ShibuiSAML2Authenticator(final String attributeAsId, final Map mappedAttributes, UserService userService) { - super(attributeAsId, mappedAttributes); + public ShibuiSAML2Authenticator(SAML2ResponseValidator loginValidator, SAML2LogoutValidator logoutValidator, String attributeAsId, Map mappedAttributes, UserService userService) { + super(loginValidator, logoutValidator, attributeAsId, mappedAttributes); this.userService = userService; } /** * After setting up the information for the user from the SAML, add user roles from the DB if they exist - * @param credentials - * @param context */ @Override - public void validate(final Credentials credentials, final WebContext context, final SessionStore sessionStore) { - super.validate(credentials, context, sessionStore); - CommonProfile profile = (CommonProfile) credentials.getUserProfile(); - profile.setRoles(userService.getUserRoles(profile.getUsername())); - credentials.setUserProfile(profile); - userService.updateLoginRecord(profile.getUsername()); + public Optional validate(CallContext ctx, Credentials credentials) { + Optional validatedCreds = super.validate(ctx, credentials); + validatedCreds.ifPresent(creds -> { + CommonProfile profile = (CommonProfile) creds.getUserProfile(); + profile.setRoles(userService.getUserRoles(profile.getUsername())); + creds.setUserProfile(profile); + userService.updateLoginRecord(profile.getUsername()); + }); + return validatedCreds; } } \ No newline at end of file