From 5c2ff8ca3314cf8fcc31c3aaa15ac4a4f07cfd7e Mon Sep 17 00:00:00 2001 From: Okke Harsta Date: Fri, 3 Nov 2023 15:22:13 +0100 Subject: [PATCH] First step in SAML migration - breaks everything Added TODO for saml migration Added TODO for saml migration WIP for SAML migration WIP for SAML migration WIP for SAML migration Start fixing all the broken tests... WIP for fixing test after sam-migration Fixed tests after saml migration --- .gitignore | 3 +- myconext-server/pom.xml | 50 ++-- .../main/java/myconext/config/BeanConfig.java | 122 ---------- .../main/java/myconext/config/NoopFilter.java | 12 - .../java/myconext/geo/MaxMindGeoLocation.java | 7 +- .../ResourceServiceProviderResolver.java | 8 +- .../ImmutableSamlConfigurationRepository.java | 33 --- .../security/GuestIdentityProviderDsl.java | 46 ---- .../GuestIdpAuthenticationRequestFilter.java | 222 ++++++++---------- .../security/SecurityConfiguration.java | 149 +++++++----- .../java/myconext/session/SessionConfig.java | 5 - .../java/myconext/sms/SMSServiceImpl.java | 7 +- .../src/main/resources/application.yml | 6 +- .../myconext/AbstractIntegrationTest.java | 11 +- .../java/myconext/api/UserControllerTest.java | 5 +- .../myconext/eduid/APIControllerTest.java | 7 +- .../myconext/geo/MaxMindGeoLocationTest.java | 9 +- ...estIdpAuthenticationRequestFilterTest.java | 43 ++-- .../myconext/tiqr/TiqrControllerTest.java | 4 +- pom.xml | 19 +- 20 files changed, 280 insertions(+), 488 deletions(-) delete mode 100644 myconext-server/src/main/java/myconext/config/BeanConfig.java delete mode 100644 myconext-server/src/main/java/myconext/config/NoopFilter.java delete mode 100644 myconext-server/src/main/java/myconext/saml/ImmutableSamlConfigurationRepository.java delete mode 100644 myconext-server/src/main/java/myconext/security/GuestIdentityProviderDsl.java diff --git a/.gitignore b/.gitignore index bb0a38c4..f6427d94 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ local.application.yml .classpath log NOTES.md -dep.tree \ No newline at end of file +dep.tree +application-test2.yml \ No newline at end of file diff --git a/myconext-server/pom.xml b/myconext-server/pom.xml index 5522ba56..5215b547 100644 --- a/myconext-server/pom.xml +++ b/myconext-server/pom.xml @@ -46,9 +46,9 @@ spring-boot-starter-web - org.springframework.security.extensions - spring-security-saml2-core - 2.0.0.M31 + org.openconext + saml-idp + 0.0.4-SNAPSHOT org.springframework.boot @@ -121,14 +121,14 @@ 0.9.10 - commons-io - commons-io - 20030203.000550 + org.apache.commons + commons-lang3 + 3.13.0 org.openconext tiqr-java-connector - 1.1.0 + 1.1.2 com.fasterxml.jackson.datatype @@ -226,24 +226,24 @@ org.springframework.boot spring-boot-maven-plugin - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + pl.project13.maven git-commit-id-plugin diff --git a/myconext-server/src/main/java/myconext/config/BeanConfig.java b/myconext-server/src/main/java/myconext/config/BeanConfig.java deleted file mode 100644 index 8c51f9f0..00000000 --- a/myconext-server/src/main/java/myconext/config/BeanConfig.java +++ /dev/null @@ -1,122 +0,0 @@ -package myconext.config; - -import myconext.geo.GeoLocation; -import myconext.mail.MailBox; -import myconext.manage.ServiceProviderResolver; -import myconext.repository.AuthenticationRequestRepository; -import myconext.repository.UserLoginRepository; -import myconext.repository.UserRepository; -import myconext.saml.ImmutableSamlConfigurationRepository; -import myconext.security.ACR; -import myconext.security.GuestIdpAuthenticationRequestFilter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.saml.provider.SamlServerConfiguration; -import org.springframework.security.saml.provider.config.SamlConfigurationRepository; -import org.springframework.security.saml.provider.identity.config.SamlIdentityProviderServerBeanConfiguration; - -import javax.servlet.Filter; - -@Configuration -public class BeanConfig extends SamlIdentityProviderServerBeanConfiguration { - - private final String redirectUrl; - private final AuthenticationRequestRepository authenticationRequestRepository; - private final UserRepository userRepository; - private final UserLoginRepository userLoginRepository; - private final int rememberMeMaxAge; - private final int nudgeAppDays; - private final int rememberMeQuestionAskedDays; - private final long expiryNonValidatedDurationDays; - private final long ssoMFADurationSeconds; - private final String mobileAppROEntityId; - private final boolean secureCookie; - private final String magicLinkUrl; - private final MailBox mailBox; - private final ServiceProviderResolver serviceProviderResolver; - private final GeoLocation geoLocation; - private final boolean featureDefaultRememberMe; - - public BeanConfig(@Value("${saml_metadata_base_path}") String samlMetadataBasePath, - @Value("${idp_redirect_url}") String redirectUrl, - @Value("${remember_me_max_age_seconds}") int rememberMeMaxAge, - @Value("${nudge_eduid_app_days}") int nudgeAppDays, - @Value("${remember_me_question_asked_days}") int rememberMeQuestionAskedDays, - @Value("${secure_cookie}") boolean secureCookie, - @Value("${email.magic-link-url}") String magicLinkUrl, - @Value("${account_linking_context_class_ref.linked_institution}") String linkedInstitution, - @Value("${account_linking_context_class_ref.validate_names}") String validateNames, - @Value("${account_linking_context_class_ref.affiliation_student}") String affiliationStudent, - @Value("${account_linking_context_class_ref.profile_mfa}") String profileMfa, - @Value("${linked_accounts.expiry-duration-days-non-validated}") long expiryNonValidatedDurationDays, - @Value("${sso_mfa_duration_seconds}") long ssoMFADurationSeconds, - @Value("${mobile_app_rp_entity_id}") String mobileAppROEntityId, - @Value("${feature.default_remember_me}") boolean featureDefaultRememberMe, - AuthenticationRequestRepository authenticationRequestRepository, - UserRepository userRepository, - UserLoginRepository userLoginRepository, - GeoLocation geoLocation, - MailBox mailBox, - ServiceProviderResolver serviceProviderResolver) { - this.immutableSamlConfigurationRepository = new ImmutableSamlConfigurationRepository(samlMetadataBasePath); - this.redirectUrl = redirectUrl; - this.rememberMeMaxAge = rememberMeMaxAge; - this.nudgeAppDays = nudgeAppDays; - this.rememberMeQuestionAskedDays = rememberMeQuestionAskedDays; - this.secureCookie = secureCookie; - this.expiryNonValidatedDurationDays = expiryNonValidatedDurationDays; - this.ssoMFADurationSeconds = ssoMFADurationSeconds; - this.mobileAppROEntityId = mobileAppROEntityId; - this.featureDefaultRememberMe= featureDefaultRememberMe; - this.authenticationRequestRepository = authenticationRequestRepository; - this.userRepository = userRepository; - this.userLoginRepository = userLoginRepository; - this.geoLocation = geoLocation; - this.magicLinkUrl = magicLinkUrl; - this.mailBox = mailBox; - this.serviceProviderResolver = serviceProviderResolver; - - ACR.initialize(linkedInstitution, validateNames, affiliationStudent, profileMfa); - } - - private final ImmutableSamlConfigurationRepository immutableSamlConfigurationRepository; - - @Override - protected SamlServerConfiguration getDefaultHostSamlServerConfiguration() { - return new SamlServerConfiguration(); - } - - @Override - public Filter idpAuthnRequestFilter() { - return new GuestIdpAuthenticationRequestFilter( - getSamlProvisioning(), - samlAssertionStore(), - redirectUrl, - serviceProviderResolver, - authenticationRequestRepository, - userRepository, - userLoginRepository, - geoLocation, - rememberMeMaxAge, - nudgeAppDays, - rememberMeQuestionAskedDays, - secureCookie, - magicLinkUrl, - mailBox, - expiryNonValidatedDurationDays, - ssoMFADurationSeconds, - mobileAppROEntityId, - featureDefaultRememberMe); - } - - public Filter samlConfigurationFilter(SamlServerConfiguration serverConfig) { - this.immutableSamlConfigurationRepository.setConfiguration(serverConfig); - return new NoopFilter(); - } - - @Override - public SamlConfigurationRepository samlConfigurationRepository() { - return immutableSamlConfigurationRepository; - } - -} diff --git a/myconext-server/src/main/java/myconext/config/NoopFilter.java b/myconext-server/src/main/java/myconext/config/NoopFilter.java deleted file mode 100644 index 6aaf5ae5..00000000 --- a/myconext-server/src/main/java/myconext/config/NoopFilter.java +++ /dev/null @@ -1,12 +0,0 @@ -package myconext.config; - -import javax.servlet.*; -import java.io.IOException; - -public class NoopFilter extends GenericFilter { - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - chain.doFilter(request, response); - } -} diff --git a/myconext-server/src/main/java/myconext/geo/MaxMindGeoLocation.java b/myconext-server/src/main/java/myconext/geo/MaxMindGeoLocation.java index 5b802766..3131eb9d 100644 --- a/myconext-server/src/main/java/myconext/geo/MaxMindGeoLocation.java +++ b/myconext-server/src/main/java/myconext/geo/MaxMindGeoLocation.java @@ -9,7 +9,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtil; +import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Value; @@ -19,6 +19,7 @@ import java.io.*; import java.net.InetAddress; import java.net.URL; +import java.nio.charset.Charset; import java.nio.file.Files; import java.text.SimpleDateFormat; import java.util.*; @@ -116,7 +117,7 @@ public void refresh() { FileOutputStream fileOutputStream = new FileOutputStream(file); InputStream inputStream = new URL(String.format(urlTemplate, licenseKey)).openStream(); - IOUtil.copy(inputStream, fileOutputStream); + IOUtils.copy(inputStream, fileOutputStream); TarArchiveInputStream fin = new TarArchiveInputStream(new GzipCompressorInputStream(new FileInputStream(file))); ArchiveEntry entry; @@ -125,7 +126,7 @@ public void refresh() { if (entry.getName().endsWith("mmdb")) { binaryData = new File(String.format("%s/%s/%s", this.downloadDirectory, name, GEO_LITE_2_CITY_MMDB)); try (OutputStream o = Files.newOutputStream(binaryData.toPath())) { - IOUtil.copy(fin, o); + IOUtils.copy(fin, o); } break; } diff --git a/myconext-server/src/main/java/myconext/manage/ResourceServiceProviderResolver.java b/myconext-server/src/main/java/myconext/manage/ResourceServiceProviderResolver.java index 9913ce8a..7da03493 100644 --- a/myconext-server/src/main/java/myconext/manage/ResourceServiceProviderResolver.java +++ b/myconext-server/src/main/java/myconext/manage/ResourceServiceProviderResolver.java @@ -4,9 +4,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import myconext.model.ServiceProvider; -import org.apache.commons.io.IOUtil; +import org.apache.commons.io.IOUtils; import org.springframework.core.io.Resource; +import java.nio.charset.Charset; import java.util.List; import java.util.Map; import java.util.Optional; @@ -17,8 +18,9 @@ public class ResourceServiceProviderResolver implements ServiceProviderResolver @SneakyThrows public ResourceServiceProviderResolver(Resource resource, ObjectMapper objectMapper) { - spNames = objectMapper.readValue(IOUtil.toString(resource.getInputStream()), new TypeReference<>() { - }); + spNames = objectMapper.readValue(IOUtils.toString(resource.getInputStream(), Charset.defaultCharset()), + new TypeReference<>() { + }); } @Override diff --git a/myconext-server/src/main/java/myconext/saml/ImmutableSamlConfigurationRepository.java b/myconext-server/src/main/java/myconext/saml/ImmutableSamlConfigurationRepository.java deleted file mode 100644 index 2819cd36..00000000 --- a/myconext-server/src/main/java/myconext/saml/ImmutableSamlConfigurationRepository.java +++ /dev/null @@ -1,33 +0,0 @@ -package myconext.saml; - -import org.springframework.security.saml.provider.SamlServerConfiguration; -import org.springframework.security.saml.provider.config.LocalProviderConfiguration; -import org.springframework.security.saml.provider.config.SamlConfigurationRepository; - -import static java.util.Arrays.asList; -import static org.springframework.util.StringUtils.hasText; - -public class ImmutableSamlConfigurationRepository implements SamlConfigurationRepository { - - private SamlServerConfiguration configuration; - private String basePath; - - public ImmutableSamlConfigurationRepository(String basePath) { - this.basePath = basePath; - } - - @Override - public SamlServerConfiguration getServerConfiguration() { - return configuration; - } - - public void setConfiguration(SamlServerConfiguration configuration) { - this.configuration = configuration; - for (LocalProviderConfiguration config : asList(configuration.getIdentityProvider(), configuration.getServiceProvider())) { - if (config != null && !hasText(config.getBasePath())) { - config.setBasePath(basePath); - } - } - } -} - diff --git a/myconext-server/src/main/java/myconext/security/GuestIdentityProviderDsl.java b/myconext-server/src/main/java/myconext/security/GuestIdentityProviderDsl.java deleted file mode 100644 index 5a96245e..00000000 --- a/myconext-server/src/main/java/myconext/security/GuestIdentityProviderDsl.java +++ /dev/null @@ -1,46 +0,0 @@ -package myconext.security; - -import myconext.config.BeanConfig; -import org.springframework.context.ApplicationContext; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.saml.provider.SamlServerConfiguration; -import org.springframework.security.saml.provider.identity.config.SamlIdentityProviderSecurityDsl; -import org.springframework.security.web.context.SecurityContextPersistenceFilter; - -import javax.servlet.Filter; - -public class GuestIdentityProviderDsl extends SamlIdentityProviderSecurityDsl { - - private final BeanConfig beanConfig; - - GuestIdentityProviderDsl(BeanConfig beanConfig) { - this.beanConfig = beanConfig; - } - - @Override - public void configure(HttpSecurity http) throws Exception { - - super.configure(http); - - ApplicationContext context = http.getSharedObject(ApplicationContext.class); - - SamlServerConfiguration serverConfig = context.getBean("idpSamlServerConfiguration", SamlServerConfiguration.class); - - Filter samlConfigurationFilter = beanConfig.samlConfigurationFilter(serverConfig); - Filter metadataFilter = beanConfig.idpMetadataFilter(); - Filter idpAuthnRequestFilter = beanConfig.idpAuthnRequestFilter(); - http - .addFilterAfter( - samlConfigurationFilter, - SecurityContextPersistenceFilter.class - ) - .addFilterAfter( - metadataFilter, - samlConfigurationFilter.getClass() - ) - .addFilterAfter( - idpAuthnRequestFilter, - metadataFilter.getClass() - ); - } -} diff --git a/myconext-server/src/main/java/myconext/security/GuestIdpAuthenticationRequestFilter.java b/myconext-server/src/main/java/myconext/security/GuestIdpAuthenticationRequestFilter.java index b85dc8d6..047994da 100644 --- a/myconext-server/src/main/java/myconext/security/GuestIdpAuthenticationRequestFilter.java +++ b/myconext-server/src/main/java/myconext/security/GuestIdpAuthenticationRequestFilter.java @@ -1,8 +1,10 @@ package myconext.security; +import lombok.NoArgsConstructor; import myconext.exceptions.UserNotFoundException; import myconext.geo.GeoLocation; import myconext.mail.MailBox; +import myconext.manage.MockServiceProviderResolver; import myconext.manage.ServiceProviderHolder; import myconext.manage.ServiceProviderResolver; import myconext.model.*; @@ -11,28 +13,22 @@ import myconext.repository.UserRepository; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Value; +import org.opensaml.core.xml.schema.XSURI; +import org.opensaml.saml.saml2.core.*; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.saml.SamlMessageStore; -import org.springframework.security.saml.SamlRequestMatcher; -import org.springframework.security.saml.provider.identity.IdentityProviderService; -import org.springframework.security.saml.provider.identity.IdpAuthenticationRequestFilter; -import org.springframework.security.saml.provider.provisioning.SamlProviderProvisioning; -import org.springframework.security.saml.saml2.attribute.Attribute; -import org.springframework.security.saml.saml2.attribute.AttributeNameFormat; -import org.springframework.security.saml.saml2.authentication.*; -import org.springframework.security.saml.saml2.metadata.Binding; -import org.springframework.security.saml.saml2.metadata.Endpoint; -import org.springframework.security.saml.saml2.metadata.NameId; -import org.springframework.security.saml.saml2.metadata.ServiceProviderMetadata; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import org.springframework.web.util.HtmlUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import saml.DefaultSAMLIdPService; +import saml.model.SAMLAttribute; +import saml.model.SAMLConfiguration; +import saml.model.SAMLStatus; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -57,10 +53,10 @@ import static myconext.log.MDCContext.logLoginWithContext; import static myconext.log.MDCContext.logWithContext; import static myconext.security.CookieResolver.cookieByName; -import static org.springframework.util.StringUtils.hasText; @SuppressWarnings("unchecked") -public class GuestIdpAuthenticationRequestFilter extends IdpAuthenticationRequestFilter implements ServiceProviderHolder { +@NoArgsConstructor(force = true) +public class GuestIdpAuthenticationRequestFilter extends OncePerRequestFilter implements ServiceProviderHolder { public static final String GUEST_IDP_REMEMBER_ME_COOKIE_NAME = "guest-idp-remember-me"; public static final String TRACKING_DEVICE_COOKIE_NAME = "TRACKING_DEVICE"; @@ -72,12 +68,12 @@ public class GuestIdpAuthenticationRequestFilter extends IdpAuthenticationReques private static final Log LOG = LogFactory.getLog(GuestIdpAuthenticationRequestFilter.class); public static final String ROLE_MFA = "ROLE_MFA"; - private final SamlRequestMatcher ssoSamlRequestMatcher; - private final SamlRequestMatcher magicSamlRequestMatcher; - private final SamlRequestMatcher continueAfterloginSamlRequestMatcher; + private final AntPathRequestMatcher ssoSamlRequestMatcher; + private final AntPathRequestMatcher magicSamlRequestMatcher; + private final AntPathRequestMatcher continueAfterloginSamlRequestMatcher; private final String redirectUrl; private final AuthenticationRequestRepository authenticationRequestRepository; - private final UserRepository userRepository; + private UserRepository userRepository; private final UserLoginRepository userLoginRepository; private final List accountLinkingContextClassReferences; private final GeoLocation geoLocation; @@ -86,7 +82,7 @@ public class GuestIdpAuthenticationRequestFilter extends IdpAuthenticationReques private final boolean secureCookie; private final String magicLinkUrl; private final MailBox mailBox; - private final ServiceProviderResolver serviceProviderResolver; + private ServiceProviderResolver serviceProviderResolver; private final ExecutorService executor; private final int nudgeAppDays; private final int rememberMeQuestionAskedDays; @@ -94,10 +90,9 @@ public class GuestIdpAuthenticationRequestFilter extends IdpAuthenticationReques private final long ssoMFADurationSeconds; private final String mobileAppROEntityId; private final boolean featureDefaultRememberMe; + private final DefaultSAMLIdPService samlIdpService; - public GuestIdpAuthenticationRequestFilter(SamlProviderProvisioning provisioning, - SamlMessageStore assertionStore, - String redirectUrl, + public GuestIdpAuthenticationRequestFilter(String redirectUrl, ServiceProviderResolver serviceProviderResolver, AuthenticationRequestRepository authenticationRequestRepository, UserRepository userRepository, @@ -112,11 +107,11 @@ public GuestIdpAuthenticationRequestFilter(SamlProviderProvisioning authenticationContextClassReferenceValues = getAuthenticationContextClassReferenceValues(authenticationRequest); + List authenticationContextClassReferenceValues = getAuthenticationContextClassReferenceValues(authnRequest); boolean accountLinkingRequired = this.accountLinkingContextClassReferences.stream().anyMatch(authenticationContextClassReferenceValues::contains); boolean mfaProfileRequired = authenticationContextClassReferenceValues.contains(ACR.PROFILE_MFA); SamlAuthenticationRequest samlAuthenticationRequest = new SamlAuthenticationRequest( - authenticationRequest.getId(), + authnRequest.getID(), issuer, - authenticationRequest.getAssertionConsumerService().getLocation(), + authnRequest.getAssertionConsumerServiceURL(), relayState, StringUtils.hasText(requesterEntityId) ? requesterEntityId : "", accountLinkingRequired, @@ -200,7 +193,7 @@ private void sso(HttpServletRequest request, HttpServletResponse response) throw samlAuthenticationRequest.setServiceName(serviceName); samlAuthenticationRequest = authenticationRequestRepository.save(samlAuthenticationRequest); - if (previousAuthenticatedUser != null && !authenticationRequest.isForceAuth()) { + if (previousAuthenticatedUser != null && !authnRequest.isForceAuthn()) { boolean applySsoMfa = isApplySsoMfa(); if ((!applySsoMfa && mfaProfileRequired) || (accountLinkingRequired && !isUserVerifiedByInstitution(previousAuthenticatedUser, authenticationContextClassReferenceValues))) { @@ -225,9 +218,7 @@ private void sso(HttpServletRequest request, HttpServletResponse response) throw + "?explanation=" + explanation + mfa; response.sendRedirect(location); } else { - ServiceProviderMetadata serviceProviderMetadata = provider.getRemoteProvider(samlAuthenticationRequest.getIssuer()); - sendAssertion(request, response, samlAuthenticationRequest, previousAuthenticatedUser, - provider, serviceProviderMetadata, authenticationRequest); + sendAssertion(request, response, samlAuthenticationRequest, previousAuthenticatedUser); } } else { addBrowserIdentificationCookie(response); @@ -237,7 +228,7 @@ private void sso(HttpServletRequest request, HttpServletResponse response) throw String stepUp = accountLinkingRequired ? "&stepup=true" : ""; String mfa = mfaProfileRequired ? "&mfa=true" : ""; - String preferMagicLink = this.nudgeMagicLink(authenticationRequest) ? "&magicLink=true" : ""; + String preferMagicLink = this.nudgeMagicLink(authnRequest) ? "&magicLink=true" : ""; String separator = (accountLinkingRequired || mfaProfileRequired || StringUtils.hasText(preferMagicLink)) ? "?n=1" : ""; String path = optionalCookie.map(c -> "/request/").orElse("/login/"); String location = this.redirectUrl + path + samlAuthenticationRequest.getId() + @@ -294,13 +285,17 @@ public static boolean hasRequiredStudentAffiliation(List affiliations) { .anyMatch(affiliation -> affiliation.startsWith("student@")); } - private List getAuthenticationContextClassReferenceValues(AuthenticationRequest authenticationRequest) { - List authenticationContextClassReferences = authenticationRequest.getAuthenticationContextClassReferences(); - if (authenticationContextClassReferences == null) { + private List getAuthenticationContextClassReferenceValues(AuthnRequest authenticationRequest) { + RequestedAuthnContext requestedAuthnContext = authenticationRequest.getRequestedAuthnContext(); + if (requestedAuthnContext == null) { return Collections.emptyList(); } - return authenticationContextClassReferences.stream() - .map(AuthenticationContextClassReference::getValue) + List authnContextClassRefs = requestedAuthnContext.getAuthnContextClassRefs(); + if (authnContextClassRefs == null) { + return Collections.emptyList(); + } + return authnContextClassRefs.stream() + .map(XSURI::getURI) .collect(toList()); } @@ -330,19 +325,19 @@ private boolean isDeflated(HttpServletRequest request) { return HttpMethod.GET.name().equalsIgnoreCase(request.getMethod()); } - private String requesterId(AuthenticationRequest authenticationRequest) { + private String requesterId(AuthnRequest authenticationRequest) { Issuer issuer = authenticationRequest.getIssuer(); String issuerValue = issuer != null ? issuer.getValue() : ""; Scoping scoping = authenticationRequest.getScoping(); - List requesterIds = scoping != null ? scoping.getRequesterIds() : null; - return CollectionUtils.isEmpty(requesterIds) ? issuerValue : requesterIds.get(0); + List requesterIDS = scoping != null ? scoping.getRequesterIDs() : null; + return CollectionUtils.isEmpty(requesterIDS) ? issuerValue : requesterIDS.get(0).getURI(); } - private boolean nudgeMagicLink(AuthenticationRequest authenticationRequest) { + private boolean nudgeMagicLink(AuthnRequest authenticationRequest) { Scoping scoping = authenticationRequest.getScoping(); - List requesterIds = scoping != null ? scoping.getRequesterIds() : null; - return !CollectionUtils.isEmpty(requesterIds) && requesterIds.stream() - .anyMatch(this.mobileAppROEntityId::equalsIgnoreCase); + List requesterIDS = scoping != null ? scoping.getRequesterIDs() : null; + return !CollectionUtils.isEmpty(requesterIDS) && requesterIDS.stream() + .anyMatch(requesterID -> this.mobileAppROEntityId.equalsIgnoreCase(requesterID.getURI())); } private void magic(HttpServletRequest request, HttpServletResponse response) throws IOException { @@ -526,7 +521,10 @@ private void continueAfterLogin(HttpServletRequest request, HttpServletResponse doSendAssertion(request, response, samlAuthenticationRequest, user); } - private void doSendAssertion(HttpServletRequest request, HttpServletResponse response, SamlAuthenticationRequest samlAuthenticationRequest, User user) { + private void doSendAssertion(HttpServletRequest request, + HttpServletResponse response, + SamlAuthenticationRequest samlAuthenticationRequest, + User user) { //ensure the magic link can't be used twice samlAuthenticationRequest.setHash(null); @@ -534,13 +532,6 @@ private void doSendAssertion(HttpServletRequest request, HttpServletResponse res authenticationRequestRepository.save(samlAuthenticationRequest); - IdentityProviderService provider = getProvisioning().getHostedProvider(); - ServiceProviderMetadata serviceProviderMetadata = provider.getRemoteProvider(samlAuthenticationRequest.getIssuer()); - - AuthenticationRequest authenticationRequest = new AuthenticationRequest(); - authenticationRequest.setId(samlAuthenticationRequest.getRequestId()); - authenticationRequest.setAssertionConsumerService(new Endpoint().setLocation(samlAuthenticationRequest.getConsumerAssertionServiceURL())); - //We support SSO for MFA, we must mark the authentication with a timestamp and an extra role boolean mfaProfileRequired = samlAuthenticationRequest.isMfaProfileRequired(); Collection authorities = user.getAuthorities(); @@ -563,8 +554,7 @@ private void doSendAssertion(HttpServletRequest request, HttpServletResponse res logLoginWithContext(user, loginMethod, true, LOG, "Successfully logged in with " + loginMethod); - sendAssertion(request, response, samlAuthenticationRequest, user, provider, - serviceProviderMetadata, authenticationRequest); + sendAssertion(request, response, samlAuthenticationRequest, user); } private void finishStepUp(SamlAuthenticationRequest samlAuthenticationRequest) { @@ -628,19 +618,20 @@ private void addTrackingCookie(HttpServletRequest request, HttpServletResponse r } } - private void sendAssertion(HttpServletRequest request, HttpServletResponse response, SamlAuthenticationRequest samlAuthenticationRequest, - User user, IdentityProviderService provider, ServiceProviderMetadata serviceProviderMetadata, - AuthenticationRequest authenticationRequest) { + private void sendAssertion(HttpServletRequest request, + HttpServletResponse response, + SamlAuthenticationRequest samlAuthenticationRequest, + User user) { String relayState = samlAuthenticationRequest.getRelayState(); String requesterEntityId = samlAuthenticationRequest.getRequesterEntityId(); - Assertion assertion = provider.assertion( - serviceProviderMetadata, authenticationRequest, user.getUid(), NameId.PERSISTENT); List authenticationContextClassReferences = samlAuthenticationRequest.getAuthenticationContextClassReferences(); - attributes(user, requesterEntityId).forEach(assertion::addAttribute); + List attributes = attributes(user, requesterEntityId); - Response samlResponse = provider.response(authenticationRequest, assertion, serviceProviderMetadata); boolean applySsoMfa = this.isApplySsoMfa(); + SAMLStatus samlStatus = SAMLStatus.SUCCESS; + String optionalMessage = null; + String authnContextClassRefValue = DefaultSAMLIdPService.authnContextClassRefPassword; if (samlAuthenticationRequest.isAccountLinkingRequired()) { boolean hasStudentAffiliation = hasRequiredStudentAffiliation(user.allEduPersonAffiliations()); @@ -650,56 +641,31 @@ private void sendAssertion(HttpServletRequest request, HttpServletResponse respo boolean missingValidName = authenticationContextClassReferences.contains(ACR.VALIDATE_NAMES) && !hasValidatedName(user); if (missingStudentAffiliation || missingValidName) { - String msg; if (missingValidName) { - msg = "The requesting service has indicated that the authenticated user is required to have a first_name and last_name." + + optionalMessage = "The requesting service has indicated that the authenticated user is required to have a first_name and last_name." + " Your institution has not provided those attributes."; } else { - msg = "The requesting service has indicated that the authenticated user is required to have an affiliation Student." + + optionalMessage = "The requesting service has indicated that the authenticated user is required to have an affiliation Student." + " Your institution has not provided this affiliation."; } - samlResponse.setStatus(new Status() - .setCode(StatusCode.NO_AUTH_CONTEXT) - .setMessage(msg) - .setDetail(msg)); + samlStatus = SAMLStatus.NO_AUTHN_CONTEXT; } else { - samlResponse.getAssertions().get(0).getAuthenticationStatements().get(0) - .getAuthenticationContext() - .setClassReference(AuthenticationContextClassReference - .fromUrn(ACR.selectACR(authenticationContextClassReferences, hasStudentAffiliation))); + authnContextClassRefValue = ACR.selectACR(authenticationContextClassReferences, hasStudentAffiliation); } } else if (samlAuthenticationRequest.isMfaProfileRequired()) { if (samlAuthenticationRequest.isTiqrFlow() || applySsoMfa) { - samlResponse.getAssertions().get(0).getAuthenticationStatements().get(0) - .getAuthenticationContext() - .setClassReference(AuthenticationContextClassReference - .fromUrn(ACR.selectACR(authenticationContextClassReferences, false))); + authnContextClassRefValue = ACR.selectACR(authenticationContextClassReferences, false); } else { - String msg = "The requesting service has indicated that a login with the eduID app is required to login."; - samlResponse.setStatus(new Status() - .setCode(StatusCode.NO_AUTH_CONTEXT) - .setMessage(msg) - .setDetail(msg)); + optionalMessage = "The requesting service has indicated that a login with the eduID app is required to login."; + samlStatus = SAMLStatus.NO_AUTHN_CONTEXT; } } else if (!applySsoMfa && !CollectionUtils.isEmpty(authenticationContextClassReferences)) { - String msg = String.format("The specified authentication context requirements '%s' cannot be met by the responder.", + optionalMessage = String.format("The specified authentication context requirements '%s' cannot be met by the responder.", String.join(", ", authenticationContextClassReferences)); - samlResponse.setStatus(new Status() - .setCode(StatusCode.NO_AUTH_CONTEXT) - .setMessage(msg) - .setDetail(msg)); + samlStatus = SAMLStatus.NO_AUTHN_CONTEXT; } - Endpoint acsUrl = provider.getPreferredEndpoint( - serviceProviderMetadata.getServiceProvider().getAssertionConsumerService(), - Binding.POST, - -1 - ); - String encoded = provider.toEncodedXml(samlResponse, false); - Map model = new HashMap<>(); - model.put("action", acsUrl.getLocation()); - model.put("SAMLResponse", encoded); - if (hasText(relayState)) { - model.put("RelayState", HtmlUtils.htmlEscape(relayState)); + if (!samlStatus.equals(SAMLStatus.SUCCESS)) { + authnContextClassRefValue = DefaultSAMLIdPService.authnContextClassRefUnspecified; } Optional optionalCookie = cookieByName(request, BROWSER_SESSION_COOKIE_NAME); optionalCookie.ifPresent(cookie -> { @@ -708,14 +674,32 @@ private void sendAssertion(HttpServletRequest request, HttpServletResponse respo }); //Tracking cookie for user new device discovery this.addTrackingCookie(request, response, user); - processHtml(request, response, getPostBindingTemplate(), model); + this.samlIdpService.sendResponse( + samlAuthenticationRequest.getIssuer(), + samlAuthenticationRequest.getRequestId(), + user.getUid(), + samlStatus, + relayState, + optionalMessage, + authnContextClassRefValue, + attributes, + response + ); } public ServiceProviderResolver getServiceProviderResolver() { return serviceProviderResolver; } - protected List attributes(User user, String requesterEntityId) { + public void setServiceProviderResolver(ServiceProviderResolver serviceProviderResolver) { + this.serviceProviderResolver = serviceProviderResolver; + } + + public void setUserRepository(UserRepository userRepository) { + this.userRepository = userRepository; + } + + protected List attributes(User user, String requesterEntityId) { List linkedAccounts = safeSortedAffiliations(user); String givenName = user.getGivenName(); String familyName = user.getFamilyName(); @@ -724,7 +708,7 @@ protected List attributes(User user, String requesterEntityId) { String displayName = String.format("%s %s", chosenName, familyName); String commonName = String.format("%s %s", givenName, familyName); String eppn = user.getEduPersonPrincipalName(); - List attributes = new ArrayList(Arrays.asList( + List attributes = new ArrayList(Arrays.asList( attribute("urn:mace:dir:attribute-def:cn", displayName), attribute("urn:mace:dir:attribute-def:displayName", displayName), attribute("urn:mace:dir:attribute-def:commonName", commonName), @@ -742,7 +726,9 @@ protected List attributes(User user, String requesterEntityId) { attributes.add(attribute("urn:mace:eduid.nl:1.1", eduIDValue)); user.getAttributes() - .forEach((key, value) -> attributes.add(attribute(key, value.toArray(new String[]{})))); + .forEach((key, values) -> + values.forEach(value -> attributes.add(attribute(key, value)))); + List scopedAffiliations = linkedAccounts.stream() .map(linkedAccount -> linkedAccount.getEduPersonAffiliations().stream() @@ -751,13 +737,11 @@ protected List attributes(User user, String requesterEntityId) { .collect(toList())) .flatMap(Collection::stream).distinct().collect(Collectors.toList()); scopedAffiliations.add("affiliate@eduid.nl"); - attributes.add(attribute("urn:mace:dir:attribute-def:eduPersonScopedAffiliation", - scopedAffiliations.toArray(new String[]{}))); + scopedAffiliations.forEach(aff -> attributes.add(attribute("urn:mace:dir:attribute-def:eduPersonScopedAffiliation", aff))); List affiliations = scopedAffiliations.stream().map(affiliation -> affiliation.substring(0, affiliation.indexOf("@"))) .distinct().collect(toList()); - attributes.add(attribute("urn:mace:dir:attribute-def:eduPersonAffiliation", - affiliations.toArray(new String[]{}))); + scopedAffiliations.forEach(aff -> attributes.add(attribute("urn:mace:dir:attribute-def:eduPersonAffiliation", aff))); return attributes; } @@ -774,8 +758,8 @@ private List safeSortedAffiliations(User user) { return user.linkedAccountsSorted(); } - private Attribute attribute(String name, String... value) { - return new Attribute().setName(name).setNameFormat(AttributeNameFormat.URI).addValues((Object[]) value); + private SAMLAttribute attribute(String name, String value) { + return new SAMLAttribute(name, value); } } diff --git a/myconext-server/src/main/java/myconext/security/SecurityConfiguration.java b/myconext-server/src/main/java/myconext/security/SecurityConfiguration.java index 32be2c76..536d0baf 100644 --- a/myconext-server/src/main/java/myconext/security/SecurityConfiguration.java +++ b/myconext-server/src/main/java/myconext/security/SecurityConfiguration.java @@ -1,14 +1,17 @@ package myconext.security; -import myconext.config.BeanConfig; +import lombok.SneakyThrows; import myconext.crypto.KeyGenerator; +import myconext.geo.GeoLocation; +import myconext.mail.MailBox; import myconext.manage.ServiceProviderResolver; -import myconext.model.ServiceProvider; +import myconext.repository.AuthenticationRequestRepository; +import myconext.repository.UserLoginRepository; import myconext.repository.UserRepository; import myconext.shibboleth.ShibbolethPreAuthenticatedProcessingFilter; import myconext.shibboleth.ShibbolethUserDetailService; import myconext.shibboleth.mock.MockShibbolethFilter; -import org.apache.commons.io.IOUtil; +import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -25,24 +28,19 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.saml.key.SimpleKey; -import org.springframework.security.saml.provider.config.RotatingKeys; -import org.springframework.security.saml.provider.identity.config.ExternalServiceProviderConfiguration; -import org.springframework.security.saml.provider.identity.config.SamlIdentityProviderSecurityConfiguration; -import org.springframework.security.saml.provider.identity.config.SamlIdentityProviderSecurityDsl; -import org.springframework.security.saml.saml2.metadata.NameId; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; +import saml.model.SAMLConfiguration; +import saml.model.SAMLIdentityProvider; +import saml.model.SAMLServiceProvider; import java.io.IOException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; -import static org.springframework.security.saml.saml2.signature.AlgorithmMethod.RSA_SHA512; -import static org.springframework.security.saml.saml2.signature.DigestMethod.SHA512; @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) @@ -52,30 +50,71 @@ public class SecurityConfiguration { @Configuration @Order(1) - public static class SamlSecurity extends SamlIdentityProviderSecurityConfiguration { - private final Resource privateKeyPath; - private final Resource certificatePath; - private final List serviceProviders = new ArrayList<>(); - private final String idpEntityId; - private final BeanConfig beanConfig; - - public SamlSecurity(BeanConfig beanConfig, - @Value("${private_key_path}") Resource privateKeyPath, + public static class SamlSecurity extends WebSecurityConfigurerAdapter { + + private final GuestIdpAuthenticationRequestFilter guestIdpAuthenticationRequestFilter; + + public SamlSecurity(@Value("${private_key_path}") Resource privateKeyPath, @Value("${certificate_path}") Resource certificatePath, @Value("${idp_entity_id}") String idpEntityId, @Value("${sp_entity_id}") String spEntityId, - @Value("${sp_entity_metadata_url}") String spMetaDataUrl) { - super("/saml/guest-idp/", beanConfig); - this.beanConfig = beanConfig; - this.privateKeyPath = privateKeyPath; - this.certificatePath = certificatePath; - this.idpEntityId = idpEntityId; + @Value("${sp_entity_metadata_url}") String spMetaDataUrl, + @Value("${saml_metadata_base_path}") String samlMetadataBasePath, + @Value("${idp_redirect_url}") String redirectUrl, + @Value("${remember_me_max_age_seconds}") int rememberMeMaxAge, + @Value("${nudge_eduid_app_days}") int nudgeAppDays, + @Value("${remember_me_question_asked_days}") int rememberMeQuestionAskedDays, + @Value("${secure_cookie}") boolean secureCookie, + @Value("${email.magic-link-url}") String magicLinkUrl, + @Value("${account_linking_context_class_ref.linked_institution}") String linkedInstitution, + @Value("${account_linking_context_class_ref.validate_names}") String validateNames, + @Value("${account_linking_context_class_ref.affiliation_student}") String affiliationStudent, + @Value("${account_linking_context_class_ref.profile_mfa}") String profileMfa, + @Value("${linked_accounts.expiry-duration-days-non-validated}") long expiryNonValidatedDurationDays, + @Value("${sso_mfa_duration_seconds}") long ssoMFADurationSeconds, + @Value("${mobile_app_rp_entity_id}") String mobileAppROEntityId, + @Value("${feature.default_remember_me}") boolean featureDefaultRememberMe, + @Value("${feature.requires_signed_authn_request}") boolean requiresSignedAuthnRequest, + AuthenticationRequestRepository authenticationRequestRepository, + UserRepository userRepository, + UserLoginRepository userLoginRepository, + GeoLocation geoLocation, + MailBox mailBox, + ServiceProviderResolver serviceProviderResolver) { + String[] keys = this.getKeys(certificatePath, privateKeyPath); + final List serviceProviders = new ArrayList<>(); List spEntityIdentifiers = commaSeparatedToList(spEntityId); List spMetaDataUrls = commaSeparatedToList(spMetaDataUrl); for (int i = 0; i < spEntityIdentifiers.size(); i++) { - serviceProviders.add(new ServiceProvider(spEntityIdentifiers.get(i), spMetaDataUrls.get(i))); + serviceProviders.add(new SAMLServiceProvider(spEntityIdentifiers.get(i), spMetaDataUrls.get(i))); } + + SAMLConfiguration configuration = new SAMLConfiguration( + new SAMLIdentityProvider(keys[0], keys[1], idpEntityId), + serviceProviders, + false + ); + this.guestIdpAuthenticationRequestFilter = new GuestIdpAuthenticationRequestFilter( + redirectUrl, + serviceProviderResolver, + authenticationRequestRepository, + userRepository, + userLoginRepository, + geoLocation, + rememberMeMaxAge, + nudgeAppDays, + rememberMeQuestionAskedDays, + secureCookie, + magicLinkUrl, + mailBox, + expiryNonValidatedDurationDays, + ssoMFADurationSeconds, + mobileAppROEntityId, + featureDefaultRememberMe, + configuration + ); + } private List commaSeparatedToList(String spEntityId) { @@ -84,55 +123,37 @@ private List commaSeparatedToList(String spEntityId) { @Override protected void configure(HttpSecurity http) throws Exception { - super.configure(http); - - String prefix = getPrefix(); - SamlIdentityProviderSecurityDsl configurer = new GuestIdentityProviderDsl(beanConfig); - - SamlIdentityProviderSecurityDsl samlIdentityProviderSecurityDsl = http.apply(configurer) - .prefix(prefix) - .useStandardFilters(false) - .entityId(idpEntityId) - .alias("guest-idp") - .singleLogout(false) - .signMetadata(true) - .signatureAlgorithms(RSA_SHA512, SHA512) - .nameIds(asList(NameId.PERSISTENT)) - .rotatingKeys(getKeys()); - serviceProviders.forEach(sp -> samlIdentityProviderSecurityDsl.serviceProvider( - new ExternalServiceProviderConfiguration() - .setAlias(sp.getEntityId()) - .setMetadata(sp.getMetaDataUrl()) - .setSkipSslValidation(false) - - )); + http + .requestMatchers() + .antMatchers("/saml/guest-idp/**") + .and() + .csrf() + .disable() + .addFilterBefore(this.guestIdpAuthenticationRequestFilter, + AbstractPreAuthenticatedProcessingFilter.class + ) + .authorizeRequests() + .antMatchers("/**").hasRole("GUEST"); } - private RotatingKeys getKeys() throws Exception { + @SneakyThrows + private String[] getKeys(Resource certificatePath, Resource privateKeyPath) { String privateKey; String certificate; - if (this.privateKeyPath.exists() && this.certificatePath.exists()) { - privateKey = read(this.privateKeyPath); - certificate = read(this.certificatePath); + if (privateKeyPath.exists() && certificatePath.exists()) { + privateKey = read(privateKeyPath); + certificate = read(certificatePath); } else { String[] keys = KeyGenerator.generateKeys(); privateKey = keys[0]; certificate = keys[1]; } - return new RotatingKeys() - .setActive( - new SimpleKey() - .setName("idp-signing-key") - .setPrivateKey(privateKey) - //to prevent null-pointer in SamlKeyStoreProvider - .setPassphrase("") - .setCertificate(certificate) - ); + return new String[]{certificate, privateKey}; } private String read(Resource resource) throws IOException { LOG.info("Reading resource: " + resource.getFilename()); - return IOUtil.toString(resource.getInputStream()); + return IOUtils.toString(resource.getInputStream(), Charset.defaultCharset()); } } diff --git a/myconext-server/src/main/java/myconext/session/SessionConfig.java b/myconext-server/src/main/java/myconext/session/SessionConfig.java index aee4d893..005e1062 100644 --- a/myconext-server/src/main/java/myconext/session/SessionConfig.java +++ b/myconext-server/src/main/java/myconext/session/SessionConfig.java @@ -14,7 +14,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; -import org.springframework.security.saml.saml2.authentication.Assertion; import org.springframework.session.data.mongo.config.annotation.web.http.EnableMongoHttpSession; import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer; import org.springframework.session.web.http.CookieSerializer; @@ -37,7 +36,6 @@ public ObjectMapper jsonMapper() { .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .registerModule(new Jdk8Module()) .registerModule(new JavaTimeModule()) - .addMixIn(Assertion.class, AssertionMixin.class) .addMixIn(HashSet.class, HashSetMixin.class) .addMixIn(User.class, UserMixin.class) .addMixIn(LinkedAccount.class, LinkedAccountMixin.class) @@ -57,9 +55,6 @@ CookieSerializer cookieSerializer(@Value("${secure_cookie}") boolean secureCooki return defaultCookieSerializer; } - private static class AssertionMixin { - } - private static class HashSetMixin { } diff --git a/myconext-server/src/main/java/myconext/sms/SMSServiceImpl.java b/myconext-server/src/main/java/myconext/sms/SMSServiceImpl.java index b7ec784a..b5be85a4 100644 --- a/myconext-server/src/main/java/myconext/sms/SMSServiceImpl.java +++ b/myconext-server/src/main/java/myconext/sms/SMSServiceImpl.java @@ -1,13 +1,14 @@ package myconext.sms; import lombok.SneakyThrows; -import org.apache.commons.io.IOUtil; +import org.apache.commons.io.IOUtils; import org.springframework.core.io.ClassPathResource; import org.springframework.http.*; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import java.net.URI; +import java.nio.charset.Charset; import java.util.List; import java.util.Locale; import java.util.Map; @@ -24,8 +25,8 @@ public class SMSServiceImpl implements SMSService { @SneakyThrows public SMSServiceImpl(String url, String bearer){ this.url = url; - this.templateNl = IOUtil.toString(new ClassPathResource("sms/template_nl.txt").getInputStream()); - this.templateEn = IOUtil.toString(new ClassPathResource("sms/template_en.txt").getInputStream()); + this.templateNl = IOUtils.toString(new ClassPathResource("sms/template_nl.txt").getInputStream(), Charset.defaultCharset()); + this.templateEn = IOUtils.toString(new ClassPathResource("sms/template_en.txt").getInputStream(), Charset.defaultCharset()); headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); headers.add(HttpHeaders.AUTHORIZATION, "Bearer "+ bearer); diff --git a/myconext-server/src/main/resources/application.yml b/myconext-server/src/main/resources/application.yml index 133841bd..cfa17e91 100644 --- a/myconext-server/src/main/resources/application.yml +++ b/myconext-server/src/main/resources/application.yml @@ -61,8 +61,8 @@ idp_redirect_url: http://localhost:3000 rp_id: localhost rp_origin: http://localhost:3000 sp_redirect_url: http://localhost:3001 -sp_entity_id: https://engine.test2.surfconext.nl/authentication/sp/metadata, https://engine.test.surfconext.nl/authentication/sp/metadata -sp_entity_metadata_url: https://engine.test2.surfconext.nl/authentication/sp/metadata, https://engine.test.surfconext.nl/authentication/sp/metadata +sp_entity_id: https://engine.test.surfconext.nl/authentication/sp/metadata +sp_entity_metadata_url: https://engine.test.surfconext.nl/authentication/sp/metadata guest_idp_entity_id: https://localhost.surf.id my_conext_url: https://my.test2.surfconext.nl domain: eduid.nl @@ -90,6 +90,8 @@ feature: create_eduid_institution_landing: True # Do we default remember the user for a longer period default_remember_me: False + # Does the SAMLIdpService expects authn requests to be signed + requires_signed_authn_request: True secure_cookie: false idp_entity_id: https://localhost.surf.id diff --git a/myconext-server/src/test/java/myconext/AbstractIntegrationTest.java b/myconext-server/src/test/java/myconext/AbstractIntegrationTest.java index 28a245da..b37fa692 100644 --- a/myconext-server/src/test/java/myconext/AbstractIntegrationTest.java +++ b/myconext-server/src/test/java/myconext/AbstractIntegrationTest.java @@ -15,7 +15,7 @@ import myconext.model.*; import myconext.repository.*; import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.IOUtil; +import org.apache.commons.io.IOUtils; import org.junit.Before; import org.junit.jupiter.api.BeforeEach; import org.junit.runner.RunWith; @@ -80,7 +80,8 @@ "eduid_api.oidcng_introspection_uri=http://localhost:8098/introspect", "cron.service-name-resolver-initial-delay-milliseconds=60000", "oidc.base-url=http://localhost:8098/", - "sso_mfa_duration_seconds=-1000" + "sso_mfa_duration_seconds=-1000", + "feature.requires_signed_authn_request=false" }) @ActiveProfiles({"test"}) @SuppressWarnings("unchecked") @@ -207,7 +208,7 @@ protected Response samlAuthnRequestResponseWithLoa(Cookie cookie, String relaySt } public static String readFile(String path) throws IOException { - return IOUtil.toString(new ClassPathResource(path).getInputStream()); + return IOUtils.toString(new ClassPathResource(path).getInputStream(), Charset.defaultCharset()); } private String deflatedBase64encoded(String input) throws IOException { @@ -258,7 +259,7 @@ protected String doOpaqueAccessToken(boolean valid, String[] scopes, String file scopeList.add("openid"); String file = String.format("oidc/%s.json", valid ? filePart : "introspect-invalid-token"); - String introspectResult = IOUtil.toString(new ClassPathResource(file).getInputStream()); + String introspectResult = IOUtils.toString(new ClassPathResource(file).getInputStream(), Charset.defaultCharset()); String introspectResultWithScope = valid ? String.format(introspectResult, String.join(" ", scopeList)) : introspectResult; stubFor(post(urlPathMatching("/introspect")).willReturn(aResponse() .withHeader("Content-Type", "application/json") @@ -306,7 +307,7 @@ protected String samlAuthnResponse(Response response, Optional optionalC response = this.get302Response(response, optionalCookieFilter); } assertEquals(HttpStatus.OK.value(), response.getStatusCode()); - String html = IOUtil.toString(response.asInputStream()); + String html = IOUtils.toString(response.asInputStream(), Charset.defaultCharset()); Matcher matcher = Pattern.compile("name=\"SAMLResponse\" value=\"(.*?)\"").matcher(html); matcher.find(); diff --git a/myconext-server/src/test/java/myconext/api/UserControllerTest.java b/myconext-server/src/test/java/myconext/api/UserControllerTest.java index d058884e..22a0271c 100644 --- a/myconext-server/src/test/java/myconext/api/UserControllerTest.java +++ b/myconext-server/src/test/java/myconext/api/UserControllerTest.java @@ -13,7 +13,7 @@ import myconext.repository.ChallengeRepository; import myconext.security.ACR; import myconext.tiqr.SURFSecureID; -import org.apache.commons.io.IOUtil; +import org.apache.commons.io.IOUtils; import org.apache.http.client.CookieStore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -27,6 +27,7 @@ import org.springframework.util.StringUtils; import java.io.IOException; +import java.nio.charset.Charset; import java.security.SecureRandom; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -250,7 +251,7 @@ public void relayState() throws IOException { String authenticationRequestId = samlAuthnRequest("Nice"); MagicLinkResponse magicLinkResponse = magicLinkRequest(new MagicLinkRequest(authenticationRequestId, user, false), HttpMethod.POST); Response response = magicResponse(magicLinkResponse); - assertTrue(IOUtil.toString(response.asInputStream()).contains("Nice")); + assertTrue(IOUtils.toString(response.asInputStream(), Charset.defaultCharset()).contains("Nice")); } @Test diff --git a/myconext-server/src/test/java/myconext/eduid/APIControllerTest.java b/myconext-server/src/test/java/myconext/eduid/APIControllerTest.java index be24f630..f76f4c84 100644 --- a/myconext-server/src/test/java/myconext/eduid/APIControllerTest.java +++ b/myconext-server/src/test/java/myconext/eduid/APIControllerTest.java @@ -3,15 +3,12 @@ import com.github.tomakehurst.wiremock.junit.WireMockRule; import io.restassured.http.ContentType; import myconext.AbstractIntegrationTest; -import org.apache.commons.io.IOUtil; import org.junit.ClassRule; import org.junit.Test; -import org.springframework.core.io.ClassPathResource; -import java.io.IOException; -import java.util.*; +import java.util.List; +import java.util.Map; -import static com.github.tomakehurst.wiremock.client.WireMock.*; import static io.restassured.RestAssured.given; import static org.junit.Assert.assertEquals; diff --git a/myconext-server/src/test/java/myconext/geo/MaxMindGeoLocationTest.java b/myconext-server/src/test/java/myconext/geo/MaxMindGeoLocationTest.java index 9771760c..d709944d 100644 --- a/myconext-server/src/test/java/myconext/geo/MaxMindGeoLocationTest.java +++ b/myconext-server/src/test/java/myconext/geo/MaxMindGeoLocationTest.java @@ -2,13 +2,14 @@ import myconext.WireMockExtension; import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtil; +import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.core.io.ClassPathResource; +import java.io.File; import java.io.IOException; import static com.github.tomakehurst.wiremock.client.WireMock.*; @@ -25,7 +26,7 @@ class MaxMindGeoLocationTest { @BeforeEach void before() throws IOException { String path = "/geo/GeoLite2-City_20220101.tar.gz"; - byte[] body = IOUtil.toByteArray(new ClassPathResource(path).getInputStream()); + byte[] body = IOUtils.toByteArray(new ClassPathResource(path).getInputStream()); stubFor(get(urlPathMatching("/maxmind")) .willReturn(aResponse() .withStatus(200) @@ -36,8 +37,8 @@ void before() throws IOException { @AfterEach void after() throws IOException { - String s = System.getProperty("java.io.tmpdir"); - FileUtils.forceDelete(s + "/geo"); + File file = new File(System.getProperty("java.io.tmpdir") + "/geo"); + FileUtils.forceDelete(file); } @Test diff --git a/myconext-server/src/test/java/myconext/security/GuestIdpAuthenticationRequestFilterTest.java b/myconext-server/src/test/java/myconext/security/GuestIdpAuthenticationRequestFilterTest.java index 804b2af6..46bdfaa3 100644 --- a/myconext-server/src/test/java/myconext/security/GuestIdpAuthenticationRequestFilterTest.java +++ b/myconext-server/src/test/java/myconext/security/GuestIdpAuthenticationRequestFilterTest.java @@ -1,14 +1,13 @@ package myconext.security; -import myconext.geo.GeoLocation; import myconext.manage.MockServiceProviderResolver; import myconext.model.LinkedAccount; import myconext.model.User; -import myconext.repository.UserLoginRepository; import myconext.repository.UserRepository; import org.junit.Test; +import org.junit.Before; import org.mockito.Mockito; -import org.springframework.security.saml.saml2.attribute.Attribute; +import saml.model.SAMLAttribute; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -23,26 +22,14 @@ public class GuestIdpAuthenticationRequestFilterTest { - private final int expiryNonValidatedDurationDays = 180; - - private final GuestIdpAuthenticationRequestFilter subject = new GuestIdpAuthenticationRequestFilter(null, - null, - null, - new MockServiceProviderResolver(), - null, - Mockito.mock(UserRepository.class), - Mockito.mock(UserLoginRepository.class), - Mockito.mock(GeoLocation.class), - 90, - 1, - 1, - false, - null, - null, - expiryNonValidatedDurationDays, - 60 * 15, - "mobile_app_rp_entity_id", - false); + private final GuestIdpAuthenticationRequestFilter subject = + new GuestIdpAuthenticationRequestFilter(); + + @Before + public void beforeEach() { + subject.setServiceProviderResolver(new MockServiceProviderResolver()); + subject.setUserRepository(Mockito.mock(UserRepository.class)); + } @Test public void isUserVerifiedByInstitutionNoLinkedAccounts() { @@ -127,11 +114,11 @@ public void attributes() { linkedAccount("Mark", "Lee", createdAt(25)) ); user.setLinkedAccounts(linkedAccounts); - List attributes = subject.attributes(user, "requesterEntityID"); - String givenName = (String) attributes.stream().filter(attr -> attr.getName().equals("urn:mace:dir:attribute-def:givenName")).findFirst().get().getValues().get(0); - String familyName = (String) attributes.stream().filter(attr -> attr.getName().equals("urn:mace:dir:attribute-def:sn")).findFirst().get().getValues().get(0); - String displayName = (String) attributes.stream().filter(attr -> attr.getName().equals("urn:mace:dir:attribute-def:displayName")).findFirst().get().getValues().get(0); - String commonName = (String) attributes.stream().filter(attr -> attr.getName().equals("urn:mace:dir:attribute-def:commonName")).findFirst().get().getValues().get(0); + List attributes = subject.attributes(user, "requesterEntityID"); + String givenName = attributes.stream().filter(attr -> attr.getName().equals("urn:mace:dir:attribute-def:givenName")).findFirst().get().getValue(); + String familyName = attributes.stream().filter(attr -> attr.getName().equals("urn:mace:dir:attribute-def:sn")).findFirst().get().getValue(); + String displayName = attributes.stream().filter(attr -> attr.getName().equals("urn:mace:dir:attribute-def:displayName")).findFirst().get().getValue(); + String commonName = attributes.stream().filter(attr -> attr.getName().equals("urn:mace:dir:attribute-def:commonName")).findFirst().get().getValue(); assertEquals("Mary", givenName); assertEquals("Poppins", familyName); diff --git a/myconext-server/src/test/java/myconext/tiqr/TiqrControllerTest.java b/myconext-server/src/test/java/myconext/tiqr/TiqrControllerTest.java index 1ca182c2..562df734 100644 --- a/myconext-server/src/test/java/myconext/tiqr/TiqrControllerTest.java +++ b/myconext-server/src/test/java/myconext/tiqr/TiqrControllerTest.java @@ -9,7 +9,7 @@ import myconext.model.MagicLinkRequest; import myconext.model.SamlAuthenticationRequest; import myconext.model.User; -import org.apache.commons.io.IOUtil; +import org.apache.commons.io.IOUtils; import org.junit.Test; import org.springframework.http.HttpMethod; import org.springframework.web.util.UriComponents; @@ -383,7 +383,7 @@ public void startAuthentication() throws Exception { List.of("TIQR_COOKIE=true", "BROWSER_SESSION=true", "TRACKING_DEVICE=", "SESSION=") .forEach(s -> assertTrue(cookies.stream().anyMatch(cookie -> cookie.startsWith(s)))); - String html = IOUtil.toString(response.asInputStream()); + String html = IOUtils.toString(response.asInputStream(), Charset.defaultCharset()); Matcher matcher = Pattern.compile("name=\"SAMLResponse\" value=\"(.*?)\"").matcher(html); matcher.find(); diff --git a/pom.xml b/pom.xml index 648300e9..1dc515e0 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.13 + 2.7.17 @@ -30,7 +30,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.9.0 + 3.11.0 11 @@ -38,7 +38,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.0.0 + 3.3.0 enforce-versions @@ -78,7 +78,7 @@ org.apache.maven.wagon wagon-webdav-jackrabbit - 3.5.1 + 3.5.3 @@ -100,6 +100,17 @@ OpenConext public snapshot repository dav:https://build.openconext.org/repository/public/snapshots + + + true + + + true + + shibboleth + shibboleth + https://build.shibboleth.net/maven/releases/ + true