From 4ebba0613ab87ade1b2380af6ee46517a43d1957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?CAS=20in=20the=20cloud=20LELEU=20J=C3=A9r=C3=B4me?= <leleuj@gmail.com> Date: Mon, 24 Jan 2022 18:08:49 +0100 Subject: [PATCH] Upgrade to the CAS server v6.4 and add the OIDC authn delegation (#550) --- Jenkinsfile | 7 +- .../archive-search-external/pom.xml | 2 +- .../archive-search-internal/pom.xml | 2 +- api/api-iam/iam-commons/pom.xml | 7 +- .../iam/common/dto/IdentityProviderDto.java | 36 ++- ...ntBuilder.java => Pac4jClientBuilder.java} | 76 +++++- api/api-iam/iam-external/pom.xml | 2 +- api/api-iam/iam-internal/pom.xml | 34 ++- .../server/config/ApiIamServerConfig.java | 6 +- .../converter/IdentityProviderConverter.java | 46 +++- .../server/idp/domain/IdentityProvider.java | 36 ++- .../IdentityProviderInternalService.java | 59 ++++- .../idp/service/SpMetadataGenerator.java | 26 +- .../src/main/resources/spring.properties | 1 + .../IdentityProviderConverterTest.java | 160 ++++++++---- ...entityProviderInternalIntegrationTest.java | 10 +- .../IdentityProviderInternalServiceTest.java | 20 ++ .../idp/service/SpMetadataGeneratorTest.java | 12 +- .../server/rest/CasControllerTest.java | 3 +- api/api-iam/iam-security/pom.xml | 2 +- .../src/main/resources/spring.properties | 1 + .../referential-commons/pom.xml | 2 +- .../referential-external/pom.xml | 2 +- .../referential-internal/pom.xml | 2 +- cas/cas-server/pom.xml | 103 ++++++-- .../config/cas-server-application-dev.yml | 73 +++--- .../config/cas-server-application-recette.yml | 70 ++--- ...dSurrogateAuthenticationPostProcessor.java | 6 +- .../IamSurrogateAuthenticationService.java | 7 +- .../authentication/UserPrincipalResolver.java | 78 ++++-- .../fr/gouv/vitamui/cas/config/AppConfig.java | 129 ++++----- .../IamClientConfigurationProperties.java | 2 - .../cas/config/InitContextConfiguration.java | 28 +- .../InitPasswordConstraintsConfiguration.java | 4 +- .../vitamui/cas/config/WebflowConfig.java | 189 ++++++-------- .../cas/pm/IamPasswordManagementService.java | 14 +- .../gouv/vitamui/cas/pm/PmMessageToSend.java | 1 - ...tSessionTicketExpirationPolicyBuilder.java | 2 +- .../cas/pm/ResetPasswordController.java | 14 +- ...va => Pac4jClientIdentityProviderDto.java} | 36 ++- .../cas/provider/ProvidersService.java | 42 +-- ...ustomOAuth20DefaultAccessTokenFactory.java | 83 ++---- .../DynamicTicketGrantingTicketFactory.java | 17 +- .../fr/gouv/vitamui/cas/util/Constants.java | 2 - .../java/fr/gouv/vitamui/cas/util/Utils.java | 4 +- .../webflow/actions/CheckMfaTokenAction.java | 7 +- ...omDelegatedClientAuthenticationAction.java | 46 +--- .../actions/CustomSendTokenAction.java | 124 ++++----- ...ustomVerifyPasswordResetRequestAction.java | 80 ------ .../cas/webflow/actions/DispatcherAction.java | 8 +- .../GeneralTerminateSessionAction.java | 106 +++----- ...8NSendPasswordResetInstructionsAction.java | 49 ++-- ...CasSimpleMultifactorWebflowConfigurer.java | 125 +++++++++ .../CustomLoginWebflowConfigurer.java | 53 ++-- .../vitamui/cas/x509/CertificateParser.java | 94 +++++++ ...RequestHeaderX509CertificateExtractor.java | 126 +++++++++ .../X509AttributeMapping.java} | 40 +-- .../X509CertificateAttributes.java} | 67 +---- .../apereo/cas/CasEmbeddedContainerUtils.java | 84 ++++-- ...ketRegistryTicketCatalogConfiguration.java | 32 --- ...aultCasDelegatingWebflowEventResolver.java | 245 ++++++++++++++++++ .../java/org/pac4j/core/client/Clients.java | 234 ----------------- .../src/main/resources/application.properties | 90 ++++--- .../src/main/resources/bootstrap.properties | 3 +- .../resources/overriden_messages.properties | 3 + .../main/resources/templates/casPwdView.html | 3 +- .../casDelegatedAuthnStopWebflow.html} | 0 .../{ => error}/casServiceErrorView.html | 0 .../templates/fragments/scripts.html | 28 +- .../src/main/resources/templates/layout.html | 2 +- .../casAccountDisabledView.html | 0 .../casAccountLockedView.html | 0 .../casAuthenticationBlockedView.html | 0 .../{ => login-error}/casExpiredPassView.html | 0 .../casMustChangePassView.html | 0 .../{ => login}/casGenericSuccessView.html | 3 +- .../templates/{ => login}/casLoginView.html | 7 +- .../templates/{ => logout}/casLogoutView.html | 3 +- .../{ => logout}/casPropagateLogoutView.html | 3 +- .../casPasswordUpdateSuccessView.html | 0 .../casResetPasswordErrorView.html | 0 .../casResetPasswordSendInstructionsView.html | 0 .../casResetPasswordSentInstructionsView.html | 0 .../casSimpleMfaLoginView.html | 0 .../mfa-simple/mfa-simple-custom-webflow.xml | 61 ----- .../vitamui/cas/BaseWebflowActionTest.java | 1 + ...IamSurrogateAuthenticationServiceTest.java | 9 +- .../UserPrincipalResolverTest.java | 30 ++- .../pm/IamPasswordManagementServiceTest.java | 15 +- .../cas/provider/ProvidersServiceTest.java | 14 +- .../actions/CheckMfaTokenActionTest.java | 74 ++++++ ...legatedClientAuthenticationActionTest.java | 32 +-- .../webflow/actions/DispatcherActionTest.java | 6 +- .../GeneralTerminateSessionActionTest.java | 6 +- .../TriggerChangePasswordActionTest.java | 59 +++++ .../cas/x509/CertificateParserTest.java | 59 +++++ .../resources/application-test.properties | 8 +- cas/cas-server/src/test/resources/client.crt | 17 ++ .../templates/cas-server/application.yml.j2 | 58 +++-- docs/fr/exploitation/sections/procedure.md | 8 +- pom.xml | 15 +- ui/ui-frontend/package.json | 14 +- 102 files changed, 2117 insertions(+), 1492 deletions(-) rename api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/{Saml2ClientBuilder.java => Pac4jClientBuilder.java} (65%) create mode 100644 api/api-iam/iam-internal/src/main/resources/spring.properties create mode 100644 api/api-ingest/ingest-internal/src/main/resources/spring.properties rename cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/{SamlIdentityProviderDto.java => Pac4jClientIdentityProviderDto.java} (74%) delete mode 100644 cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomVerifyPasswordResetRequestAction.java create mode 100644 cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomCasSimpleMultifactorWebflowConfigurer.java create mode 100644 cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CertificateParser.java create mode 100644 cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CustomRequestHeaderX509CertificateExtractor.java rename cas/cas-server/src/main/java/fr/gouv/vitamui/cas/{webflow/actions/NoOpAction.java => x509/X509AttributeMapping.java} (75%) rename cas/cas-server/src/main/java/fr/gouv/vitamui/cas/{util/VitamStatusCode.java => x509/X509CertificateAttributes.java} (55%) delete mode 100644 cas/cas-server/src/main/java/org/apereo/cas/config/HazelcastTicketRegistryTicketCatalogConfiguration.java create mode 100644 cas/cas-server/src/main/java/org/apereo/cas/web/flow/resolver/impl/DefaultCasDelegatingWebflowEventResolver.java delete mode 100644 cas/cas-server/src/main/java/org/pac4j/core/client/Clients.java rename cas/cas-server/src/main/resources/templates/{casPac4jStopWebflow.html => delegated-authn/casDelegatedAuthnStopWebflow.html} (100%) rename cas/cas-server/src/main/resources/templates/{ => error}/casServiceErrorView.html (100%) rename cas/cas-server/src/main/resources/templates/{ => login-error}/casAccountDisabledView.html (100%) rename cas/cas-server/src/main/resources/templates/{ => login-error}/casAccountLockedView.html (100%) rename cas/cas-server/src/main/resources/templates/{ => login-error}/casAuthenticationBlockedView.html (100%) rename cas/cas-server/src/main/resources/templates/{ => login-error}/casExpiredPassView.html (100%) rename cas/cas-server/src/main/resources/templates/{ => login-error}/casMustChangePassView.html (100%) rename cas/cas-server/src/main/resources/templates/{ => login}/casGenericSuccessView.html (86%) rename cas/cas-server/src/main/resources/templates/{ => login}/casLoginView.html (94%) rename cas/cas-server/src/main/resources/templates/{ => logout}/casLogoutView.html (89%) rename cas/cas-server/src/main/resources/templates/{ => logout}/casPropagateLogoutView.html (95%) rename cas/cas-server/src/main/resources/templates/{ => password-reset}/casPasswordUpdateSuccessView.html (100%) rename cas/cas-server/src/main/resources/templates/{ => password-reset}/casResetPasswordErrorView.html (100%) rename cas/cas-server/src/main/resources/templates/{ => password-reset}/casResetPasswordSendInstructionsView.html (100%) rename cas/cas-server/src/main/resources/templates/{ => password-reset}/casResetPasswordSentInstructionsView.html (100%) rename cas/cas-server/src/main/resources/templates/{ => simple-mfa}/casSimpleMfaLoginView.html (100%) delete mode 100644 cas/cas-server/src/main/resources/webflow/mfa-simple/mfa-simple-custom-webflow.xml create mode 100644 cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenActionTest.java create mode 100644 cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/TriggerChangePasswordActionTest.java create mode 100644 cas/cas-server/src/test/java/fr/gouv/vitamui/cas/x509/CertificateParserTest.java create mode 100644 cas/cas-server/src/test/resources/client.crt diff --git a/Jenkinsfile b/Jenkinsfile index bd5b58f27..6b02ffcc4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -63,7 +63,7 @@ pipeline { sh 'sudo yum remove -y nodejs' sh 'curl -sL https://rpm.nodesource.com/setup_16.x | sudo -E bash -' sh 'sudo yum install -y nodejs' - // sh 'sudo yum install -y nodejs-16.9.0-1nodesource' + // sh 'sudo yum install -y nodejs-16.9.0-1nodesource' sh 'node -v' sh '/usr/bin/node -v' sh 'npm -v' @@ -80,10 +80,13 @@ pipeline { environment { PUPPETEER_DOWNLOAD_HOST="${env.SERVICE_NEXUS_URL}/repository/puppeteer-chrome/" JAVA_TOOL_OPTIONS="" + NODE_OPTIONS="--max_old_space_size=12288" } steps { - sh 'node -v' + sh 'node -v' sh 'npmrc default' + //sh 'export NODE_OPTIONS="--max-old-space-size=12288"' + // sh ''' // $MVN_COMMAND clean verify org.owasp:dependency-check-maven:aggregate -Pvitam -pl '!cots/vitamui-nginx,!cots/vitamui-mongod,!cots/vitamui-logstash,!cots/vitamui-mongo-express' $JAVA_TOOL_OPTIONS // ''' diff --git a/api/api-archive-search/archive-search-external/pom.xml b/api/api-archive-search/archive-search-external/pom.xml index 415bf66e5..aa63aaac9 100644 --- a/api/api-archive-search/archive-search-external/pom.xml +++ b/api/api-archive-search/archive-search-external/pom.xml @@ -110,7 +110,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> </dependency> <!-- UTIL --> diff --git a/api/api-archive-search/archive-search-internal/pom.xml b/api/api-archive-search/archive-search-internal/pom.xml index 8483ba75e..58fdf49b1 100644 --- a/api/api-archive-search/archive-search-internal/pom.xml +++ b/api/api-archive-search/archive-search-internal/pom.xml @@ -111,7 +111,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> </dependency> <dependency> diff --git a/api/api-iam/iam-commons/pom.xml b/api/api-iam/iam-commons/pom.xml index 44bba5b1e..a11dfbdff 100644 --- a/api/api-iam/iam-commons/pom.xml +++ b/api/api-iam/iam-commons/pom.xml @@ -40,7 +40,12 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.pac4j</groupId> + <artifactId>pac4j-oidc</artifactId> <scope>provided</scope> </dependency> diff --git a/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/dto/IdentityProviderDto.java b/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/dto/IdentityProviderDto.java index f37f2d16b..db5beea30 100644 --- a/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/dto/IdentityProviderDto.java +++ b/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/dto/IdentityProviderDto.java @@ -37,6 +37,7 @@ package fr.gouv.vitamui.iam.common.dto; import java.util.List; +import java.util.Map; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @@ -67,6 +68,7 @@ public class IdentityProviderDto extends CustomerIdDto { */ private static final long serialVersionUID = 2372968720503585884L; + // Common data to all providers private String identifier; @NotNull @@ -85,6 +87,18 @@ public class IdentityProviderDto extends CustomerIdDto { @Size(min = 1) private List<String> patterns; + private boolean readonly; + + + // Common data to external providers (SAML + OIDC) + private String mailAttribute; + + private String identifierAttribute; + + private boolean autoProvisioningEnabled; + + + // SAML provider data private String keystoreBase64; private String keystorePassword; @@ -97,13 +111,25 @@ public class IdentityProviderDto extends CustomerIdDto { private Integer maximumAuthenticationLifetime; - private boolean readonly; + private AuthnRequestBindingEnum authnRequestBinding = AuthnRequestBindingEnum.POST; - private String mailAttribute; - private String identifierAttribute; + // OIDC provider data + private String clientId; - private AuthnRequestBindingEnum authnRequestBinding = AuthnRequestBindingEnum.POST; + private String clientSecret; - private boolean autoProvisioningEnabled; + private String discoveryUrl; + + private String scope; + + private String preferredJwsAlgorithm; + + private Map<String, String> customParams; + + private Boolean useState; + + private Boolean useNonce; + + private Boolean usePkce; } diff --git a/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/Saml2ClientBuilder.java b/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/Pac4jClientBuilder.java similarity index 65% rename from api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/Saml2ClientBuilder.java rename to api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/Pac4jClientBuilder.java index 95401e1a3..037c24f04 100644 --- a/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/Saml2ClientBuilder.java +++ b/api/api-iam/iam-commons/src/main/java/fr/gouv/vitamui/iam/common/utils/Pac4jClientBuilder.java @@ -37,14 +37,17 @@ package fr.gouv.vitamui.iam.common.utils; import java.util.Base64; +import java.util.Map; import java.util.Optional; +import com.nimbusds.jose.JWSAlgorithm; import fr.gouv.vitamui.iam.common.enums.AuthnRequestBindingEnum; import org.apache.commons.lang3.StringUtils; import org.opensaml.saml.common.xml.SAMLConstants; -import org.pac4j.core.util.Pac4jConstants; +import org.pac4j.core.client.IndirectClient; import org.pac4j.core.exception.TechnicalException; -import org.pac4j.core.util.CommonHelper; +import org.pac4j.oidc.client.OidcClient; +import org.pac4j.oidc.config.OidcConfiguration; import org.pac4j.saml.client.SAML2Client; import org.pac4j.saml.config.SAML2Configuration; import org.springframework.beans.factory.annotation.Value; @@ -59,24 +62,29 @@ import lombok.Getter; import lombok.Setter; /** - * A pac4j SAML2 client builder. + * A pac4j client builder. * * */ @Getter @Setter -public class Saml2ClientBuilder { +public class Pac4jClientBuilder { @Value("${login.url}") @NotNull private String casLoginUrl; - public Optional<SAML2Client> buildSaml2Client(final IdentityProviderDto provider) { + public Optional<IndirectClient> buildClient(final IdentityProviderDto provider) { final String technicalName = provider.getTechnicalName(); final String keystoreBase64 = provider.getKeystoreBase64(); final String keystorePassword = provider.getKeystorePassword(); final String privateKeyPassword = provider.getPrivateKeyPassword(); final String idpMetadata = provider.getIdpMetadata(); + + final String clientId = provider.getClientId(); + final String clientSecret = provider.getClientSecret(); + final String discoveryUrl = provider.getDiscoveryUrl(); + try { if (technicalName != null && keystoreBase64 != null && keystorePassword != null && privateKeyPassword != null && idpMetadata != null @@ -105,14 +113,59 @@ public class Saml2ClientBuilder { } final SAML2Client saml2Client = new SAML2Client(saml2Config); - saml2Client.setName(technicalName); - final String callbackUrl = CommonHelper.addParameter(casLoginUrl, Pac4jConstants.DEFAULT_CLIENT_NAME_PARAMETER, technicalName); - saml2Client.setCallbackUrl(callbackUrl); + setCallbackUrl(saml2Client, technicalName); saml2Client.init(); return Optional.of(saml2Client); + + } else if (clientId != null && clientSecret != null && discoveryUrl != null) { + + final OidcConfiguration oidcConfiguration = new OidcConfiguration(); + oidcConfiguration.setClientId(clientId); + oidcConfiguration.setSecret(clientSecret); + oidcConfiguration.setDiscoveryURI(discoveryUrl); + + final String scope = provider.getScope(); + if (scope != null) { + oidcConfiguration.setScope(scope); + } else { + oidcConfiguration.setScope("openid"); + } + final String algo = provider.getPreferredJwsAlgorithm(); + if (algo != null) { + oidcConfiguration.setPreferredJwsAlgorithm(JWSAlgorithm.parse(algo)); + } + final Map<String, String> customParams = provider.getCustomParams(); + if (customParams != null) { + oidcConfiguration.setCustomParams(customParams); + } + final Boolean useState = provider.getUseState(); + if (useState != null) { + oidcConfiguration.setWithState(useState); + } else { + oidcConfiguration.setWithState(true); + } + final Boolean useNonce = provider.getUseNonce(); + if (useNonce != null) { + oidcConfiguration.setUseNonce(useNonce); + } else { + oidcConfiguration.setUseNonce(true); + } + final Boolean usePkce = provider.getUsePkce(); + if (usePkce != null) { + oidcConfiguration.setDisablePkce(!usePkce); + } else { + oidcConfiguration.setDisablePkce(true); + } + + final OidcClient oidcClient = new OidcClient(); + setCallbackUrl(oidcClient, technicalName); + + oidcClient.init(); + return Optional.of(oidcClient); + } - } catch(final TechnicalException e) { + } catch (final TechnicalException e) { if(e.getMessage().contains("Error loading keystore")) { throw new InvalidFormatException(e.getMessage(), ErrorsConstants.ERRORS_VALID_KEYSPWD); } else if(e.getMessage().contains("Can't obtain SP private key")) { @@ -123,4 +176,9 @@ public class Saml2ClientBuilder { } return Optional.empty(); } + + private void setCallbackUrl(final IndirectClient client, final String technicalName) { + client.setName(technicalName); + client.setCallbackUrl(casLoginUrl); + } } diff --git a/api/api-iam/iam-external/pom.xml b/api/api-iam/iam-external/pom.xml index 400b1f584..7d21f0435 100644 --- a/api/api-iam/iam-external/pom.xml +++ b/api/api-iam/iam-external/pom.xml @@ -117,7 +117,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> </dependency> <!-- UTIL --> diff --git a/api/api-iam/iam-internal/pom.xml b/api/api-iam/iam-internal/pom.xml index 567707dcf..697423e18 100644 --- a/api/api-iam/iam-internal/pom.xml +++ b/api/api-iam/iam-internal/pom.xml @@ -131,7 +131,15 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> + </dependency> + <dependency> + <groupId>org.pac4j</groupId> + <artifactId>pac4j-oidc</artifactId> + </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> </dependency> <!-- Spring Data Mongo--> @@ -217,6 +225,18 @@ <groupId>com.zaxxer</groupId> <artifactId>*</artifactId> </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.datatype</groupId> + <artifactId>*</artifactId> + </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.jaxrs</groupId> + <artifactId>*</artifactId> + </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.dataformat</groupId> + <artifactId>*</artifactId> + </exclusion> </exclusions> </dependency> <dependency> @@ -239,6 +259,18 @@ <groupId>com.zaxxer</groupId> <artifactId>*</artifactId> </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.datatype</groupId> + <artifactId>*</artifactId> + </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.jaxrs</groupId> + <artifactId>*</artifactId> + </exclusion> + <exclusion> + <groupId>com.fasterxml.jackson.dataformat</groupId> + <artifactId>*</artifactId> + </exclusion> </exclusions> </dependency> diff --git a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/config/ApiIamServerConfig.java b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/config/ApiIamServerConfig.java index 7fcd6724a..8e47a28c8 100644 --- a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/config/ApiIamServerConfig.java +++ b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/config/ApiIamServerConfig.java @@ -53,7 +53,7 @@ import fr.gouv.vitamui.commons.vitam.api.administration.IngestContractService; import fr.gouv.vitamui.commons.vitam.api.config.VitamAccessConfig; import fr.gouv.vitamui.commons.vitam.api.config.VitamAdministrationConfig; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; -import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; +import fr.gouv.vitamui.iam.common.utils.Pac4jClientBuilder; import fr.gouv.vitamui.iam.internal.server.application.converter.ApplicationConverter; import fr.gouv.vitamui.iam.internal.server.application.dao.ApplicationRepository; import fr.gouv.vitamui.iam.internal.server.application.service.ApplicationInternalService; @@ -200,8 +200,8 @@ public class ApiIamServerConfig extends AbstractContextConfiguration { } @Bean - public Saml2ClientBuilder saml2ClientBuilder() { - return new Saml2ClientBuilder(); + public Pac4jClientBuilder pac4jClientBuilder() { + return new Pac4jClientBuilder(); } @Bean diff --git a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverter.java b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverter.java index c3c29b7d8..864000151 100644 --- a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverter.java +++ b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverter.java @@ -79,6 +79,24 @@ public class IdentityProviderConverter implements Converter<IdentityProviderDto, public static final String AUTO_PROVISIONING_ENABLED_KEY = "Mise à jour automatique des utilisateurs"; + public static final String CLIENT_ID_KEY = "Identifiant client"; + + public static final String CLIENT_SECRET_KEY = "Secret client"; + + public static final String DISCOVERY_URL_KEY = "URL de découverte des metadata"; + + public static final String SCOPE_KEY = "Périmètre"; + + public static final String PREFERRED_JWS_ALGORITHM_KEY = "Algorithme JWS préféré"; + + public static final String CUSTOM_PARAMS_KEY = "Paramètres spécifiques"; + + public static final String USE_STATE_KEY = "Avec state"; + + public static final String USE_NONCE_KEY = "Avec nonce"; + + public static final String USE_PKCE_KEY = "Avec PKCE"; + private final SpMetadataGenerator spMetadataGenerator; public IdentityProviderConverter(final SpMetadataGenerator spMetadataGenerator) { @@ -98,12 +116,15 @@ public class IdentityProviderConverter implements Converter<IdentityProviderDto, logbookData.put(AUTHENTICATION_REQUEST_BINDING_KEY, String.valueOf(dto.getAuthnRequestBinding())); logbookData.put(MAXIMUM_AUTHENTICATION_LIFE_TIME, String.valueOf(dto.getMaximumAuthenticationLifetime())); logbookData.put(AUTO_PROVISIONING_ENABLED_KEY, String.valueOf(dto.isAutoProvisioningEnabled())); + logbookData.put(CLIENT_ID_KEY, String.valueOf(dto.getClientId())); + logbookData.put(DISCOVERY_URL_KEY, String.valueOf(dto.getDiscoveryUrl())); return ApiUtils.toJson(logbookData); } @Override public IdentityProvider convertDtoToEntity(final IdentityProviderDto dto) { final IdentityProvider provider = new IdentityProvider(); + // Common provider.setId(dto.getId()); provider.setIdentifier(dto.getIdentifier()); provider.setName(dto.getName()); @@ -111,20 +132,31 @@ public class IdentityProviderConverter implements Converter<IdentityProviderDto, provider.setInternal(dto.getInternal()); provider.setTechnicalName(dto.getTechnicalName()); convertPatterns(dto, provider); + provider.setReadonly(dto.isReadonly()); + provider.setCustomerId(dto.getCustomerId()); + // SAML + OIDC + provider.setMailAttribute(dto.getMailAttribute()); + provider.setIdentifierAttribute(dto.getIdentifierAttribute()); + provider.setAutoProvisioningEnabled(dto.isAutoProvisioningEnabled()); + // SAML provider.setKeystoreBase64(dto.getKeystoreBase64()); provider.setKeystorePassword(dto.getKeystorePassword()); provider.setPrivateKeyPassword(dto.getKeystorePassword()); dto.setPrivateKeyPassword(dto.getKeystorePassword()); provider.setIdpMetadata(dto.getIdpMetadata()); - provider.setMailAttribute(dto.getMailAttribute()); - provider.setIdentifierAttribute(dto.getIdentifierAttribute()); provider.setAuthnRequestBinding(dto.getAuthnRequestBinding()); - final String spMetadata = spMetadataGenerator.generate(dto); - provider.setSpMetadata(spMetadata); - provider.setCustomerId(dto.getCustomerId()); + provider.setSpMetadata(spMetadataGenerator.generate(dto)); provider.setMaximumAuthenticationLifetime(dto.getMaximumAuthenticationLifetime()); - provider.setReadonly(dto.isReadonly()); - provider.setAutoProvisioningEnabled(dto.isAutoProvisioningEnabled()); + // OIDC + provider.setClientId(dto.getClientId()); + provider.setClientSecret(dto.getClientSecret()); + provider.setDiscoveryUrl(dto.getDiscoveryUrl()); + provider.setScope(dto.getScope()); + provider.setPreferredJwsAlgorithm(dto.getPreferredJwsAlgorithm()); + provider.setCustomParams(dto.getCustomParams()); + provider.setUseState(dto.getUseState()); + provider.setUseNonce(dto.getUseNonce()); + provider.setUsePkce(dto.getUsePkce()); return provider; } diff --git a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/domain/IdentityProvider.java b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/domain/IdentityProvider.java index 0f3854800..e94fe8378 100644 --- a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/domain/IdentityProvider.java +++ b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/domain/IdentityProvider.java @@ -37,6 +37,7 @@ package fr.gouv.vitamui.iam.internal.server.idp.domain; import java.util.List; +import java.util.Map; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @@ -66,6 +67,7 @@ import lombok.ToString; @ToString(callSuper = true, exclude = { "keystoreBase64", "keystorePassword", "privateKeyPassword", "idpMetadata", "spMetadata" }) public class IdentityProvider extends CustomerIdDocument { + // Common data to all providers @NotNull @Length(min = 1, max = 12) private String identifier; @@ -87,6 +89,18 @@ public class IdentityProvider extends CustomerIdDocument { @Size(min = 1) private List<String> patterns; + private boolean readonly; + + + // Common data to external providers (SAML + OIDC) + private String mailAttribute; + + private String identifierAttribute; + + private boolean autoProvisioningEnabled; + + + // SAML provider data private String keystoreBase64; private String keystorePassword; @@ -99,13 +113,25 @@ public class IdentityProvider extends CustomerIdDocument { private Integer maximumAuthenticationLifetime; - private boolean readonly; + private AuthnRequestBindingEnum authnRequestBinding = AuthnRequestBindingEnum.POST; - private String mailAttribute; - private String identifierAttribute; + // OIDC provider data + private String clientId; - private AuthnRequestBindingEnum authnRequestBinding = AuthnRequestBindingEnum.POST; + private String clientSecret; - private boolean autoProvisioningEnabled; + private String discoveryUrl; + + private String scope; + + private String preferredJwsAlgorithm; + + private Map<String, String> customParams; + + private Boolean useState; + + private Boolean useNonce; + + private Boolean usePkce; } diff --git a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalService.java b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalService.java index ae1cfd189..aa48afc91 100644 --- a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalService.java +++ b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalService.java @@ -247,6 +247,18 @@ public class IdentityProviderInternalService extends VitamUICrudService<Identity entity.setPatterns(patterns); } + break; + case "mailAttribute" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.MAIL_ATTRIBUTE_KEY, StringUtils.EMPTY, StringUtils.EMPTY)); + entity.setMailAttribute(CastUtils.toString(entry.getValue())); + break; + case "identifierAttribute" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.IDENTIFIER_ATTRIBUTE_KEY, StringUtils.EMPTY, StringUtils.EMPTY)); + entity.setIdentifierAttribute(CastUtils.toString(entry.getValue())); + break; + case "autoProvisioningEnabled" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.AUTO_PROVISIONING_ENABLED_KEY, entity.isAutoProvisioningEnabled(), entry.getValue())); + entity.setAutoProvisioningEnabled(CastUtils.toBoolean(entry.getValue())); break; case "keystoreBase64" : logbooks.add(new EventDiffDto(IdentityProviderConverter.KEYSTORE_BASE_64_KEY, StringUtils.EMPTY, StringUtils.EMPTY)); @@ -271,23 +283,48 @@ public class IdentityProviderInternalService extends VitamUICrudService<Identity maximumAuthenticationLifeTime)); entity.setMaximumAuthenticationLifetime(maximumAuthenticationLifeTime); break; - case "mailAttribute" : - logbooks.add(new EventDiffDto(IdentityProviderConverter.MAIL_ATTRIBUTE_KEY, StringUtils.EMPTY, StringUtils.EMPTY)); - entity.setMailAttribute(CastUtils.toString(entry.getValue())); - break; - case "identifierAttribute" : - logbooks.add(new EventDiffDto(IdentityProviderConverter.IDENTIFIER_ATTRIBUTE_KEY, StringUtils.EMPTY, StringUtils.EMPTY)); - entity.setIdentifierAttribute(CastUtils.toString(entry.getValue())); - break; case "authnRequestBinding" : final String authnRequestBindingAsString = CastUtils.toString(entry.getValue()); final AuthnRequestBindingEnum newAuthnRequestBinding = EnumUtils.stringToEnum(AuthnRequestBindingEnum.class, authnRequestBindingAsString); logbooks.add(new EventDiffDto(IdentityProviderConverter.AUTHENTICATION_REQUEST_BINDING_KEY, entity.getAuthnRequestBinding(), newAuthnRequestBinding)); entity.setAuthnRequestBinding(newAuthnRequestBinding); break; - case "autoProvisioningEnabled" : - logbooks.add(new EventDiffDto(IdentityProviderConverter.AUTO_PROVISIONING_ENABLED_KEY, entity.isAutoProvisioningEnabled(), entry.getValue())); - entity.setAutoProvisioningEnabled(CastUtils.toBoolean(entry.getValue())); + case "clientId" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.CLIENT_ID_KEY, entity.getClientId(), entry.getValue())); + entity.setClientId(CastUtils.toString(entry.getValue())); + break; + case "clientSecret" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.CLIENT_SECRET_KEY, StringUtils.EMPTY, StringUtils.EMPTY)); + entity.setClientSecret(CastUtils.toString(entry.getValue())); + break; + case "discoveryUrl" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.DISCOVERY_URL_KEY, entity.getDiscoveryUrl(), entry.getValue())); + entity.setDiscoveryUrl(CastUtils.toString(entry.getValue())); + break; + case "scope" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.SCOPE_KEY, entity.getScope(), entry.getValue())); + entity.setScope(CastUtils.toString(entry.getValue())); + break; + case "preferredJwsAlgorithm" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.PREFERRED_JWS_ALGORITHM_KEY, entity.getPreferredJwsAlgorithm(), entry.getValue())); + entity.setPreferredJwsAlgorithm(CastUtils.toString(entry.getValue())); + break; + case "customParams" : + Map<String, String> customParams = CastUtils.toMap(entry.getValue()); + logbooks.add(new EventDiffDto(IdentityProviderConverter.CUSTOM_PARAMS_KEY, entity.getCustomParams(), customParams)); + entity.setCustomParams(customParams); + break; + case "useState" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.USE_STATE_KEY, entity.getUseState(), entry.getValue())); + entity.setUseState(CastUtils.toBoolean(entry.getValue())); + break; + case "useNonce" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.USE_NONCE_KEY, entity.getUseNonce(), entry.getValue())); + entity.setUseNonce(CastUtils.toBoolean(entry.getValue())); + break; + case "usePkce" : + logbooks.add(new EventDiffDto(IdentityProviderConverter.USE_PKCE_KEY, entity.getUsePkce(), entry.getValue())); + entity.setUsePkce(CastUtils.toBoolean(entry.getValue())); break; default : throw new IllegalArgumentException("Unable to patch provider " + entity.getId() + ": key " + entry.getKey() + " is not allowed"); diff --git a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGenerator.java b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGenerator.java index 319d2a364..319f0bb48 100644 --- a/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGenerator.java +++ b/api/api-iam/iam-internal/src/main/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGenerator.java @@ -36,14 +36,12 @@ */ package fr.gouv.vitamui.iam.internal.server.idp.service; -import java.io.IOException; -import java.util.Optional; - +import org.pac4j.core.client.IndirectClient; import org.pac4j.saml.client.SAML2Client; import org.springframework.beans.factory.annotation.Autowired; import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; -import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; +import fr.gouv.vitamui.iam.common.utils.Pac4jClientBuilder; /** * A service provider metadata generator. @@ -53,25 +51,21 @@ import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; public class SpMetadataGenerator { @Autowired - private Saml2ClientBuilder saml2ClientBuilder; + private Pac4jClientBuilder pac4jClientBuilder; public String generate(final IdentityProviderDto provider) { - final Optional<SAML2Client> optionalSaml2Client = saml2ClientBuilder.buildSaml2Client(provider); - if (optionalSaml2Client.isPresent()) { - try { - return optionalSaml2Client.get().getServiceProviderMetadataResolver().getMetadata(); - } catch (final IOException e) { - throw new RuntimeException(e); - } + final IndirectClient client = pac4jClientBuilder.buildClient(provider).orElse(null); + if (client instanceof SAML2Client) { + return ((SAML2Client) client).getServiceProviderMetadataResolver().getMetadata(); } return null; } - public Saml2ClientBuilder getSaml2ClientBuilder() { - return saml2ClientBuilder; + public Pac4jClientBuilder getPac4jClientBuilder() { + return pac4jClientBuilder; } - public void setSaml2ClientBuilder(final Saml2ClientBuilder saml2ClientBuilder) { - this.saml2ClientBuilder = saml2ClientBuilder; + public void setPac4jClientBuilder(final Pac4jClientBuilder pac4jClientBuilder) { + this.pac4jClientBuilder = pac4jClientBuilder; } } diff --git a/api/api-iam/iam-internal/src/main/resources/spring.properties b/api/api-iam/iam-internal/src/main/resources/spring.properties new file mode 100644 index 000000000..da97d1db4 --- /dev/null +++ b/api/api-iam/iam-internal/src/main/resources/spring.properties @@ -0,0 +1 @@ +spring.index.ignore=true diff --git a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverterTest.java b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverterTest.java index 0261f40ee..22ed9180c 100644 --- a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverterTest.java +++ b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/converter/IdentityProviderConverterTest.java @@ -3,6 +3,8 @@ package fr.gouv.vitamui.iam.internal.server.idp.converter; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; +import java.util.List; +import java.util.Map; import fr.gouv.vitamui.iam.common.enums.AuthnRequestBindingEnum; import org.junit.Test; @@ -18,6 +20,32 @@ import fr.gouv.vitamui.iam.internal.server.idp.service.SpMetadataGenerator; public class IdentityProviderConverterTest { + private static final String CUSTOMER_ID = "customerId"; + private static final boolean ENABLED = true; + private static final String ID = "id"; + private static final String IDENTIFIER = "identifier"; + private static final String IDP_METADATA = "idpMetadata"; + private static final boolean INTERNAL = true; + private static final String KEYSTORE_BASE64 = "keystoreBase64"; + private static final String KEYSTORE_PASSWORD = "keystorePassword"; + private static final int MAXIMUM_AUTHENTICATION_LIFETIME = 5; + private static final String NAME = "name"; + private static final List<String> PATTERNS = Arrays.asList("@test.com"); + private static final String PRIVATE_KEY_PASSWORD = "privateKeyPassword"; + private static final String SP_METADATA = "spMetadata"; + private static final String TECHNICAL_NAME = "technicalname"; + private static final String MAIL_ATTRIBUTE = "mailAttribute"; + private static final String IDENTIFIER_ATTRIBUTE = "identifierAttribute"; + private static final AuthnRequestBindingEnum AUTHN_REQUEST_BINDING = AuthnRequestBindingEnum.POST; + private static final String SECRET = "secret"; + private static final String DISCOVERY_URL = "http://discoveryurl"; + private static final String SCOPE = "openid"; + private static final String PREFERRED_JWS_ALGORITHM = "HS256"; + private static final Map CUSTOM_PARAMS = Map.of("prompt", "none"); + private static final boolean USE_STATE = true; + private static final boolean USE_NONCE = true; + private static final boolean USE_PKCE = true; + private final SpMetadataGenerator spMetadataGenerator = Mockito.mock(SpMetadataGenerator.class); private final IdentityProviderConverter converter = new IdentityProviderConverter(spMetadataGenerator); @@ -25,23 +53,32 @@ public class IdentityProviderConverterTest { @Test public void testConvertEntityToDto() { IdentityProvider idp = new IdentityProvider(); - idp.setCustomerId("customerId"); - idp.setEnabled(true); - idp.setId("id"); - idp.setIdentifier("identifier"); - idp.setIdpMetadata("idpMetadata"); - idp.setInternal(true); - idp.setKeystoreBase64("keystoreBase64"); - idp.setKeystorePassword("keystorePassword"); - idp.setMaximumAuthenticationLifetime(5); - idp.setName("name"); - idp.setPatterns(Arrays.asList("@test.com")); - idp.setPrivateKeyPassword("privateKeyPassword"); - idp.setSpMetadata("spMetadata"); - idp.setTechnicalName("technicalname"); - idp.setMailAttribute("mailAttribute"); - idp.setIdentifierAttribute("identifierAttribute"); - idp.setAuthnRequestBinding(AuthnRequestBindingEnum.POST); + idp.setCustomerId(CUSTOMER_ID); + idp.setEnabled(ENABLED); + idp.setId(ID); + idp.setIdentifier(IDENTIFIER); + idp.setIdpMetadata(IDP_METADATA); + idp.setInternal(INTERNAL); + idp.setKeystoreBase64(KEYSTORE_BASE64); + idp.setKeystorePassword(KEYSTORE_PASSWORD); + idp.setMaximumAuthenticationLifetime(MAXIMUM_AUTHENTICATION_LIFETIME); + idp.setName(NAME); + idp.setPatterns(PATTERNS); + idp.setPrivateKeyPassword(PRIVATE_KEY_PASSWORD); + idp.setSpMetadata(SP_METADATA); + idp.setTechnicalName(TECHNICAL_NAME); + idp.setMailAttribute(MAIL_ATTRIBUTE); + idp.setIdentifierAttribute(IDENTIFIER_ATTRIBUTE); + idp.setAuthnRequestBinding(AUTHN_REQUEST_BINDING); + idp.setClientId(ID); + idp.setClientSecret(SECRET); + idp.setDiscoveryUrl(DISCOVERY_URL); + idp.setScope(SCOPE); + idp.setPreferredJwsAlgorithm(PREFERRED_JWS_ALGORITHM); + idp.setCustomParams(CUSTOM_PARAMS); + idp.setUseState(USE_STATE); + idp.setUseNonce(USE_NONCE); + idp.setUsePkce(USE_PKCE); IdentityProviderDto res = converter.convertEntityToDto(idp); assertThat(res).isEqualToIgnoringGivenFields(idp); } @@ -49,23 +86,32 @@ public class IdentityProviderConverterTest { @Test public void testConvertDtoToEntity() { IdentityProviderDto idp = new IdentityProviderDto(); - idp.setCustomerId("customerId"); - idp.setEnabled(true); - idp.setId("id"); - idp.setIdentifier("identifier"); - idp.setIdpMetadata("idpMetadata"); - idp.setInternal(true); - idp.setKeystoreBase64("keystoreBase64"); - idp.setKeystorePassword("keystorePassword"); - idp.setMaximumAuthenticationLifetime(5); - idp.setName("name"); - idp.setPatterns(Arrays.asList("@test.com")); - idp.setPrivateKeyPassword("privateKeyPassword"); - idp.setSpMetadata("spMetadata"); - idp.setTechnicalName("technicalname"); - idp.setMailAttribute("mailAttribute"); - idp.setIdentifierAttribute("identifierAttribute"); - idp.setAuthnRequestBinding(AuthnRequestBindingEnum.POST); + idp.setCustomerId(CUSTOMER_ID); + idp.setEnabled(ENABLED); + idp.setId(ID); + idp.setIdentifier(IDENTIFIER); + idp.setIdpMetadata(IDP_METADATA); + idp.setInternal(INTERNAL); + idp.setKeystoreBase64(KEYSTORE_BASE64); + idp.setKeystorePassword(KEYSTORE_PASSWORD); + idp.setMaximumAuthenticationLifetime(MAXIMUM_AUTHENTICATION_LIFETIME); + idp.setName(NAME); + idp.setPatterns(PATTERNS); + idp.setPrivateKeyPassword(PRIVATE_KEY_PASSWORD); + idp.setSpMetadata(SP_METADATA); + idp.setTechnicalName(TECHNICAL_NAME); + idp.setMailAttribute(MAIL_ATTRIBUTE); + idp.setIdentifierAttribute(IDENTIFIER_ATTRIBUTE); + idp.setAuthnRequestBinding(AUTHN_REQUEST_BINDING); + idp.setClientId(ID); + idp.setClientSecret(SECRET); + idp.setDiscoveryUrl(DISCOVERY_URL); + idp.setScope(SCOPE); + idp.setPreferredJwsAlgorithm(PREFERRED_JWS_ALGORITHM); + idp.setCustomParams(CUSTOM_PARAMS); + idp.setUseState(USE_STATE); + idp.setUseNonce(USE_NONCE); + idp.setUsePkce(USE_PKCE); IdentityProvider res = converter.convertDtoToEntity(idp); assertThat(res).isEqualToIgnoringGivenFields(idp, "spMetadata"); } @@ -73,23 +119,32 @@ public class IdentityProviderConverterTest { @Test public void testConvertToLogbook() throws InvalidParseOperationException { IdentityProviderDto idp = new IdentityProviderDto(); - idp.setCustomerId("customerId"); - idp.setEnabled(true); - idp.setId("id"); - idp.setIdentifier("identifier"); - idp.setIdpMetadata("idpMetadata"); - idp.setInternal(true); - idp.setKeystoreBase64("keystoreBase64"); - idp.setKeystorePassword("keystorePassword"); - idp.setMaximumAuthenticationLifetime(5); - idp.setName("name"); - idp.setPatterns(Arrays.asList("@test.com")); - idp.setPrivateKeyPassword("privateKeyPassword"); - idp.setSpMetadata("spMetadata"); - idp.setTechnicalName("technicalname"); - idp.setMailAttribute("mailAttribute"); - idp.setIdentifierAttribute("identifierAttribute"); - idp.setAuthnRequestBinding(AuthnRequestBindingEnum.POST); + idp.setCustomerId(CUSTOMER_ID); + idp.setEnabled(ENABLED); + idp.setId(ID); + idp.setIdentifier(IDENTIFIER); + idp.setIdpMetadata(IDP_METADATA); + idp.setInternal(INTERNAL); + idp.setKeystoreBase64(KEYSTORE_BASE64); + idp.setKeystorePassword(KEYSTORE_PASSWORD); + idp.setMaximumAuthenticationLifetime(MAXIMUM_AUTHENTICATION_LIFETIME); + idp.setName(NAME); + idp.setPatterns(PATTERNS); + idp.setPrivateKeyPassword(PRIVATE_KEY_PASSWORD); + idp.setSpMetadata(SP_METADATA); + idp.setTechnicalName(TECHNICAL_NAME); + idp.setMailAttribute(MAIL_ATTRIBUTE); + idp.setIdentifierAttribute(IDENTIFIER_ATTRIBUTE); + idp.setAuthnRequestBinding(AUTHN_REQUEST_BINDING); + idp.setClientId(ID); + idp.setClientSecret(SECRET); + idp.setDiscoveryUrl(DISCOVERY_URL); + idp.setScope(SCOPE); + idp.setPreferredJwsAlgorithm(PREFERRED_JWS_ALGORITHM); + idp.setCustomParams(CUSTOM_PARAMS); + idp.setUseState(USE_STATE); + idp.setUseNonce(USE_NONCE); + idp.setUsePkce(USE_PKCE); String json = converter.convertToLogbook(idp); @@ -103,6 +158,7 @@ public class IdentityProviderConverterTest { assertThat(jsonNode.get(IdentityProviderConverter.MAIL_ATTRIBUTE_KEY)).isNotNull(); assertThat(jsonNode.get(IdentityProviderConverter.IDENTIFIER_ATTRIBUTE_KEY)).isNotNull(); assertThat(jsonNode.get(IdentityProviderConverter.AUTHENTICATION_REQUEST_BINDING_KEY)).isNotNull(); + assertThat(jsonNode.get(IdentityProviderConverter.CLIENT_ID_KEY)).isNotNull(); + assertThat(jsonNode.get(IdentityProviderConverter.DISCOVERY_URL_KEY)).isNotNull(); } - } diff --git a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalIntegrationTest.java b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalIntegrationTest.java index 28f05d4ec..4958a3445 100644 --- a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalIntegrationTest.java +++ b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalIntegrationTest.java @@ -140,10 +140,18 @@ public class IdentityProviderInternalIntegrationTest extends AbstractLogbookInte service.patch(partialDto); partialDto.remove("autoProvisioningEnabled"); + partialDto.put("clientId", "1"); + service.patch(partialDto); + partialDto.remove("clientId"); + + partialDto.put("discoveryUrl", "http://url"); + service.patch(partialDto); + partialDto.remove("discoveryUrl"); + final Criteria criteria = Criteria.where("obId").is(dto.getIdentifier()).and("obIdReq").is(MongoDbCollections.PROVIDERS).and("evType") .is(EventType.EXT_VITAMUI_UPDATE_IDP); final Collection<Event> events = eventRepository.findAll(Query.query(criteria)); - assertThat(events).hasSize(6); + assertThat(events).hasSize(8); } private IdentityProviderDto createIdp() { diff --git a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalServiceTest.java b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalServiceTest.java index 380432ce9..60e617c52 100644 --- a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalServiceTest.java +++ b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/IdentityProviderInternalServiceTest.java @@ -305,6 +305,17 @@ public class IdentityProviderInternalServiceTest extends AbstractServerIdentityB partialDto.put("keystorePassword", "keyspwd"); partialDto.put("idpMetadata", "<xml></xml>"); partialDto.put("autoProvisioningEnabled", true); + partialDto.put("clientId", "1"); + partialDto.put("clientSecret", "secret"); + partialDto.put("discoveryUrl", "http://url"); + partialDto.put("scope", "openid"); + partialDto.put("preferredJwsAlgorithm", "HS256"); + Map<String, String> customParams = new HashMap<>(); + customParams.put("prompt", "login"); + partialDto.put("customParams", customParams); + partialDto.put("useState", true); + partialDto.put("useNonce", true); + partialDto.put("usePkce", true); service.processPatch(idp, partialDto); assertThat(idp.getName()).isEqualTo("nameTest"); @@ -315,6 +326,15 @@ public class IdentityProviderInternalServiceTest extends AbstractServerIdentityB assertThat(idp.getIdpMetadata()).isEqualTo("<xml></xml>"); assertThat(idp.getPatterns()).isEqualTo(Arrays.asList(".*@vitamui.com", ".*@vitamui.com")); assertThat(idp.isAutoProvisioningEnabled()).isTrue(); + assertThat(idp.getClientId()).isEqualTo("1"); + assertThat(idp.getClientSecret()).isEqualTo("secret"); + assertThat(idp.getDiscoveryUrl()).isEqualTo("http://url"); + assertThat(idp.getScope()).isEqualTo("openid"); + assertThat(idp.getPreferredJwsAlgorithm()).isEqualTo("HS256"); + assertThat(idp.getCustomParams()).isEqualTo(customParams); + assertThat(idp.getUseState()).isTrue(); + assertThat(idp.getUseNonce()).isTrue(); + assertThat(idp.getUsePkce()).isTrue(); } @Test diff --git a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGeneratorTest.java b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGeneratorTest.java index 1cda03a94..378b6740d 100644 --- a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGeneratorTest.java +++ b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/idp/service/SpMetadataGeneratorTest.java @@ -10,10 +10,10 @@ import org.springframework.core.io.ClassPathResource; import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; import fr.gouv.vitamui.iam.common.utils.IdentityProviderBuilder; -import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; +import fr.gouv.vitamui.iam.common.utils.Pac4jClientBuilder; /** - * Tests {@link Saml2ClientBuilder} and {@link SpMetadataGenerator}. + * Tests {@link Pac4jClientBuilder} and {@link SpMetadataGenerator}. * * */ @@ -21,17 +21,17 @@ public final class SpMetadataGeneratorTest { private static final String CAS_URL = "http://cas/login"; - private Saml2ClientBuilder builder; + private Pac4jClientBuilder builder; @Before public void setUp() { - builder = new Saml2ClientBuilder(); + builder = new Pac4jClientBuilder(); builder.setCasLoginUrl(CAS_URL); } @Test public void testSimpleProvider() { - assertFalse(builder.buildSaml2Client(new IdentityProviderDto()).isPresent()); + assertFalse(builder.buildClient(new IdentityProviderDto()).isPresent()); } @Test @@ -40,7 +40,7 @@ public final class SpMetadataGeneratorTest { new ClassPathResource("test-idp/sp-test-keystore.jks"), "password", "password", new ClassPathResource("test-idp/idp-test-metadata.xml"), "clientId", false, "mailAttribute", "identifierAttribute", AuthnRequestBindingEnum.POST, false).build(); final SpMetadataGenerator generator = new SpMetadataGenerator(); - generator.setSaml2ClientBuilder(builder); + generator.setPac4jClientBuilder(builder); final String metadata = generator.generate(provider); assertTrue(metadata.contains("entityID=\"http://cas/login/idp0\"")); assertTrue(metadata.contains( diff --git a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/rest/CasControllerTest.java b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/rest/CasControllerTest.java index f85f19632..9c13a493c 100644 --- a/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/rest/CasControllerTest.java +++ b/api/api-iam/iam-internal/src/test/java/fr/gouv/vitamui/iam/internal/server/rest/CasControllerTest.java @@ -256,10 +256,11 @@ public final class CasControllerTest extends AbstractServerIdentityBuilder { } @Test - public void testLoginKoNullPasswords() { + public void testLoginKoBlankPasswords() { user.setType(UserTypeEnum.NOMINATIVE); final LoginRequestDto request = new LoginRequestDto(); request.setUsername(EMAIL); + request.setPassword(""); try { controller.login(request); fail("should fail"); diff --git a/api/api-iam/iam-security/pom.xml b/api/api-iam/iam-security/pom.xml index 3b8d72647..c57ba5373 100644 --- a/api/api-iam/iam-security/pom.xml +++ b/api/api-iam/iam-security/pom.xml @@ -51,7 +51,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> <scope>provided</scope> </dependency> diff --git a/api/api-ingest/ingest-internal/src/main/resources/spring.properties b/api/api-ingest/ingest-internal/src/main/resources/spring.properties new file mode 100644 index 000000000..da97d1db4 --- /dev/null +++ b/api/api-ingest/ingest-internal/src/main/resources/spring.properties @@ -0,0 +1 @@ +spring.index.ignore=true diff --git a/api/api-referential/referential-commons/pom.xml b/api/api-referential/referential-commons/pom.xml index 105ebf3cf..20100c25b 100644 --- a/api/api-referential/referential-commons/pom.xml +++ b/api/api-referential/referential-commons/pom.xml @@ -35,7 +35,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> <scope>provided</scope> </dependency> diff --git a/api/api-referential/referential-external/pom.xml b/api/api-referential/referential-external/pom.xml index 171b2c218..1558130e2 100644 --- a/api/api-referential/referential-external/pom.xml +++ b/api/api-referential/referential-external/pom.xml @@ -115,7 +115,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> </dependency> <!-- UTIL --> diff --git a/api/api-referential/referential-internal/pom.xml b/api/api-referential/referential-internal/pom.xml index eb3c3ba3b..dbc2b43df 100644 --- a/api/api-referential/referential-internal/pom.xml +++ b/api/api-referential/referential-internal/pom.xml @@ -157,7 +157,7 @@ <!-- PAC4J --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> </dependency> <!-- UTIL --> diff --git a/cas/cas-server/pom.xml b/cas/cas-server/pom.xml index 3cb4ab9c5..cb418f60e 100644 --- a/cas/cas-server/pom.xml +++ b/cas/cas-server/pom.xml @@ -12,17 +12,19 @@ <properties> <assertj-core.version>3.11.1</assertj-core.version> - <jackson.version>2.10.0</jackson.version> - <lombok.version>1.18.10</lombok.version> - <micrometer.version>1.3.0</micrometer.version> - <mockito.version>1.10.19</mockito.version> - <spring.boot.version>2.2.0.RELEASE</spring.boot.version> - <spring.cloud.consul.version>2.2.2.RELEASE</spring.cloud.consul.version> - <spring.security.version>5.2.0.RELEASE</spring.security.version> - <spring.test.version>5.2.0.RELEASE</spring.test.version> - <spring.version>5.2.0.RELEASE</spring.version> - <swagger.version>1.5.18</swagger.version> - <thymeleaf-spring5.version>3.0.11.RELEASE</thymeleaf-spring5.version> + <jackson.version>2.12.4</jackson.version> + <lombok.version>1.18.20</lombok.version> + <micrometer.version>1.7.3</micrometer.version> + <mockito.version>3.12.1</mockito.version> + <spring.boot.version>2.5.4</spring.boot.version> + <spring.cloud.consul.version>3.0.3</spring.cloud.consul.version> + <spring.security.version>5.5.2</spring.security.version> + <spring.test.version>5.3.9</spring.test.version> + <spring.version>5.3.9</spring.version> + <swagger.version>2.1.10</swagger.version> + <mongo.version>4.3.1</mongo.version> + <spring-webmvc-pac4j.version>5.0.0</spring-webmvc-pac4j.version> + <thymeleaf-spring5.version>3.0.12.RELEASE</thymeleaf-spring5.version> <rpm.skip>false</rpm.skip> <rpm.jar-file>${project.build.finalName}.war</rpm.jar-file> @@ -46,6 +48,14 @@ <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </exclusion> + <exclusion> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-api</artifactId> + </exclusion> + <exclusion> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + </exclusion> </exclusions> </dependency> @@ -88,6 +98,11 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>org.mongodb</groupId> + <artifactId>mongodb-driver-sync</artifactId> + <version>${mongo.version}</version> + </dependency> <!-- authentication delegation --> <dependency> @@ -139,11 +154,11 @@ <dependency> <groupId>org.pac4j</groupId> <artifactId>spring-webmvc-pac4j</artifactId> - <version>${pac4j.version}</version> + <version>${spring-webmvc-pac4j.version}</version> </dependency> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> @@ -158,6 +173,18 @@ <version>${cas.version}</version> </dependency> + <!-- X509 authentication --> + <dependency> + <groupId>org.apereo.cas</groupId> + <artifactId>cas-server-support-x509-webflow</artifactId> + <version>${cas.version}</version> + </dependency> + <dependency> + <groupId>org.apereo.cas</groupId> + <artifactId>cas-server-support-x509-core</artifactId> + <version>${cas.version}</version> + </dependency> + <!-- subrogation --> <dependency> <groupId>org.apereo.cas</groupId> @@ -196,6 +223,11 @@ <artifactId>cas-server-support-pm-core</artifactId> <version>${cas.version}</version> </dependency> + <dependency> + <groupId>org.apereo.cas</groupId> + <artifactId>cas-server-core-notifications</artifactId> + <version>${cas.version}</version> + </dependency> <!-- multi-factor authentication --> <dependency> @@ -223,6 +255,11 @@ <artifactId>cas-server-support-sms-twilio</artifactId> <version>${cas.version}</version> </dependency> + <dependency> + <groupId>org.apereo.cas</groupId> + <artifactId>cas-server-core-authentication-mfa-api</artifactId> + <version>${cas.version}</version> + </dependency> <!-- throttling --> <dependency> @@ -282,8 +319,6 @@ <artifactId>opentracing-spring-jaeger-web-starter</artifactId> </dependency> - - <!-- OAuth support --> <dependency> <groupId>org.apereo.cas</groupId> @@ -334,6 +369,16 @@ </dependency> <!-- logs --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-logging</artifactId> + <exclusions> + <exclusion> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-to-slf4j</artifactId> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> @@ -348,6 +393,11 @@ <artifactId>jul-to-slf4j</artifactId> <version>${slf4j.version}</version> </dependency> + <dependency> + <groupId>org.gandon.tomcat</groupId> + <artifactId>juli-to-slf4j</artifactId> + <version>1.1.1</version> + </dependency> <!-- UTIL --> <dependency> @@ -359,6 +409,21 @@ <artifactId>thymeleaf-spring5</artifactId> <version>${thymeleaf-spring5.version}</version> </dependency> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + <version>1.15</version> + </dependency> + <dependency> + <groupId>org.webjars</groupId> + <artifactId>jquery-ui</artifactId> + <version>1.12.1</version> + </dependency> + <dependency> + <groupId>org.webjars</groupId> + <artifactId>font-awesome</artifactId> + <version>5.11.2</version> + </dependency> <!-- TEST --> <dependency> @@ -425,6 +490,7 @@ <exclude>WEB-INF/lib/log4j-web-*.jar</exclude> <exclude>WEB-INF/lib/log4j-slf4j-impl-*.jar</exclude> <exclude>WEB-INF/lib/log4j-slf4j18-impl-*.jar</exclude> + <exclude>WEB-INF/lib/log4j-layout-template-json-*.jar</exclude> <exclude>WEB-INF/lib/spring-boot-starter-log4j2-*.jar</exclude> <exclude>WEB-INF/lib/slf4j-api-1.8.0-beta4.jar</exclude> <exclude>WEB-INF/lib/jcl-over-slf4j-1.8.0-beta4.jar</exclude> @@ -432,6 +498,8 @@ <exclude>WEB-INF/lib/jackson-core-2.8.10.jar</exclude> <exclude>WEB-INF/lib/jackson-databind-2.8.10.jar</exclude> <exclude>WEB-INF/lib/jackson-dataformat-yaml-2.8.10.jar</exclude> + <exclude>WEB-INF/lib/cas-server-core-logging-api-*.jar</exclude> + <exclude>WEB-INF/lib/slf4j-api-*.jar</exclude> </excludes> </overlay> </overlays> @@ -444,13 +512,16 @@ WEB-INF/lib/log4j-web-*.jar, WEB-INF/lib/log4j-slf4j-impl-*.jar, WEB-INF/lib/log4j-slf4j18-impl-*.jar, + WEB-INF/lib/log4j-layout-template-json-*.jar, WEB-INF/lib/spring-boot-starter-log4j2-*.jar, WEB-INF/lib/slf4j-api-1.8.0-beta4.jar, WEB-INF/lib/jcl-over-slf4j-1.8.0-beta4.jar, WEB-INF/lib/jul-to-slf4j-1.8.0-beta4.jar, WEB-INF/lib/jackson-core-2.8.10.jar, WEB-INF/lib/jackson-databind-2.8.10.jar, - WEB-INF/lib/jackson-dataformat-yaml-2.8.10.jar + WEB-INF/lib/jackson-dataformat-yaml-2.8.10.jar, + WEB-INF/lib/cas-server-core-logging-api-*.jar, + WEB-INF/lib/slf4j-api-*.jar </packagingExcludes> </configuration> </plugin> diff --git a/cas/cas-server/src/main/config/cas-server-application-dev.yml b/cas/cas-server/src/main/config/cas-server-application-dev.yml index 53d7b185f..a7ed5d45f 100644 --- a/cas/cas-server/src/main/config/cas-server-application-dev.yml +++ b/cas/cas-server/src/main/config/cas-server-application-dev.yml @@ -49,7 +49,7 @@ iam-client: cas.authn.accept.users: -cas.messageBundle.baseNames: classpath:overriden_messages,classpath:messages +cas.message-bundle.base-names: classpath:overriden_messages,classpath:messages cas.tgc.path: /cas @@ -61,45 +61,46 @@ cas.authn.pm.reset.crypto.enabled: true cas.server.prefix: https://dev.vitamui.com:8080/cas login.url: ${cas.server.prefix}/login -cas.serviceRegistry.mongo.clientUri: mongodb://mongod_dbuser_cas:mongod_dbpwd_cas@localhost:27018/cas +cas.service-registry.mongo.client-uri: mongodb://mongod_dbuser_cas:mongod_dbpwd_cas@localhost:27018/cas -#cas.serviceRegistry.mongo.port: 27018 -#cas.serviceRegistry.mongo.databaseName: cas -#cas.serviceRegistry.mongo.authenticationDatabaseName: cas -#cas.serviceRegistry.mongo.replicaSet: rs0 -cas.serviceRegistry.mongo.collection: services -#cas.serviceRegistry.mongo.userId: mongod_dbuser_cas -#cas.serviceRegistry.mongo.password: mongod_dbpwd_cas +#cas.service-registry.mongo.port: 27018 +#cas.service-registry.mongo.database-name: cas +#cas.service-registry.mongo.authentication-database-name: cas +#cas.service-registry.mongo.replica-set: rs0 +cas.service-registry.mongo.collection: services +#cas.service-registry.mongo.user-id: mongod_dbuser_cas +#cas.service-registry.mongo.password: mongod_dbpwd_cas cas.authn.surrogate.separator: "," -cas.authn.surrogate.sms.attributeName: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS +cas.authn.surrogate.sms.attribute-name: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS # 24 hours cache for login delegation -cas.ticket.tst.timeToKillInSeconds: 86400 +# Must be at least 24 hours as this cache is also used for password management and its 24-hour-creation email +cas.ticket.tst.time-to-kill-in-seconds: 86400 -cas.authn.pm.enabled: true -cas.authn.pm.policyPattern: '^(?=(.*[$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]){2,})(?=(?:.*[a-z]){2,})(?=(?:.*[A-Z]){2,})(?=(?:.*[\d]){2,})[A-Za-zÀ-ÿ0-9$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]{${password.length},}$' +cas.authn.pm.core.enabled: true +cas.authn.pm.core.policy-pattern: '^(?=(.*[$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]){2,})(?=(?:.*[a-z]){2,})(?=(?:.*[A-Z]){2,})(?=(?:.*[\d]){2,})[A-Za-zÀ-ÿ0-9$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]{${password.length},}$' cas.authn.pm.reset.mail.subject: Requete de reinitialisation de mot de passe cas.authn.pm.reset.mail.text: "Changez de mot de passe via le lien: %s" cas.authn.pm.reset.mail.from: serveur-cas@noreply.com # 1 Day : 24 * 60 Minutes to reset password -cas.authn.pm.reset.expirationMinutes: 1440 -cas.authn.pm.reset.mail.attributeName: email -cas.authn.pm.reset.securityQuestionsEnabled: false -cas.authn.pm.reset.includeServerIpAddress: false -cas.authn.pm.autoLogin: true +cas.authn.pm.reset.expiration-minutes: 1440 +cas.authn.pm.reset.mail.attribute-name: email +cas.authn.pm.reset.security-questions-enabled: false +cas.authn.pm.reset.include-server-ip-address: false +cas.authn.pm.core.auto-login: true cas.authn.mfa.simple.sms.from: 'changeme' cas.authn.mfa.simple.sms.text: 'Code : %s' -cas.authn.mfa.simple.sms.attributeName: mobile -cas.authn.mfa.simple.timeToKillInSeconds: 3600 -cas.authn.mfa.simple.tokenLength: 4 -cas.authn.mfa.globalPrincipalAttributeNameTriggers: computedOtp -cas.authn.mfa.globalPrincipalAttributeValueRegex: 'true' +cas.authn.mfa.simple.sms.attribute-name: mobile +cas.authn.mfa.simple.time-to-kill-in-seconds: 3600 +cas.authn.mfa.simple.token-length: 4 +cas.authn.mfa.triggers.principal.global-principal-attribute-name-triggers: computedOtp +cas.authn.mfa.triggers.principal.global-principal-attribute-value-regex: 'true' cas.authn.mfa.simple.mail.text: xxx @@ -113,13 +114,13 @@ spring.mail.properties.mail.smtp.starttls.enable: false cas.authn.throttle.failure.threshold: 2 -cas.authn.throttle.failure.rangeSeconds: 3 +cas.authn.throttle.failure.range-seconds: 3 cas: logout: - followServiceRedirects: true - redirectParameter: next + follow-service-redirects: true + redirect-parameter: next management.endpoints.enabled-by-default: true @@ -128,8 +129,8 @@ cas.monitor.endpoints.endpoint.defaults.access[0]: PERMIT # for SMS: -cas.smsProvider.twilio.accountId: changeme -cas.smsProvider.twilio.token: changeme +cas.sms-provider.twilio.account-id: changeme +cas.sms-provider.twilio.token: changeme vitamui.portal.url: https://dev.vitamui.com:4200/ @@ -142,7 +143,8 @@ ip.header: X-Real-IP # 8 hours in seconds -api.token.ttl: 28800 +# the old api.token.ttl property +cas.authn.oauth.access-token.max-time-to-live-in-seconds: 28800 server-identity: @@ -155,7 +157,6 @@ server-identity: theme: # vitamui-platform-name: VITAM-UI # vitamui-favicon: /absolute/path/to/favicon.ico - # vitam-logo: /absolute/path/to/logo.png # vitamui-logo-large: /absolute/path/to/logo.png primary: '#702382' secondary: '#241f63' @@ -171,7 +172,7 @@ opentracing: host: localhost port: 6831 -debug: true +#debug: true logging: config: src/main/config/logback-dev.xml level: @@ -181,11 +182,11 @@ logging: org.apereo.inspektr.audit.support: 'OFF' # Cas CORS (necessary for mobile app) -cas.httpWebRequest.cors.enabled: true -cas.httpWebRequest.cors.allowCredentials: false -cas.httpWebRequest.cors.allowOrigins: [ '*' ] -cas.httpWebRequest.cors.allowMethods: [ '*' ] -cas.httpWebRequest.cors.allowHeaders: [ '*' ] +cas.http-web-request.cors.enabled: true +cas.http-web-request.cors.allow-credentials: false +cas.http-web-request.cors.allow-origins: [ '*' ] +cas.http-web-request.cors.allow-methods: [ '*' ] +cas.http-web-request.cors.allow-headers: [ '*' ] # Password configuration password: diff --git a/cas/cas-server/src/main/config/cas-server-application-recette.yml b/cas/cas-server/src/main/config/cas-server-application-recette.yml index bf82b4c93..6b47e561e 100644 --- a/cas/cas-server/src/main/config/cas-server-application-recette.yml +++ b/cas/cas-server/src/main/config/cas-server-application-recette.yml @@ -41,7 +41,7 @@ iam-client: cas.authn.accept.users: -cas.messageBundle.baseNames: classpath:overriden_messages,classpath:messages +cas.message-bundle.base-names: classpath:overriden_messages,classpath:messages cas.tgc.path: /cas @@ -53,45 +53,46 @@ cas.authn.pm.reset.crypto.enabled: true cas.server.prefix: https://dev.vitamui.com:8080/cas login.url: ${cas.server.prefix}/login -cas.serviceRegistry.mongo.clientUri: mongodb://mongod_dbuser_cas:mongod_dbpwd_cas@localhost:27018/cas +cas.service-registry.mongo.client-uri: mongodb://mongod_dbuser_cas:mongod_dbpwd_cas@localhost:27018/cas -#cas.serviceRegistry.mongo.port: 27018 -#cas.serviceRegistry.mongo.databaseName: cas -#cas.serviceRegistry.mongo.authenticationDatabaseName: cas -#cas.serviceRegistry.mongo.replicaSet: rs0 -cas.serviceRegistry.mongo.collection: services -#cas.serviceRegistry.mongo.userId: mongod_dbuser_cas -#cas.serviceRegistry.mongo.password: mongod_dbpwd_cas +#cas.service-registry.mongo.port: 27018 +#cas.service-registry.mongo.database-name: cas +#cas.service-registry.mongo.authentication-database-name: cas +#cas.service-registry.mongo.replica-set: rs0 +cas.service-registry.mongo.collection: services +#cas.service-registry.mongo.user-id: mongod_dbuser_cas +#cas.service-registry.mongo.password: mongod_dbpwd_cas cas.authn.surrogate.separator: "," -cas.authn.surrogate.sms.attributeName: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS +cas.authn.surrogate.sms.attribute-name: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS # 24 hours cache for login delegation -cas.ticket.tst.timeToKillInSeconds: 86400 +# Must be at least 24 hours as this cache is also used for password management and its 24-hour-creation email +cas.ticket.tst.time-to-kill-in-seconds: 86400 -cas.authn.pm.enabled: true -cas.authn.pm.policyPattern: '^(?=(.*[$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]){2,})(?=(?:.*[a-z]){2,})(?=(?:.*[A-Z]){2,})(?=(?:.*[\d]){2,})[A-Za-zÀ-ÿ0-9$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]{12,}$' +cas.authn.pm.core.enabled: true +cas.authn.pm.core.policy-pattern: '^(?=(.*[$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]){2,})(?=(?:.*[a-z]){2,})(?=(?:.*[A-Z]){2,})(?=(?:.*[\d]){2,})[A-Za-zÀ-ÿ0-9$@!%*#£?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]{12,}$' cas.authn.pm.reset.mail.subject: Requete de reinitialisation de mot de passe cas.authn.pm.reset.mail.text: "Changez de mot de passe via le lien: %s" cas.authn.pm.reset.mail.from: serveur-cas@noreply.com # 1 Day : 24 * 60 Minutes to reset password -cas.authn.pm.reset.expirationMinutes: 1440 -cas.authn.pm.reset.mail.attributeName: email -cas.authn.pm.reset.securityQuestionsEnabled: false -cas.authn.pm.reset.includeServerIpAddress: false -cas.authn.pm.autoLogin: true +cas.authn.pm.reset.expiration-minutes: 1440 +cas.authn.pm.reset.mail.attribute-name: email +cas.authn.pm.reset.security-questions-enabled: false +cas.authn.pm.reset.include-server-ip-address: false +cas.authn.pm.core.auto-login: true cas.authn.mfa.simple.sms.from: 'changeme' cas.authn.mfa.simple.sms.text: 'Code : %s' -cas.authn.mfa.simple.sms.attributeName: mobile -cas.authn.mfa.simple.timeToKillInSeconds: 3600 -cas.authn.mfa.simple.tokenLength: 4 -cas.authn.mfa.globalPrincipalAttributeNameTriggers: computedOtp -cas.authn.mfa.globalPrincipalAttributeValueRegex: 'true' +cas.authn.mfa.simple.sms.attribute-name: mobile +cas.authn.mfa.simple.time-to-kill-in-seconds: 3600 +cas.authn.mfa.simple.token-length: 4 +cas.authn.mfa.triggers.principal.global-principal-attribute-name-triggers: computedOtp +cas.authn.mfa.triggers.principal.global-principal-attribute-value-regex: 'true' cas.authn.mfa.simple.mail.text: xxx spring.mail.host: localhost @@ -104,13 +105,13 @@ spring.mail.properties.mail.smtp.starttls.enable: false cas.authn.throttle.failure.threshold: 2 -cas.authn.throttle.failure.rangeSeconds: 3 +cas.authn.throttle.failure.range-seconds: 3 cas: logout: - followServiceRedirects: true - redirectParameter: next + follow-service-redirects: true + redirect-parameter: next management.endpoints.enabled-by-default: true @@ -119,8 +120,8 @@ cas.monitor.endpoints.endpoint.defaults.access[0]: PERMIT # for SMS: -cas.smsProvider.twilio.accountId: changeme -cas.smsProvider.twilio.token: changeme +cas.sms-provider.twilio.account-id: changeme +cas.sms-provider.twilio.token: changeme vitamui.portal.url: https://dev.vitamui.com:9000/ @@ -133,7 +134,8 @@ ip.header: X-Real-IP # 8 hours in seconds -api.token.ttl: 28800 +# the old api.token.ttl property +cas.authn.oauth.access-token.max-time-to-live-in-seconds: 28800 server-identity: @@ -164,11 +166,11 @@ logging: # org.apereo.inspektr.audit.support: 'OFF' # Cas CORS (necessary for mobile app) -cas.httpWebRequest.cors.enabled: true -cas.httpWebRequest.cors.allowCredentials: false -cas.httpWebRequest.cors.allowOrigins: [ '*' ] -cas.httpWebRequest.cors.allowMethods: [ '*' ] -cas.httpWebRequest.cors.allowHeaders: [ '*' ] +cas.http-web-request.cors.enabled: true +cas.http-web-request.cors.allow-credentials: false +cas.http-web-request.cors.allow-origins: [ '*' ] +cas.http-web-request.cors.allow-methods: [ '*' ] +cas.http-web-request.cors.allow-headers: [ '*' ] # Password configuration password: diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/DelegatedSurrogateAuthenticationPostProcessor.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/DelegatedSurrogateAuthenticationPostProcessor.java index bfdc73ae9..ebb69020e 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/DelegatedSurrogateAuthenticationPostProcessor.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/DelegatedSurrogateAuthenticationPostProcessor.java @@ -52,6 +52,8 @@ import org.springframework.webflow.execution.RequestContextHolder; import lombok.val; +import java.util.Collections; + /** * Post-processor which also handles the surrogation in the authentication delegation. * @@ -82,7 +84,7 @@ public class DelegatedSurrogateAuthenticationPostProcessor extends SurrogateAuth val requestContext = RequestContextHolder.getRequestContext(); val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext); - val webContext = new JEEContext(request, response, sessionStore); + val webContext = new JEEContext(request, response); val surrogateInSession = sessionStore.get(webContext, Constants.SURROGATE).orElse(null); if (surrogateInSession != null) { LOGGER.debug("surrogate: {} found after authentication delegation -> overriding credential", surrogateInSession); @@ -91,7 +93,7 @@ public class DelegatedSurrogateAuthenticationPostProcessor extends SurrogateAuth newCredential.setSurrogateUsername((String) surrogateInSession); WebUtils.putCredential(requestContext, newCredential); - final AuthenticationTransaction newTransaction = DefaultAuthenticationTransaction.of(transaction.getService(), newCredential); + final AuthenticationTransaction newTransaction = new DefaultAuthenticationTransaction(transaction.getService(), Collections.singletonList(newCredential)); super.process(builder, newTransaction); } else { return; diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationService.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationService.java index a25433a70..94deb8add 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationService.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationService.java @@ -37,6 +37,7 @@ package fr.gouv.vitamui.cas.authentication; import java.util.List; +import java.util.Optional; import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.authentication.principal.Service; @@ -51,8 +52,6 @@ import fr.gouv.vitamui.iam.common.enums.SubrogationStatusEnum; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; import lombok.val; -import lombok.val; - /** * Specific surrogate service based on the IAM API. * @@ -74,8 +73,8 @@ public class IamSurrogateAuthenticationService extends BaseSurrogateAuthenticati } @Override - public boolean canAuthenticateAsInternal(final String surrogate, final Principal principal, final Service service) { - val id = (String) principal.getAttributes().get(UserPrincipalResolver.SUPER_USER_ID_ATTRIBUTE).get(0); + public boolean canAuthenticateAsInternal(String surrogate, Principal principal, Optional<Service> service) { + val id = principal.getId(); boolean canAuthenticate = false; try { val subrogations = casExternalRestClient.getSubrogationsBySuperUserId(utils.buildContext(id), id); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolver.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolver.java index c068c04c5..3bfd91ee7 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolver.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolver.java @@ -36,14 +36,20 @@ */ package fr.gouv.vitamui.cas.authentication; +import java.security.cert.CertificateParsingException; import java.util.*; +import java.util.regex.Pattern; import fr.gouv.vitamui.cas.provider.ProvidersService; -import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; +import fr.gouv.vitamui.cas.x509.CertificateParser; +import fr.gouv.vitamui.cas.x509.X509AttributeMapping; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang.StringUtils; +import org.apereo.cas.adaptors.x509.authentication.principal.X509CertificateCredential; import org.apereo.cas.authentication.AuthenticationHandler; import org.apereo.cas.authentication.Credential; +import org.apereo.cas.authentication.SurrogatePrincipal; import org.apereo.cas.authentication.SurrogateUsernamePasswordCredential; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; import org.apereo.cas.authentication.principal.ClientCredential; @@ -81,6 +87,7 @@ import static fr.gouv.vitamui.commons.api.CommonConstants.*; @RequiredArgsConstructor public class UserPrincipalResolver implements PrincipalResolver { + public static final String EMAIL_VALID_REGEXP = "^[_a-z0-9]+(((\\.|-)[_a-z0-9]+))*@[a-z0-9-]+(\\.[a-z0-9-]+)*(\\.[a-z]{2,})$"; public static final String SUPER_USER_ID_ATTRIBUTE = "superUserId"; public static final String COMPUTED_OTP = "computedOtp"; @@ -98,19 +105,50 @@ public class UserPrincipalResolver implements PrincipalResolver { private final ProvidersService providersService; + private final X509AttributeMapping x509EmailAttributeMapping; + + private final X509AttributeMapping x509IdentifierAttributeMapping; + + private final String x509DefaultDomain; + @Override public Principal resolve(final Credential credential, final Optional<Principal> optPrincipal, final Optional<AuthenticationHandler> handler) { val principal = optPrincipal.get(); - val userId = principal.getId(); + val principalId = principal.getId(); val requestContext = RequestContextHolder.getRequestContext(); final boolean surrogationCall; - final String username; + String username; final String superUsername; - final String userProviderId; + String userProviderId; final Optional<String> technicalUserId; - if (credential instanceof SurrogateUsernamePasswordCredential) { + // x509 certificate + if (credential instanceof X509CertificateCredential) { + try { + val certificate = ((X509CertificateCredential) credential).getCertificate(); + username = CertificateParser.extract(certificate, x509EmailAttributeMapping); + technicalUserId = Optional.ofNullable(CertificateParser.extract(certificate, x509IdentifierAttributeMapping)); + } catch (final CertificateParsingException e) { + throw new RuntimeException(e.getMessage()); + } + superUsername = null; + userProviderId = null; + surrogationCall = false; + + String userDomain = username; + + // If the certificate does not contain the user mail, then we use the default domain configured + if (StringUtils.isBlank(userDomain) || !Pattern.matches(EMAIL_VALID_REGEXP, userDomain)) { + userDomain = String.format("@%s", x509DefaultDomain); + username = null; + } + + val userProvider = identityProviderHelper.findByUserIdentifier(providersService.getProviders(), userDomain); + if (userProvider.isPresent()) { + userProviderId = userProvider.get().getId(); + } + } else if (credential instanceof SurrogateUsernamePasswordCredential) { // login/password + surrogation val surrogationCredential = (SurrogateUsernamePasswordCredential) credential; username = surrogationCredential.getSurrogateUsername(); @@ -120,7 +158,7 @@ public class UserPrincipalResolver implements PrincipalResolver { surrogationCall = true; } else if (credential instanceof UsernamePasswordCredential) { // login/password - username = userId; + username = principalId; superUsername = null; userProviderId = null; technicalUserId = Optional.empty(); @@ -129,12 +167,12 @@ public class UserPrincipalResolver implements PrincipalResolver { // authentication delegation (+ surrogation) val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext); - val webContext = new JEEContext(request, response, sessionStore); + val webContext = new JEEContext(request, response); val clientCredential = (ClientCredential) credential; val providerName = clientCredential.getClientName(); val provider = identityProviderHelper.findByTechnicalName(providersService.getProviders(), providerName).get(); val mailAttribute = provider.getMailAttribute(); - String email = userId; + String email = principalId; if (CommonHelper.isNotBlank(mailAttribute)) { val mails = principal.getAttributes().get(mailAttribute); if (mails == null || mails.size() == 0 || CommonHelper.isBlank((String) mails.get(0))) { @@ -142,13 +180,13 @@ public class UserPrincipalResolver implements PrincipalResolver { return NullPrincipal.getInstance(); } else { val mail = (String) mails.get(0); - LOGGER.info("Provider: '{}' requested specific mail attribute: '{}' for id: '{}' replaced by: '{}'", providerName, mailAttribute, userId, mail); + LOGGER.info("Provider: '{}' requested specific mail attribute: '{}' for id: '{}' replaced by: '{}'", providerName, mailAttribute, principalId, mail); email = mail; } } val identifierAttribute = provider.getIdentifierAttribute(); - String identifier = userId; + String identifier = principalId; if (CommonHelper.isNotBlank(identifierAttribute)) { val identifiers = principal.getAttributes().get(identifierAttribute); if (identifiers == null || identifiers.size() == 0 || CommonHelper.isBlank((String) identifiers.get(0))) { @@ -156,7 +194,7 @@ public class UserPrincipalResolver implements PrincipalResolver { return NullPrincipal.getInstance(); } else { val identifierAttr = (String) identifiers.get(0); - LOGGER.info("Provider: '{}' requested specific identifier attribute: '{}' for id: '{}' replaced by: '{}'", providerName, identifierAttribute, userId, identifierAttr); + LOGGER.info("Provider: '{}' requested specific identifier attribute: '{}' for id: '{}' replaced by: '{}'", providerName, identifierAttribute, principalId, identifierAttr); identifier = identifierAttr; } } @@ -221,10 +259,14 @@ public class UserPrincipalResolver implements PrincipalResolver { attributes.put(ADDRESS_ATTRIBUTE, Collections.singletonList(new CasJsonWrapper(user.getAddress()))); attributes.put(ANALYTICS_ATTRIBUTE, Collections.singletonList(new CasJsonWrapper(user.getAnalytics()))); attributes.put(INTERNAL_CODE, Collections.singletonList(user.getInternalCode())); - attributes.put(INTERNAL_CODE, Collections.singletonList(user.getInternalCode())); + UserDto superUser = null; if (surrogationCall) { attributes.put(SUPER_USER_ATTRIBUTE, Collections.singletonList(superUsername)); - final UserDto superUser = casExternalRestClient.getUser(utils.buildContext(superUsername), superUsername, null, Optional.empty(), Optional.empty()); + superUser = casExternalRestClient.getUser(utils.buildContext(superUsername), superUsername, null, Optional.empty(), Optional.empty()); + if (superUser == null) { + LOGGER.debug("No super user found for: {}", superUsername); + return NullPrincipal.getInstance(); + } attributes.put(SUPER_USER_IDENTIFIER_ATTRIBUTE, Collections.singletonList(superUser.getIdentifier())); attributes.put(SUPER_USER_ID_ATTRIBUTE, Collections.singletonList(superUser.getId())); } @@ -242,13 +284,19 @@ public class UserPrincipalResolver implements PrincipalResolver { profiles.forEach(profile -> profile.getRoles().forEach(role -> roles.add(role.getName()))); attributes.put(ROLES_ATTRIBUTE, new ArrayList(roles)); } - return principalFactory.createPrincipal(user.getId(), attributes); + val createdPrincipal = principalFactory.createPrincipal(user.getId(), attributes); + if (surrogationCall) { + val createdSuperPrincipal = principalFactory.createPrincipal(superUser.getId()); + return new SurrogatePrincipal(createdSuperPrincipal, createdPrincipal); + } else { + return createdPrincipal; + } } @Override public boolean supports(final Credential credential) { return credential instanceof UsernamePasswordCredential || credential instanceof ClientCredential - || credential instanceof SurrogateUsernamePasswordCredential; + || credential instanceof SurrogateUsernamePasswordCredential || credential instanceof X509CertificateCredential; } @Override diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/AppConfig.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/AppConfig.java index ef5aeb42e..cb3898a74 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/AppConfig.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/AppConfig.java @@ -45,6 +45,7 @@ import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.cas.ticket.CustomOAuth20DefaultAccessTokenFactory; import fr.gouv.vitamui.cas.ticket.DynamicTicketGrantingTicketFactory; import fr.gouv.vitamui.cas.util.Utils; +import fr.gouv.vitamui.cas.x509.X509AttributeMapping; import fr.gouv.vitamui.commons.api.identity.ServerIdentityAutoConfiguration; import fr.gouv.vitamui.commons.api.identity.ServerIdentityConfiguration; import fr.gouv.vitamui.commons.api.logger.VitamUILogger; @@ -52,7 +53,7 @@ import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; import fr.gouv.vitamui.commons.security.client.config.password.PasswordConfiguration; import fr.gouv.vitamui.commons.security.client.password.PasswordValidator; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; -import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; +import fr.gouv.vitamui.iam.common.utils.Pac4jClientBuilder; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; import fr.gouv.vitamui.iam.external.client.IamExternalRestClientFactory; import fr.gouv.vitamui.iam.external.client.IdentityProviderExternalRestClient; @@ -68,12 +69,10 @@ import org.apereo.cas.authentication.principal.PrincipalFactory; import org.apereo.cas.authentication.principal.PrincipalResolver; import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService; import org.apereo.cas.configuration.CasConfigurationProperties; +import org.apereo.cas.configuration.support.Beans; import org.apereo.cas.pm.PasswordHistoryService; import org.apereo.cas.pm.PasswordManagementService; import org.apereo.cas.services.ServicesManager; -import org.apereo.cas.support.oauth.authenticator.Authenticators; -import org.apereo.cas.support.oauth.web.OAuth20HandlerInterceptorAdapter; -import org.apereo.cas.support.oauth.web.response.accesstoken.ext.AccessTokenGrantRequestExtractor; import org.apereo.cas.ticket.BaseTicketCatalogConfigurer; import org.apereo.cas.ticket.ExpirationPolicyBuilder; import org.apereo.cas.ticket.TicketCatalog; @@ -85,12 +84,8 @@ import org.apereo.cas.ticket.accesstoken.OAuth20DefaultAccessToken; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.token.JwtBuilder; import org.apereo.cas.util.crypto.CipherExecutor; -import org.pac4j.core.client.Client; -import org.pac4j.core.client.DirectClient; -import org.pac4j.core.config.Config; +import org.pac4j.core.client.Clients; import org.pac4j.core.context.session.SessionStore; -import org.pac4j.core.http.adapter.JEEHttpActionAdapter; -import org.pac4j.springframework.web.SecurityInterceptor; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -104,12 +99,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; +import org.springframework.http.converter.HttpMessageConverter; import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.web.servlet.HandlerInterceptor; - -import java.util.Collection; -import java.util.Objects; -import java.util.stream.Collectors; /** * Configure all beans to customize the CAS server. @@ -191,9 +182,6 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Qualifier("delegatedClientDistributedSessionStore") private SessionStore delegatedClientDistributedSessionStore; - @Autowired - private TicketRegistry ticketRegistry; - @Autowired @Qualifier("centralAuthenticationService") private ObjectProvider<CentralAuthenticationService> centralAuthenticationService; @@ -206,12 +194,12 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Qualifier("passwordHistoryService") private PasswordHistoryService passwordHistoryService; + @Autowired + private PasswordConfiguration passwordConfiguration; + @Value("${token.api.cas}") private String tokenApiCas; - @Value("${api.token.ttl}") - private Integer apiTokenTtl; - @Value("${ip.header}") private String ipHeaderName; @@ -221,44 +209,41 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Value("${vitamui.cas.identity}") private String casIdentity; - @Autowired - @Qualifier("oauthSecConfig") - private ObjectProvider<Config> oauthSecConfig; + @Value("${theme.vitamui-logo-large:#{null}}") + private String vitamuiLargeLogoPath; - @Autowired - @Qualifier("accessTokenGrantRequestExtractors") - private Collection<AccessTokenGrantRequestExtractor> accessTokenGrantRequestExtractors; + @Value("${theme.vitamui-favicon:#{null}}") + private String vitamuiFaviconPath; - @Bean - public SecurityInterceptor requiresAuthenticationAuthorizeInterceptor() { - val interceptor = new SecurityInterceptor(oauthSecConfig.getObject(), Authenticators.CAS_OAUTH_CLIENT, JEEHttpActionAdapter.INSTANCE); - interceptor.setAuthorizers("none"); - return interceptor; - } + @Value("${vitamui.authn.x509.emailAttribute:}") + private String x509EmailAttribute; - @Bean - public PasswordValidator passwordValidator() { - return new PasswordValidator(); - } + @Value("${vitamui.authn.x509.emailAttributeParsing:}") + private String x509EmailAttributeParsing; - @Autowired - private PasswordConfiguration passwordConfiguration; + @Value("${vitamui.authn.x509.emailAttributeExpansion:}") + private String x509EmailAttributeExpansion; - @Bean - public SecurityInterceptor requiresAuthenticationAccessTokenInterceptor() { - val secConfig = oauthSecConfig.getObject(); - val clients = - Objects.requireNonNull(secConfig).getClients().findAllClients().stream().filter(client -> client instanceof DirectClient).map(Client::getName) - .collect(Collectors.joining(",")); - val interceptor = new SecurityInterceptor(oauthSecConfig.getObject(), clients, JEEHttpActionAdapter.INSTANCE); - interceptor.setAuthorizers("none"); - return interceptor; - } + @Value("${vitamui.authn.x509.identifierAttribute:}") + private String x509IdentifierAttribute; + + @Value("${vitamui.authn.x509.identifierAttributeParsing:}") + private String x509IdentifierAttributeParsing; + + @Value("${vitamui.authn.x509.identifierAttributeExpansion:}") + private String x509IdentifierAttributeExpansion; + + @Value("${vitamui.authn.x509.defaultDomain:}") + private String x509DefaultDomain; + + // position matters unfortunately: the ticketRegistry must be autowired after (= under) others + // as it depends on the catalog instantiated above + @Autowired + private TicketRegistry ticketRegistry; @Bean - public HandlerInterceptor oauthHandlerInterceptorAdapter() { - return new OAuth20HandlerInterceptorAdapter(requiresAuthenticationAccessTokenInterceptor(), requiresAuthenticationAuthorizeInterceptor(), - accessTokenGrantRequestExtractors); + public PasswordValidator passwordValidator() { + return new PasswordValidator(); } @Bean @@ -269,8 +254,16 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Bean @RefreshScope public PrincipalResolver surrogatePrincipalResolver() { - return new UserPrincipalResolver(principalFactory, casRestClient(), utils(), delegatedClientDistributedSessionStore, identityProviderHelper(), - providersService()); + val emailMapping = new X509AttributeMapping(x509EmailAttribute, x509EmailAttributeParsing, x509EmailAttributeExpansion); + val identifierMapping = new X509AttributeMapping(x509IdentifierAttribute, x509IdentifierAttributeParsing, x509IdentifierAttributeExpansion); + return new UserPrincipalResolver(principalFactory, casRestClient(), utils(), delegatedClientDistributedSessionStore, + identityProviderHelper(), providersService(), emailMapping, identifierMapping, x509DefaultDomain); + } + + @Bean + @RefreshScope + public PrincipalResolver x509SubjectDNPrincipalResolver() { + return surrogatePrincipalResolver(); } @Bean @@ -292,6 +285,14 @@ public class AppConfig extends BaseTicketCatalogConfigurer { registeredServiceAccessStrategyEnforcer, surrogateEligibilityAuditableExecution, delegatedClientDistributedSessionStore); } + // overrides the CAS specific message converter to prevent + // the CasRestExternalClient to use the 'application/vnd.cas.services+yaml;charset=UTF-8' + // content type and to fail + @Bean + public HttpMessageConverter yamlHttpMessageConverter() { + return null; + } + @Bean public IamExternalRestClientFactory iamRestClientFactory() { LOGGER.debug("Iam client factory: {}", iamClientProperties); @@ -308,14 +309,20 @@ public class AppConfig extends BaseTicketCatalogConfigurer { return iamRestClientFactory().getIdentityProviderExternalRestClient(); } + @RefreshScope + @Bean + public Clients builtClients() { + return new Clients(casProperties.getServer().getLoginUrl()); + } + @Bean public ProvidersService providersService() { - return new ProvidersService(); + return new ProvidersService(builtClients(), identityProviderCrudRestClient(), pac4jClientBuilder(), utils()); } @Bean - public Saml2ClientBuilder saml2ClientBuilder() { - return new Saml2ClientBuilder(); + public Pac4jClientBuilder pac4jClientBuilder() { + return new Pac4jClientBuilder(); } @Bean @@ -331,7 +338,7 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Bean public TicketGrantingTicketFactory defaultTicketGrantingTicketFactory() { return new DynamicTicketGrantingTicketFactory(ticketGrantingTicketUniqueIdGenerator, grantingTicketExpirationPolicy.getObject(), - protocolTicketCipherExecutor); + protocolTicketCipherExecutor, servicesManager, utils()); } @Bean @@ -343,8 +350,10 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Override public void configureTicketCatalog(final TicketCatalog plan) { final TicketDefinition metadata = buildTicketDefinition(plan, "TOK", OAuth20DefaultAccessToken.class, Ordered.HIGHEST_PRECEDENCE); - metadata.getProperties().setStorageName("oauthAccessTokensCache"); - metadata.getProperties().setStorageTimeout(apiTokenTtl); + metadata.getProperties().setStorageName(casProperties.getAuthn().getOauth().getAccessToken().getStorageName()); + val timeout = Beans.newDuration(casProperties.getAuthn().getOauth().getAccessToken().getMaxTimeToLiveInSeconds()).getSeconds(); + metadata.getProperties().setStorageTimeout(timeout); + metadata.getProperties().setExcludeFromCascade(casProperties.getLogout().isRemoveDescendantTickets()); registerTicketDefinition(plan, metadata); } @@ -365,7 +374,7 @@ public class AppConfig extends BaseTicketCatalogConfigurer { @Bean public ServletContextInitializer servletContextInitializer() { - return new InitContextConfiguration(); + return new InitContextConfiguration(vitamuiLargeLogoPath, vitamuiFaviconPath); } @Bean diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/IamClientConfigurationProperties.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/IamClientConfigurationProperties.java index 84fbf421f..4f2a38a0e 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/IamClientConfigurationProperties.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/IamClientConfigurationProperties.java @@ -37,7 +37,6 @@ package fr.gouv.vitamui.cas.config; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; import fr.gouv.vitamui.commons.rest.client.configuration.RestClientConfiguration; @@ -46,7 +45,6 @@ import fr.gouv.vitamui.commons.rest.client.configuration.RestClientConfiguration * * */ -@Component @ConfigurationProperties(value = "iam-client") public class IamClientConfigurationProperties extends RestClientConfiguration { diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitContextConfiguration.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitContextConfiguration.java index 9bbdf4113..245519732 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitContextConfiguration.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitContextConfiguration.java @@ -41,7 +41,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import org.springframework.beans.factory.annotation.Value; +import lombok.RequiredArgsConstructor; import org.springframework.boot.web.servlet.ServletContextInitializer; import javax.servlet.ServletContext; @@ -53,35 +53,20 @@ import fr.gouv.vitamui.commons.api.logger.VitamUILogger; import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; /** - * Custom initial flow action to retrieve pre-filled inputs. + * Custom context initializer to pre-fill logo and favicon. */ +@RequiredArgsConstructor public class InitContextConfiguration implements ServletContextInitializer { private static final VitamUILogger LOGGER = VitamUILoggerFactory.getInstance(InitContextConfiguration.class); - @Value("${theme.vitam-logo:#{null}}") - private String vitamLogoPath; - - @Value("${theme.vitamui-logo-large:#{null}}") - private String vitamuiLargeLogoPath; - - @Value("${theme.vitamui-favicon:#{null}}") - private String vitamuiFaviconPath; + private final String vitamuiLargeLogoPath; + private final String vitamuiFaviconPath; @Override public void onStartup(final ServletContext servletContext) throws ServletException { - if (vitamLogoPath != null) { - try { - final Path logoFile = Paths.get(vitamLogoPath); - final String logo = DatatypeConverter.printBase64Binary(Files.readAllBytes(logoFile)); - servletContext.setAttribute(Constants.VITAM_LOGO, logo); - } catch (final IOException e) { - LOGGER.warn("Can't find vitam logo"); - e.printStackTrace(); - } - } if (vitamuiLargeLogoPath != null) { try { final Path logoFile = Paths.get(vitamuiLargeLogoPath); @@ -105,7 +90,4 @@ public class InitContextConfiguration implements ServletContextInitializer { } } - } - - diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitPasswordConstraintsConfiguration.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitPasswordConstraintsConfiguration.java index bbda59c6f..905ea1417 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitPasswordConstraintsConfiguration.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/InitPasswordConstraintsConfiguration.java @@ -48,7 +48,7 @@ import javax.servlet.ServletException; import java.util.Objects; /** - * Custom flow action for password complexity configuration. + * Custom context initializer for password complexity configuration. */ public class InitPasswordConstraintsConfiguration implements ServletContextInitializer { @@ -126,5 +126,3 @@ public class InitPasswordConstraintsConfiguration implements ServletContextIniti } } } - - diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java index 6e4a9b523..8faab1e71 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java @@ -36,41 +36,38 @@ */ package fr.gouv.vitamui.cas.config; +import fr.gouv.vitamui.cas.webflow.configurer.CustomCasSimpleMultifactorWebflowConfigurer; +import fr.gouv.vitamui.cas.x509.CustomRequestHeaderX509CertificateExtractor; import org.apereo.cas.CentralAuthenticationService; -import org.apereo.cas.audit.AuditableExecution; -import org.apereo.cas.authentication.AuthenticationEventExecutionPlan; -import org.apereo.cas.authentication.AuthenticationServiceSelectionPlan; -import org.apereo.cas.authentication.AuthenticationSystemSupport; import org.apereo.cas.authentication.adaptive.AdaptiveAuthenticationPolicy; import org.apereo.cas.configuration.CasConfigurationProperties; -import org.apereo.cas.mfa.simple.web.flow.CasSimpleMultifactorWebflowConfigurer; +import org.apereo.cas.logout.LogoutManager; +import org.apereo.cas.mfa.simple.CasSimpleMultifactorTokenCommunicationStrategy; +import org.apereo.cas.mfa.simple.ticket.CasSimpleMultifactorAuthenticationTicketFactory; +import org.apereo.cas.notifications.CommunicationsManager; import org.apereo.cas.pm.PasswordManagementService; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.ticket.TransientSessionTicket; -import org.apereo.cas.ticket.TransientSessionTicketFactory; import org.apereo.cas.ticket.factory.DefaultTicketFactory; import org.apereo.cas.ticket.factory.DefaultTransientSessionTicketFactory; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.ticket.registry.TicketRegistrySupport; -import org.apereo.cas.util.CollectionUtils; -import org.apereo.cas.util.io.CommunicationsManager; -import org.apereo.cas.web.DelegatedClientWebflowManager; import org.apereo.cas.web.cookie.CasCookieBuilder; import org.apereo.cas.web.flow.CasWebflowConfigurer; -import org.apereo.cas.web.flow.CasWebflowConstants; -import org.apereo.cas.web.flow.SingleSignOnParticipationStrategy; +import org.apereo.cas.web.flow.DelegatedClientAuthenticationConfigurationContext; +import org.apereo.cas.web.flow.X509CertificateCredentialsRequestHeaderAction; +import org.apereo.cas.web.flow.actions.ConsumerExecutionAction; +import org.apereo.cas.web.flow.actions.StaticEventExecutionAction; import org.apereo.cas.web.flow.resolver.CasDelegatingWebflowEventResolver; import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver; -import org.apereo.cas.web.support.ArgumentExtractor; -import org.pac4j.core.client.Clients; +import org.apereo.cas.web.flow.util.MultifactorAuthenticationWebflowUtils; import org.pac4j.core.context.session.SessionStore; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.HierarchicalMessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -78,11 +75,9 @@ import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Lazy; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.webflow.config.FlowDefinitionRegistryBuilder; import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; import org.springframework.webflow.engine.builder.support.FlowBuilderServices; import org.springframework.webflow.execution.Action; -import org.thymeleaf.spring5.SpringTemplateEngine; import fr.gouv.vitamui.cas.pm.PmTransientSessionTicketExpirationPolicyBuilder; import fr.gouv.vitamui.cas.pm.ResetPasswordController; @@ -91,11 +86,9 @@ import fr.gouv.vitamui.cas.util.Utils; import fr.gouv.vitamui.cas.webflow.actions.CheckMfaTokenAction; import fr.gouv.vitamui.cas.webflow.actions.CustomDelegatedClientAuthenticationAction; import fr.gouv.vitamui.cas.webflow.actions.CustomSendTokenAction; -import fr.gouv.vitamui.cas.webflow.actions.CustomVerifyPasswordResetRequestAction; import fr.gouv.vitamui.cas.webflow.actions.DispatcherAction; import fr.gouv.vitamui.cas.webflow.actions.GeneralTerminateSessionAction; import fr.gouv.vitamui.cas.webflow.actions.I18NSendPasswordResetInstructionsAction; -import fr.gouv.vitamui.cas.webflow.actions.NoOpAction; import fr.gouv.vitamui.cas.webflow.actions.TriggerChangePasswordAction; import fr.gouv.vitamui.cas.webflow.configurer.CustomLoginWebflowConfigurer; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; @@ -108,10 +101,6 @@ import lombok.val; @Configuration public class WebflowConfig { - @Autowired - @Qualifier("servicesManager") - private ObjectProvider<ServicesManager> servicesManager; - @Autowired @Qualifier("ticketGrantingTicketCookieGenerator") private ObjectProvider<CasCookieBuilder> ticketGrantingTicketCookieGenerator; @@ -120,14 +109,6 @@ public class WebflowConfig { @Qualifier("warnCookieGenerator") private ObjectProvider<CasCookieBuilder> warnCookieGenerator; - @Autowired - @Qualifier("authenticationServiceSelectionPlan") - private ObjectProvider<AuthenticationServiceSelectionPlan> authenticationRequestServiceSelectionStrategies; - - @Autowired - @Qualifier("authenticationEventExecutionPlan") - private ObjectProvider<AuthenticationEventExecutionPlan> AuthenticationEventExecutionStrategies; - @Autowired @Qualifier("communicationsManager") private CommunicationsManager communicationsManager; @@ -157,76 +138,70 @@ public class WebflowConfig { private FlowDefinitionRegistry loginFlowDefinitionRegistry; @Autowired - private ApplicationContext applicationContext; + private ConfigurableApplicationContext applicationContext; @Autowired private CasExternalRestClient casRestClient; @Autowired - @Qualifier("initialAuthenticationAttemptWebflowEventResolver") - private ObjectProvider<CasDelegatingWebflowEventResolver> initialAuthenticationAttemptWebflowEventResolver; - - @Autowired - @Qualifier("builtClients") - private ObjectProvider<Clients> builtClients; - - @Autowired - @Qualifier("serviceTicketRequestWebflowEventResolver") - private ObjectProvider<CasWebflowEventResolver> serviceTicketRequestWebflowEventResolver; + private TicketRegistry ticketRegistry; @Autowired - @Qualifier("adaptiveAuthenticationPolicy") - private ObjectProvider<AdaptiveAuthenticationPolicy> adaptiveAuthenticationPolicy; + @Qualifier("centralAuthenticationService") + private ObjectProvider<CentralAuthenticationService> centralAuthenticationService; @Autowired - @Qualifier("registeredServiceDelegatedAuthenticationPolicyAuditableEnforcer") - private ObjectProvider<AuditableExecution> registeredServiceDelegatedAuthenticationPolicyAuditableEnforcer; + @Qualifier("delegatedClientDistributedSessionStore") + private ObjectProvider<SessionStore> delegatedClientDistributedSessionStore; @Autowired - @Qualifier("defaultAuthenticationSystemSupport") - private ObjectProvider<AuthenticationSystemSupport> authenticationSystemSupport; + private Utils utils; @Autowired - private TicketRegistry ticketRegistry; + private TicketRegistrySupport ticketRegistrySupport; @Autowired - @Qualifier("argumentExtractor") - private ObjectProvider<ArgumentExtractor> argumentExtractor; + @Qualifier("messageSource") + private HierarchicalMessageSource messageSource; @Autowired - @Qualifier("centralAuthenticationService") - private ObjectProvider<CentralAuthenticationService> centralAuthenticationService; + @Qualifier("casSimpleMultifactorAuthenticationTicketFactory") + private CasSimpleMultifactorAuthenticationTicketFactory casSimpleMultifactorAuthenticationTicketFactory; @Autowired - @Qualifier("singleSignOnParticipationStrategy") - private ObjectProvider<SingleSignOnParticipationStrategy> webflowSingleSignOnParticipationStrategy; + private LogoutManager logoutManager; @Autowired - @Qualifier("delegatedClientDistributedSessionStore") - private ObjectProvider<SessionStore> delegatedClientDistributedSessionStore; + @Qualifier(DelegatedClientAuthenticationConfigurationContext.DEFAULT_BEAN_NAME) + private DelegatedClientAuthenticationConfigurationContext delegatedClientAuthenticationConfigurationContext; @Autowired - private Utils utils; + @Qualifier("mfaSimpleMultifactorTokenCommunicationStrategy") + private CasSimpleMultifactorTokenCommunicationStrategy mfaSimpleMultifactorTokenCommunicationStrategy; @Autowired - private DelegatedClientWebflowManager delegatedClientWebflowManager; + @Qualifier("mfaSimpleAuthenticatorFlowRegistry") + private FlowDefinitionRegistry mfaSimpleAuthenticatorFlowRegistry; @Autowired - private TicketRegistrySupport ticketRegistrySupport; + @Qualifier("servicesManager") + private ServicesManager servicesManager; @Autowired - @Qualifier("messageSource") - private HierarchicalMessageSource messageSource; + @Qualifier("frontChannelLogoutAction") + private Action frontChannelLogoutAction; @Autowired - private SpringTemplateEngine springTemplateEngine; + @Qualifier("adaptiveAuthenticationPolicy") + private ObjectProvider<AdaptiveAuthenticationPolicy> adaptiveAuthenticationPolicy; @Autowired - private ThymeleafProperties thymeleafProperties; + @Qualifier("serviceTicketRequestWebflowEventResolver") + private ObjectProvider<CasWebflowEventResolver> serviceTicketRequestWebflowEventResolver; @Autowired - @Qualifier("casSimpleMultifactorAuthenticationTicketFactory") - private TransientSessionTicketFactory casSimpleMultifactorAuthenticationTicketFactory; + @Qualifier("initialAuthenticationAttemptWebflowEventResolver") + private ObjectProvider<CasDelegatingWebflowEventResolver> initialAuthenticationAttemptWebflowEventResolver; @Value("${vitamui.portal.url}") private String vitamuiPortalUrl; @@ -234,6 +209,15 @@ public class WebflowConfig { @Value("${cas.authn.surrogate.separator}") private String surrogationSeparator; + @Value("${theme.vitamui-platform-name:VITAM-UI}") + private String vitamuiPlatformName; + + @Value("${vitamui.authn.x509.enabled:false}") + private boolean x509AuthnEnabled; + + @Value("${vitamui.authn.x509.mandatory:false}") + private boolean x509AuthnMandatory; + @Bean public DispatcherAction dispatcherAction() { return new DispatcherAction(providersService, identityProviderHelper, casRestClient, surrogationSeparator, utils, @@ -252,7 +236,7 @@ public class WebflowConfig { pmTicketFactory.addTicketFactory(TransientSessionTicket.class, pmTicketFactory()); return new I18NSendPasswordResetInstructionsAction(casProperties, communicationsManager, passwordManagementService, ticketRegistry, pmTicketFactory, - messageSource, providersService, identityProviderHelper, utils); + messageSource, providersService, identityProviderHelper, utils, vitamuiPlatformName); } @Bean @@ -274,36 +258,17 @@ public class WebflowConfig { @Bean @Lazy public Action delegatedAuthenticationAction() { - return new CustomDelegatedClientAuthenticationAction( - initialAuthenticationAttemptWebflowEventResolver.getObject(), - serviceTicketRequestWebflowEventResolver.getObject(), - adaptiveAuthenticationPolicy.getObject(), - builtClients.getObject(), - servicesManager.getObject(), - registeredServiceDelegatedAuthenticationPolicyAuditableEnforcer.getObject(), - delegatedClientWebflowManager, - authenticationSystemSupport.getObject(), - casProperties, - authenticationRequestServiceSelectionStrategies.getObject(), - centralAuthenticationService.getObject(), - webflowSingleSignOnParticipationStrategy.getObject(), - delegatedClientDistributedSessionStore.getObject(), - CollectionUtils.wrap(argumentExtractor.getObject()), - identityProviderHelper, - providersService, - utils, - ticketRegistry, - vitamuiPortalUrl, - surrogationSeparator); + return new CustomDelegatedClientAuthenticationAction(delegatedClientAuthenticationConfigurationContext, identityProviderHelper, + providersService, utils, ticketRegistry, vitamuiPortalUrl, surrogationSeparator); } @Bean @RefreshScope public Action terminateSessionAction() { return new GeneralTerminateSessionAction(centralAuthenticationService.getObject(), - ticketGrantingTicketCookieGenerator.getObject(), - warnCookieGenerator.getObject(), - casProperties.getLogout()); + ticketGrantingTicketCookieGenerator.getObject(), warnCookieGenerator.getObject(), + logoutManager, applicationContext, utils, casRestClient, servicesManager, casProperties, + frontChannelLogoutAction); } @Bean @@ -314,30 +279,25 @@ public class WebflowConfig { @Bean public Action loadSurrogatesListAction() { - return new NoOpAction("success"); + return StaticEventExecutionAction.SUCCESS; } @Bean @RefreshScope public Action mfaSimpleMultifactorSendTokenAction() { val simple = casProperties.getAuthn().getMfa().getSimple(); - return new CustomSendTokenAction(ticketRegistry, communicationsManager, - casSimpleMultifactorAuthenticationTicketFactory, simple, utils); - } - - @Bean - public FlowDefinitionRegistry customMfaSimpleAuthenticatorFlowRegistry() { - val builder = new FlowDefinitionRegistryBuilder(this.applicationContext, this.flowBuilderServices); - builder.setBasePath(CasWebflowConstants.BASE_CLASSPATH_WEBFLOW); - builder.addFlowLocationPattern("/mfa-simple/mfa-simple-custom-webflow.xml"); - return builder.build(); + return new CustomSendTokenAction(ticketRegistry, communicationsManager, casSimpleMultifactorAuthenticationTicketFactory, + simple, mfaSimpleMultifactorTokenCommunicationStrategy, utils); } @Bean @DependsOn("defaultWebflowConfigurer") public CasWebflowConfigurer mfaSimpleMultifactorWebflowConfigurer() { - return new CasSimpleMultifactorWebflowConfigurer(flowBuilderServices, loginFlowDefinitionRegistry, - customMfaSimpleAuthenticatorFlowRegistry(), applicationContext, casProperties); + val cfg = new CustomCasSimpleMultifactorWebflowConfigurer(flowBuilderServices, + loginFlowDefinitionRegistry, mfaSimpleAuthenticatorFlowRegistry, applicationContext, casProperties, + MultifactorAuthenticationWebflowUtils.getMultifactorAuthenticationWebflowCustomizers(applicationContext)); + cfg.setOrder(100); + return cfg; } @Bean @@ -349,13 +309,28 @@ public class WebflowConfig { @Lazy @RefreshScope public Action delegatedAuthenticationClientLogoutAction() { - return new NoOpAction(null); + return new ConsumerExecutionAction(ctx -> {}); } @Bean @RefreshScope - public Action verifyPasswordResetRequestAction() { - return new CustomVerifyPasswordResetRequestAction(casProperties, passwordManagementService, centralAuthenticationService.getObject()); + public Action delegatedAuthenticationClientFinishLogoutAction() { + return new ConsumerExecutionAction(ctx -> {}); } + @Bean + @RefreshScope + public Action x509Check() { + if (x509AuthnEnabled) { + val sslHeaderName = casProperties.getAuthn().getX509().getSslHeaderName(); + val certificateExtractor = new CustomRequestHeaderX509CertificateExtractor(sslHeaderName, x509AuthnMandatory); + + return new X509CertificateCredentialsRequestHeaderAction(initialAuthenticationAttemptWebflowEventResolver.getObject(), + serviceTicketRequestWebflowEventResolver.getObject(), + adaptiveAuthenticationPolicy.getObject(), + certificateExtractor, casProperties); + } else { + return new StaticEventExecutionAction("error"); + } + } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementService.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementService.java index 8c786840d..c2b5c2d2d 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementService.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementService.java @@ -55,10 +55,7 @@ import org.apereo.cas.authentication.Credential; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService; import org.apereo.cas.configuration.model.support.pm.PasswordManagementProperties; -import org.apereo.cas.pm.BasePasswordManagementService; -import org.apereo.cas.pm.InvalidPasswordException; -import org.apereo.cas.pm.PasswordChangeRequest; -import org.apereo.cas.pm.PasswordHistoryService; +import org.apereo.cas.pm.*; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.util.crypto.CipherExecutor; import org.apereo.cas.web.support.WebUtils; @@ -129,8 +126,10 @@ public class IamPasswordManagementService extends BasePasswordManagementService val requestContext = RequestContextHolder.getRequestContext(); val authentication = WebUtils.getAuthentication(requestContext); if (authentication != null) { + // login/pwd subrogation String superUsername = (String) utils.getAttributeValue(authentication.getAttributes(), SurrogateAuthenticationService.AUTHENTICATION_ATTR_SURROGATE_PRINCIPAL); if (superUsername == null) { + // authn delegation subrogation superUsername = (String) utils.getAttributeValue(authentication.getPrincipal().getAttributes(), SUPER_USER_ATTRIBUTE); } LOGGER.debug("is it currently a superUser: {}", superUsername); @@ -152,7 +151,7 @@ public class IamPasswordManagementService extends BasePasswordManagementService throw new PasswordConfirmException(); } - if (!passwordValidator.isValid(getProperties().getPolicyPattern(), bean.getPassword())) { + if (!passwordValidator.isValid(getProperties().getCore().getPolicyPattern(), bean.getPassword())) { throw new PasswordNotMatchRegexException(); } @@ -194,7 +193,8 @@ public class IamPasswordManagementService extends BasePasswordManagementService } @Override - public String findEmail(final String username) { + public String findEmail(final PasswordManagementQuery query) { + val username = query.getUsername(); String email = null; val usernameWithLowercase = username.toLowerCase().trim(); try { @@ -210,7 +210,7 @@ public class IamPasswordManagementService extends BasePasswordManagementService } @Override - public Map<String, String> getSecurityQuestions(final String username) { + public Map<String, String> getSecurityQuestions(final PasswordManagementQuery query) { throw new UnsupportedOperationException("security questions/answers are not available"); } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmMessageToSend.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmMessageToSend.java index d9f5df004..54290cd4e 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmMessageToSend.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmMessageToSend.java @@ -38,7 +38,6 @@ package fr.gouv.vitamui.cas.pm; import java.util.Locale; -import org.apache.commons.lang3.StringUtils; import org.springframework.context.HierarchicalMessageSource; import lombok.EqualsAndHashCode; diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmTransientSessionTicketExpirationPolicyBuilder.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmTransientSessionTicketExpirationPolicyBuilder.java index 49e07c48a..2104ba191 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmTransientSessionTicketExpirationPolicyBuilder.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmTransientSessionTicketExpirationPolicyBuilder.java @@ -63,7 +63,7 @@ public class PmTransientSessionTicketExpirationPolicyBuilder extends TransientSe val attributes = RequestContextHolder.getRequestAttributes(); if (attributes != null) { try { - val expInMinutes = (Integer) attributes.getAttribute(PM_EXPIRATION_IN_MINUTES_ATTRIBUTE, 0); + val expInMinutes = (Long) attributes.getAttribute(PM_EXPIRATION_IN_MINUTES_ATTRIBUTE, 0); if (expInMinutes != null) { return new HardTimeoutExpirationPolicy(expInMinutes * 60); } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/ResetPasswordController.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/ResetPasswordController.java index 46a02a858..57aef3f8c 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/ResetPasswordController.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/ResetPasswordController.java @@ -44,12 +44,13 @@ import lombok.val; import org.apache.commons.lang3.StringUtils; import org.apereo.cas.authentication.principal.Service; import org.apereo.cas.configuration.CasConfigurationProperties; +import org.apereo.cas.notifications.CommunicationsManager; +import org.apereo.cas.pm.PasswordManagementQuery; import org.apereo.cas.pm.PasswordManagementService; import org.apereo.cas.pm.web.flow.PasswordManagementWebflowUtils; import org.apereo.cas.ticket.factory.DefaultTransientSessionTicketFactory; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.util.CollectionUtils; -import org.apereo.cas.util.io.CommunicationsManager; import org.apereo.cas.web.flow.CasWebflowConfigurer; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.HierarchicalMessageSource; @@ -105,23 +106,23 @@ public class ResetPasswordController { return false; } - communicationsManager.validate(); if (!communicationsManager.isMailSenderDefined()) { LOGGER.warn("CAS is unable to send password-reset emails given no settings are defined to account for email servers"); return false; } val usernameLower = username.toLowerCase().trim(); - val email = passwordManagementService.findEmail(usernameLower); + val query = PasswordManagementQuery.builder().username(usernameLower).build(); + val email = passwordManagementService.findEmail(query); if (StringUtils.isBlank(email)) { LOGGER.warn("No recipient is provided"); return false; } - final String url = buildPasswordResetUrl(usernameLower, casProperties); final Locale locale = new Locale(language); final long expMinutes = PmMessageToSend.ONE_DAY.equals(ttl) ? 24 * 60L : casProperties.getAuthn().getPm().getReset().getExpirationMinutes(); - final PmMessageToSend messageToSend = PmMessageToSend.buildMessage(messageSource, firstname, lastname, String.valueOf(expMinutes), url, vitamuiPlatformName, locale); request.setAttribute(PmTransientSessionTicketExpirationPolicyBuilder.PM_EXPIRATION_IN_MINUTES_ATTRIBUTE, expMinutes); + final String url = buildPasswordResetUrl(usernameLower, casProperties); + final PmMessageToSend messageToSend = PmMessageToSend.buildMessage(messageSource, firstname, lastname, String.valueOf(expMinutes), url, vitamuiPlatformName, locale); LOGGER.debug("Generated password reset URL [{}] for: {} ({}); Link is only active for the next [{}] minute(s)", utils.sanitizePasswordResetUrl(url), email, messageToSend.getSubject(), expMinutes); @@ -130,7 +131,8 @@ public class ResetPasswordController { } protected String buildPasswordResetUrl(final String username, final CasConfigurationProperties casProperties) { - val token = passwordManagementService.createToken(username); + val query = PasswordManagementQuery.builder().username(username).build(); + val token = passwordManagementService.createToken(query); val properties = CollectionUtils.<String, Serializable>wrap(PasswordManagementWebflowUtils.FLOWSCOPE_PARAMETER_NAME_TOKEN, token); val ticket = pmTicketFactory.create((Service) null, properties); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/SamlIdentityProviderDto.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/Pac4jClientIdentityProviderDto.java similarity index 74% rename from cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/SamlIdentityProviderDto.java rename to cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/Pac4jClientIdentityProviderDto.java index de43a123f..7828f2029 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/SamlIdentityProviderDto.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/Pac4jClientIdentityProviderDto.java @@ -37,37 +37,51 @@ package fr.gouv.vitamui.cas.provider; import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; -import org.pac4j.saml.client.SAML2Client; +import org.pac4j.core.client.IndirectClient; /** - * SAML identity provider. + * Pac4j client identity provider. * * */ -public class SamlIdentityProviderDto extends IdentityProviderDto { +public class Pac4jClientIdentityProviderDto extends IdentityProviderDto { - private final SAML2Client saml2Client; + private final IndirectClient client; - public SamlIdentityProviderDto(final IdentityProviderDto dto, final SAML2Client saml2Client) { + public Pac4jClientIdentityProviderDto(final IdentityProviderDto dto, final IndirectClient client) { setId(dto.getId()); setName(dto.getName()); setTechnicalName(dto.getTechnicalName()); setInternal(dto.getInternal()); + setEnabled(dto.getEnabled()); setPatterns(dto.getPatterns()); + setReadonly(dto.isReadonly()); + + setMailAttribute(dto.getMailAttribute()); + setIdentifierAttribute(dto.getIdentifierAttribute()); + setAutoProvisioningEnabled(dto.isAutoProvisioningEnabled()); + setKeystoreBase64(dto.getKeystoreBase64()); setKeystorePassword(dto.getKeystorePassword()); setPrivateKeyPassword(dto.getPrivateKeyPassword()); setIdpMetadata(dto.getIdpMetadata()); setSpMetadata(dto.getSpMetadata()); - setPatterns(dto.getPatterns()); setMaximumAuthenticationLifetime(dto.getMaximumAuthenticationLifetime()); - setMailAttribute(dto.getMailAttribute()); - setIdentifierAttribute(dto.getIdentifierAttribute()); setAuthnRequestBinding(dto.getAuthnRequestBinding()); - this.saml2Client = saml2Client; + + setClientId(dto.getClientId()); + setClientSecret(dto.getClientSecret()); + setDiscoveryUrl(dto.getDiscoveryUrl()); + setScope(dto.getScope()); + setPreferredJwsAlgorithm(dto.getPreferredJwsAlgorithm()); + setCustomParams(dto.getCustomParams()); + setUseState(dto.getUseState()); + setUseNonce(dto.getUseNonce()); + setUsePkce(dto.getUsePkce()); + this.client = client; } - public SAML2Client getSaml2Client() { - return saml2Client; + public IndirectClient getClient() { + return client; } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/ProvidersService.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/ProvidersService.java index 3bb2cc36f..07ae84412 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/ProvidersService.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/provider/ProvidersService.java @@ -36,20 +36,16 @@ */ package fr.gouv.vitamui.cas.provider; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.pac4j.core.client.Client; import org.pac4j.core.client.Clients; -import org.pac4j.saml.client.SAML2Client; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; +import org.pac4j.core.client.IndirectClient; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.util.Assert; @@ -58,10 +54,9 @@ import fr.gouv.vitamui.commons.api.logger.VitamUILogger; import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; import fr.gouv.vitamui.iam.common.dto.common.ProviderEmbeddedOptions; -import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; +import fr.gouv.vitamui.iam.common.utils.Pac4jClientBuilder; import fr.gouv.vitamui.iam.external.client.IdentityProviderExternalRestClient; import lombok.Getter; -import lombok.Setter; /** * Retrieve all the identity providers from the IAM API. @@ -69,29 +64,20 @@ import lombok.Setter; * */ @Getter -@Setter +@RequiredArgsConstructor public class ProvidersService { private static final VitamUILogger LOGGER = VitamUILoggerFactory.getInstance(ProvidersService.class); private List<IdentityProviderDto> providers = new ArrayList<>(); - @Autowired - @Qualifier("builtClients") - private Clients clients; + private final Clients clients; - @Autowired - private IdentityProviderExternalRestClient identityProviderExternalRestClient; + private final IdentityProviderExternalRestClient identityProviderExternalRestClient; - @Autowired - private Saml2ClientBuilder saml2ClientBuilder; + private final Pac4jClientBuilder pac4jClientBuilder; - @Autowired - private Utils utils; - - public ProvidersService() { - // do nothing - } + private final Utils utils; @PostConstruct public void afterPropertiesSet() { @@ -114,18 +100,18 @@ public class ProvidersService { final List<IdentityProviderDto> temporaryProviders = identityProviderExternalRestClient.getAll(utils.buildContext(null), Optional.empty(), Optional.of(ProviderEmbeddedOptions.KEYSTORE + "," + ProviderEmbeddedOptions.IDPMETADATA)); // sort by identifier. This is needed in order to take the internal provider first. - Collections.sort(temporaryProviders, (provider1, provider2) -> provider1.getIdentifier().compareTo(provider2.getIdentifier())); + Collections.sort(temporaryProviders, Comparator.comparing(IdentityProviderDto::getIdentifier)); LOGGER.debug("Reloaded {} providers: {}", temporaryProviders.size(), StringUtils.join(temporaryProviders.stream().map(IdentityProviderDto::getId).collect(Collectors.toList()), ", ")); final List<Client> newClients = new ArrayList<>(); final List<IdentityProviderDto> newProviders = new ArrayList<>(); temporaryProviders.forEach(p -> { - final SAML2Client saml2Client = saml2ClientBuilder.buildSaml2Client(p).orElse(null); - if (saml2Client != null) { - newClients.add(saml2Client); + final IndirectClient client = pac4jClientBuilder.buildClient(p).orElse(null); + if (client != null) { + newClients.add(client); } - newProviders.add(new SamlIdentityProviderDto(p, saml2Client)); + newProviders.add(new Pac4jClientIdentityProviderDto(p, client)); }); clients.setClients(newClients); providers = newProviders; diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/CustomOAuth20DefaultAccessTokenFactory.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/CustomOAuth20DefaultAccessTokenFactory.java index 3416cc57b..e753dcf7f 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/CustomOAuth20DefaultAccessTokenFactory.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/CustomOAuth20DefaultAccessTokenFactory.java @@ -40,25 +40,17 @@ import fr.gouv.vitamui.commons.api.CommonConstants; import org.apereo.cas.authentication.Authentication; import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.authentication.principal.Service; -import org.apereo.cas.configuration.support.Beans; import org.apereo.cas.services.ServicesManager; -import org.apereo.cas.support.oauth.services.OAuthRegisteredService; +import org.apereo.cas.support.oauth.OAuth20GrantTypes; +import org.apereo.cas.support.oauth.OAuth20ResponseTypes; import org.apereo.cas.support.oauth.util.OAuth20Utils; -import org.apereo.cas.ticket.ExpirationPolicy; import org.apereo.cas.ticket.ExpirationPolicyBuilder; -import org.apereo.cas.ticket.Ticket; -import org.apereo.cas.ticket.TicketFactory; import org.apereo.cas.ticket.TicketGrantingTicket; -import org.apereo.cas.ticket.accesstoken.OAuth20AccessToken; -import org.apereo.cas.ticket.accesstoken.OAuth20AccessTokenExpirationPolicy; -import org.apereo.cas.ticket.accesstoken.OAuth20AccessTokenFactory; -import org.apereo.cas.ticket.accesstoken.OAuth20DefaultAccessToken; +import org.apereo.cas.ticket.accesstoken.*; import org.apereo.cas.token.JwtBuilder; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.val; -import org.apache.commons.lang3.StringUtils; import java.util.Collection; import java.util.List; @@ -69,55 +61,39 @@ import java.util.Map; * * */ -@RequiredArgsConstructor @Getter -public class CustomOAuth20DefaultAccessTokenFactory implements OAuth20AccessTokenFactory { +public class CustomOAuth20DefaultAccessTokenFactory extends OAuth20DefaultAccessTokenFactory { - /** - * ExpirationPolicy for refresh tokens. - */ - protected final ExpirationPolicyBuilder<OAuth20AccessToken> expirationPolicy; - - /** - * JWT builder instance. - */ - protected final JwtBuilder jwtBuilder; - - /** - * Services manager. - */ - protected final ServicesManager servicesManager; + public CustomOAuth20DefaultAccessTokenFactory(final ExpirationPolicyBuilder<OAuth20AccessToken> expirationPolicy, + final JwtBuilder jwtBuilder, + final ServicesManager servicesManager) { + super(expirationPolicy, jwtBuilder, servicesManager); + } @Override - public OAuth20AccessToken create(final Service service, final Authentication authentication, + public OAuth20AccessToken create(final Service service, + final Authentication authentication, final TicketGrantingTicket ticketGrantingTicket, - final Collection<String> scopes, final String clientId, - final Map<String, Map<String, Object>> requestClaims) { + final Collection<String> scopes, + final String token, + final String clientId, + final Map<String, Map<String, Object>> requestClaims, + final OAuth20ResponseTypes responseType, + final OAuth20GrantTypes grantType) { val registeredService = OAuth20Utils.getRegisteredOAuthServiceByClientId(jwtBuilder.getServicesManager(), clientId); val expirationPolicyToUse = determineExpirationPolicyForService(registeredService); + // CUSTO: don't generate the identifier, but use the token of the principal val accessTokenId = generateAccessTokenId(authentication); val at = new OAuth20DefaultAccessToken(accessTokenId, service, authentication, - expirationPolicyToUse, ticketGrantingTicket, scopes, - clientId, requestClaims); + expirationPolicyToUse, ticketGrantingTicket, token, scopes, + clientId, requestClaims, responseType, grantType); if (ticketGrantingTicket != null) { ticketGrantingTicket.getDescendantTickets().add(at.getId()); } return at; } - @Override - public OAuth20AccessToken create(final Service service, final Authentication authentication, - final Collection<String> scopes, final String clientId, - final Map<String, Map<String, Object>> requestClaims) { - val accessTokenId = generateAccessTokenId(authentication); - val registeredService = OAuth20Utils.getRegisteredOAuthServiceByClientId(jwtBuilder.getServicesManager(), clientId); - val expirationPolicyToUse = determineExpirationPolicyForService(registeredService); - return new OAuth20DefaultAccessToken(accessTokenId, service, authentication, - expirationPolicyToUse, null, - scopes, clientId, requestClaims); - } - private String generateAccessTokenId(final Authentication authentication) { final Principal principal = authentication.getPrincipal(); final List<Object> authToken = principal.getAttributes().get(CommonConstants.AUTHTOKEN_ATTRIBUTE); @@ -126,23 +102,4 @@ public class CustomOAuth20DefaultAccessTokenFactory implements OAuth20AccessToke } return (String) authToken.get(0); } - - @Override - public TicketFactory get(final Class<? extends Ticket> clazz) { - return this; - } - - private ExpirationPolicy determineExpirationPolicyForService(final OAuthRegisteredService registeredService) { - if (registeredService != null && registeredService.getAccessTokenExpirationPolicy() != null) { - val policy = registeredService.getAccessTokenExpirationPolicy(); - val maxTime = policy.getMaxTimeToLive(); - val ttl = policy.getTimeToKill(); - if (StringUtils.isNotBlank(maxTime) && StringUtils.isNotBlank(ttl)) { - return new OAuth20AccessTokenExpirationPolicy( - Beans.newDuration(maxTime).getSeconds(), - Beans.newDuration(ttl).getSeconds()); - } - } - return this.expirationPolicy.buildTicketExpirationPolicy(); - } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/DynamicTicketGrantingTicketFactory.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/DynamicTicketGrantingTicketFactory.java index 789a542a0..88c6a6fb3 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/DynamicTicketGrantingTicketFactory.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/ticket/DynamicTicketGrantingTicketFactory.java @@ -40,11 +40,12 @@ import fr.gouv.vitamui.cas.util.Utils; import fr.gouv.vitamui.commons.api.enums.UserTypeEnum; import org.apereo.cas.authentication.Authentication; import org.apereo.cas.authentication.principal.Principal; +import org.apereo.cas.authentication.principal.Service; +import org.apereo.cas.services.ServicesManager; import org.apereo.cas.ticket.*; import org.apereo.cas.ticket.expiration.HardTimeoutExpirationPolicy; import org.apereo.cas.ticket.factory.DefaultTicketGrantingTicketFactory; import org.apereo.cas.util.crypto.CipherExecutor; -import org.springframework.beans.factory.annotation.Autowired; import java.io.Serializable; import java.util.List; @@ -59,18 +60,20 @@ import static fr.gouv.vitamui.commons.api.CommonConstants.*; */ public class DynamicTicketGrantingTicketFactory extends DefaultTicketGrantingTicketFactory { - @Autowired - private Utils utils; + private final Utils utils; public DynamicTicketGrantingTicketFactory(final UniqueTicketIdGenerator ticketGrantingTicketUniqueTicketIdGenerator, final ExpirationPolicyBuilder<TicketGrantingTicket> ticketGrantingTicketExpirationPolicy, - final CipherExecutor<Serializable, String> cipherExecutor) { - super(ticketGrantingTicketUniqueTicketIdGenerator, ticketGrantingTicketExpirationPolicy, cipherExecutor); + final CipherExecutor<Serializable, String> cipherExecutor, + final ServicesManager servicesManager, + final Utils utils) { + super(ticketGrantingTicketUniqueTicketIdGenerator, ticketGrantingTicketExpirationPolicy, cipherExecutor, servicesManager); + this.utils = utils; } @Override protected <T extends TicketGrantingTicket> T produceTicket(final Authentication authentication, - final String tgtId, final Class<T> clazz) { + final String tgtId, final Service service, final Class<T> clazz) { final Principal principal = authentication.getPrincipal(); final Map<String, List<Object>> attributes = principal.getAttributes(); final String superUser = (String) utils.getAttributeValue(attributes, SUPER_USER_ATTRIBUTE); @@ -79,7 +82,7 @@ public class DynamicTicketGrantingTicketFactory extends DefaultTicketGrantingTic return (T) new TicketGrantingTicketImpl( tgtId, authentication, new HardTimeoutExpirationPolicy(170 * 60)); } else { - return super.produceTicket(authentication, tgtId, clazz); + return super.produceTicket(authentication, tgtId, service, clazz); } } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Constants.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Constants.java index 1595e8389..866985641 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Constants.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Constants.java @@ -53,8 +53,6 @@ public abstract class Constants { // web: public static final String PORTAL_URL = "portalUrl"; - public static final String VITAM_LOGO = "vitamLogo"; - public static final String VITAM_UI_LARGE_LOGO = "vitamuiLargeLogo"; public static final String VITAM_UI_FAVICON = "vitamuiFavicon"; diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Utils.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Utils.java index bb0c3d9e4..65b900ed3 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Utils.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/Utils.java @@ -46,9 +46,9 @@ import org.apereo.cas.CasProtocolConstants; import org.apereo.cas.configuration.model.support.cookie.TicketGrantingCookieProperties; import org.apereo.cas.web.flow.CasWebflowConstants; import org.apereo.cas.web.support.WebUtils; +import org.pac4j.core.client.IndirectClient; import org.pac4j.core.util.Pac4jConstants; import org.pac4j.core.util.CommonHelper; -import org.pac4j.saml.client.SAML2Client; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.webflow.context.ExternalContext; @@ -92,7 +92,7 @@ public class Utils { return new ExternalHttpContext(casTenantIdentifier, casToken, "cas+" + username, casIdentity); } - public Event performClientRedirection(final Action action, final SAML2Client client, final RequestContext requestContext) throws IOException { + public Event performClientRedirection(final Action action, final IndirectClient client, final RequestContext requestContext) throws IOException { final HttpServletResponse response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext); val service = WebUtils.getService(requestContext); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenAction.java index 508107480..b306699f6 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenAction.java @@ -39,9 +39,8 @@ package fr.gouv.vitamui.cas.webflow.actions; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import lombok.val; -import org.apereo.cas.mfa.simple.CasSimpleMultifactorAuthenticationTicketFactory; import org.apereo.cas.mfa.simple.CasSimpleMultifactorTokenCredential; -import org.apereo.cas.ticket.TransientSessionTicket; +import org.apereo.cas.mfa.simple.ticket.CasSimpleMultifactorAuthenticationTicket; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.web.support.WebUtils; import org.springframework.webflow.action.AbstractAction; @@ -64,11 +63,11 @@ public class CheckMfaTokenAction extends AbstractAction { protected Event doExecute(final RequestContext requestContext) { val credential = WebUtils.getCredential(requestContext); val tokenCredential = (CasSimpleMultifactorTokenCredential) credential; - val token = CasSimpleMultifactorAuthenticationTicketFactory.PREFIX + "-" + tokenCredential.getToken(); + val token = CasSimpleMultifactorAuthenticationTicket.PREFIX + "-" + tokenCredential.getToken(); LOGGER.debug("Checking token: {}", token); WebUtils.putCredential(requestContext, new CasSimpleMultifactorTokenCredential(token)); - val acct = this.ticketRegistry.getTicket(token, TransientSessionTicket.class); + val acct = this.ticketRegistry.getTicket(token, CasSimpleMultifactorAuthenticationTicket.class); if (acct != null) { val creationTime = acct.getCreationTime(); val now_less_one_minute = ZonedDateTime.now().minus(60, ChronoUnit.SECONDS); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationAction.java index a9c876d6a..59af1cb11 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationAction.java @@ -36,7 +36,7 @@ */ package fr.gouv.vitamui.cas.webflow.actions; -import fr.gouv.vitamui.cas.provider.SamlIdentityProviderDto; +import fr.gouv.vitamui.cas.provider.Pac4jClientIdentityProviderDto; import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.cas.util.Constants; import fr.gouv.vitamui.cas.util.Utils; @@ -44,31 +44,16 @@ import fr.gouv.vitamui.commons.api.logger.VitamUILogger; import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; import org.apache.commons.lang3.StringUtils; -import org.apereo.cas.CentralAuthenticationService; -import org.apereo.cas.audit.AuditableExecution; -import org.apereo.cas.authentication.AuthenticationServiceSelectionPlan; -import org.apereo.cas.authentication.AuthenticationSystemSupport; -import org.apereo.cas.authentication.adaptive.AdaptiveAuthenticationPolicy; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; -import org.apereo.cas.configuration.CasConfigurationProperties; -import org.apereo.cas.services.ServicesManager; import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.ticket.registry.TicketRegistry; -import org.apereo.cas.web.DelegatedClientWebflowManager; import org.apereo.cas.web.flow.DelegatedClientAuthenticationAction; -import org.apereo.cas.web.flow.SingleSignOnParticipationStrategy; -import org.apereo.cas.web.flow.resolver.CasDelegatingWebflowEventResolver; -import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver; -import org.apereo.cas.web.support.ArgumentExtractor; +import org.apereo.cas.web.flow.DelegatedClientAuthenticationConfigurationContext; import org.apereo.cas.web.support.WebUtils; -import org.pac4j.core.client.Clients; -import org.pac4j.core.context.JEEContext; -import org.pac4j.core.context.session.SessionStore; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import java.io.IOException; -import java.util.List; import lombok.val; @@ -88,8 +73,6 @@ public class CustomDelegatedClientAuthenticationAction extends DelegatedClientAu private final ProvidersService providersService; - private final CasConfigurationProperties casProperties; - private final Utils utils; private final TicketRegistry ticketRegistry; @@ -98,32 +81,15 @@ public class CustomDelegatedClientAuthenticationAction extends DelegatedClientAu private final String surrogationSeparator; - public CustomDelegatedClientAuthenticationAction(final CasDelegatingWebflowEventResolver initialAuthenticationAttemptWebflowEventResolver, - final CasWebflowEventResolver serviceTicketRequestWebflowEventResolver, - final AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy, - final Clients clients, - final ServicesManager servicesManager, - final AuditableExecution delegatedAuthenticationPolicyEnforcer, - final DelegatedClientWebflowManager delegatedClientWebflowManager, - final AuthenticationSystemSupport authenticationSystemSupport, - final CasConfigurationProperties casProperties, - final AuthenticationServiceSelectionPlan authenticationRequestServiceSelectionStrategies, - final CentralAuthenticationService centralAuthenticationService, - final SingleSignOnParticipationStrategy singleSignOnParticipationStrategy, - final SessionStore<JEEContext> sessionStore, - final List<ArgumentExtractor> argumentExtractors, + public CustomDelegatedClientAuthenticationAction(final DelegatedClientAuthenticationConfigurationContext context, final IdentityProviderHelper identityProviderHelper, final ProvidersService providersService, final Utils utils, final TicketRegistry ticketRegistry, final String vitamuiPortalUrl, final String surrogationSeparator) { - super(initialAuthenticationAttemptWebflowEventResolver, serviceTicketRequestWebflowEventResolver, adaptiveAuthenticationPolicy, - clients, servicesManager, delegatedAuthenticationPolicyEnforcer, delegatedClientWebflowManager, authenticationSystemSupport, - casProperties, authenticationRequestServiceSelectionStrategies, centralAuthenticationService, singleSignOnParticipationStrategy, - sessionStore, argumentExtractors); + super(context); this.identityProviderHelper = identityProviderHelper; - this.casProperties = casProperties; this.providersService = providersService; this.utils = utils; this.ticketRegistry = ticketRegistry; @@ -185,8 +151,8 @@ public class CustomDelegatedClientAuthenticationAction extends DelegatedClientAu val optProvider = identityProviderHelper.findByTechnicalName(providersService.getProviders(), idp); if (optProvider.isPresent()) { val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context); - response.addCookie(utils.buildIdpCookie(idp, casProperties.getTgc())); - val client = ((SamlIdentityProviderDto) optProvider.get()).getSaml2Client(); + response.addCookie(utils.buildIdpCookie(idp, configContext.getCasProperties().getTgc())); + val client = ((Pac4jClientIdentityProviderDto) optProvider.get()).getClient(); LOGGER.debug("Force redirect to the SAML IdP: {}", client.getName()); try { return utils.performClientRedirection(this, client, context); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomSendTokenAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomSendTokenAction.java index a2c46c797..8353e4426 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomSendTokenAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomSendTokenAction.java @@ -37,78 +37,83 @@ package fr.gouv.vitamui.cas.webflow.actions; import fr.gouv.vitamui.cas.util.Utils; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.apache.commons.lang3.StringUtils; import org.apereo.cas.authentication.principal.Principal; -import org.apereo.cas.configuration.model.support.mfa.CasSimpleMultifactorProperties; -import org.apereo.cas.mfa.simple.CasSimpleMultifactorAuthenticationHandler; -import org.apereo.cas.mfa.simple.CasSimpleMultifactorAuthenticationTicketFactory; +import org.apereo.cas.configuration.model.support.mfa.CasSimpleMultifactorAuthenticationProperties; +import org.apereo.cas.mfa.simple.CasSimpleMultifactorTokenCommunicationStrategy; +import org.apereo.cas.mfa.simple.ticket.CasSimpleMultifactorAuthenticationTicket; +import org.apereo.cas.mfa.simple.ticket.CasSimpleMultifactorAuthenticationTicketFactory; +import org.apereo.cas.mfa.simple.web.flow.CasSimpleMultifactorSendTokenAction; +import org.apereo.cas.notifications.CommunicationsManager; import org.apereo.cas.ticket.Ticket; -import org.apereo.cas.ticket.TransientSessionTicketFactory; import org.apereo.cas.ticket.registry.TicketRegistry; -import org.apereo.cas.util.CollectionUtils; -import org.apereo.cas.util.io.CommunicationsManager; import org.apereo.cas.web.flow.CasWebflowConstants; import org.apereo.cas.web.support.WebUtils; -import org.springframework.binding.message.MessageBuilder; -import org.springframework.webflow.action.AbstractAction; -import org.springframework.webflow.action.EventFactorySupport; -import org.springframework.webflow.core.collection.LocalAttributeMap; + +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.commons.lang3.StringUtils; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; +/** + * The custom action to send SMS for the MFA simple token. + */ @Slf4j -@RequiredArgsConstructor -public class CustomSendTokenAction extends AbstractAction { - private static final String MESSAGE_MFA_TOKEN_SENT = "cas.mfa.simple.label.tokensent"; +public class CustomSendTokenAction extends CasSimpleMultifactorSendTokenAction { - private final TicketRegistry ticketRegistry; - private final CommunicationsManager communicationsManager; - private final TransientSessionTicketFactory ticketFactory; - private final CasSimpleMultifactorProperties properties; private final Utils utils; + public CustomSendTokenAction(final TicketRegistry ticketRegistry, + final CommunicationsManager communicationsManager, + final CasSimpleMultifactorAuthenticationTicketFactory ticketFactory, + final CasSimpleMultifactorAuthenticationProperties properties, + final CasSimpleMultifactorTokenCommunicationStrategy tokenCommunicationStrategy, + final Utils utils) { + super(ticketRegistry, communicationsManager, ticketFactory, properties, tokenCommunicationStrategy); + this.utils = utils; + } + + @Override + protected boolean isSmsSent(final CommunicationsManager communicationsManager, + final CasSimpleMultifactorAuthenticationProperties properties, + final Principal principal, + final Ticket token) { + if (communicationsManager.isSmsSenderDefined()) { + val smsProperties = properties.getSms(); + String smsText = StringUtils.isNotBlank(smsProperties.getText()) + ? smsProperties.getFormattedText(token.getId()) + : token.getId(); + // CUSTO: remove the prefix + smsText = smsText.replace(CasSimpleMultifactorAuthenticationTicket.PREFIX + "-", ""); + return communicationsManager.sms(principal, smsProperties.getAttributeName(), smsText, smsProperties.getFrom()); + } + return false; + } + @Override protected Event doExecute(final RequestContext requestContext) { val authentication = WebUtils.getInProgressAuthentication(); - val principal = authentication.getPrincipal(); - val principalAttributes = principal.getAttributes(); + val principal = resolvePrincipal(authentication.getPrincipal()); - // custo + // check for a principal attribute and redirect to a custom page when missing + val principalAttributes = principal.getAttributes(); val mobile = (String) utils.getAttributeValue(principalAttributes, "mobile"); if (mobile == null) { requestContext.getFlowScope().put("firstname", utils.getAttributeValue(principalAttributes, "firstname")); return getEventFactorySupport().event(this, "missingPhone"); } - val service = WebUtils.getService(requestContext); - val token = ticketFactory.create(service, CollectionUtils.wrap(CasSimpleMultifactorAuthenticationHandler.PROPERTY_PRINCIPAL, principal)); - LOGGER.debug("Created multifactor authentication token [{}] for service [{}]", token, service); + // remove token + WebUtils.removeSimpleMultifactorAuthenticationToken(requestContext); - val smsSent = isSmsSent(communicationsManager, properties, principal, token); - val emailSent = isMailSent(communicationsManager, properties, principal, token); + val event = super.doExecute(requestContext); - if (smsSent || emailSent) { - ticketRegistry.addTicket(token); - LOGGER.debug("Successfully submitted token via SMS and/or email to [{}]", principal.getId()); - - val resolver = new MessageBuilder() - .info() - .code(MESSAGE_MFA_TOKEN_SENT) - .defaultText(MESSAGE_MFA_TOKEN_SENT) - .build(); - requestContext.getMessageContext().addMessage(resolver); - - // custo + // add the obfuscated phone to the webflow in case of success + if (CasWebflowConstants.TRANSITION_ID_SUCCESS.equals(event.getId())) { requestContext.getFlowScope().put("mobile", obfuscateMobile(mobile)); - - val attributes = new LocalAttributeMap("token", token.getId()); - return new EventFactorySupport().event(this, CasWebflowConstants.TRANSITION_ID_SUCCESS, attributes); } - LOGGER.error("Both email and SMS communication strategies failed to submit token [{}] to user", token); - return error(); + + return event; } private String obfuscateMobile(final String mobile) { @@ -116,31 +121,4 @@ public class CustomSendTokenAction extends AbstractAction { m = m.substring(0, 2) + " XX XX XX " + m.substring(m.length() - 2); return m; } - - private boolean isSmsSent(final CommunicationsManager communicationsManager, - final CasSimpleMultifactorProperties properties, - final Principal principal, - final Ticket token) { - if (communicationsManager.isSmsSenderDefined()) { - val smsProperties = properties.getSms(); - String smsText = StringUtils.isNotBlank(smsProperties.getText()) - ? smsProperties.getFormattedText(token.getId()) - : token.getId(); - // custo: remove the CAS prefix - smsText = smsText.replace(CasSimpleMultifactorAuthenticationTicketFactory.PREFIX + "-", ""); - return communicationsManager.sms(principal, smsProperties.getAttributeName(), smsText, smsProperties.getFrom()); - } - return false; - } - - private boolean isMailSent(final CommunicationsManager communicationsManager, - final CasSimpleMultifactorProperties properties, - final Principal principal, - final Ticket token) { - if (communicationsManager.isMailSenderDefined()) { - val mailProperties = properties.getMail(); - return communicationsManager.email(principal, mailProperties.getAttributeName(), mailProperties, mailProperties.getFormattedBody(token.getId())); - } - return false; - } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomVerifyPasswordResetRequestAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomVerifyPasswordResetRequestAction.java deleted file mode 100644 index ec00f2d7f..000000000 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomVerifyPasswordResetRequestAction.java +++ /dev/null @@ -1,80 +0,0 @@ -package fr.gouv.vitamui.cas.webflow.actions; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.apache.commons.lang3.StringUtils; -import org.apereo.cas.CentralAuthenticationService; -import org.apereo.cas.configuration.CasConfigurationProperties; -import org.apereo.cas.pm.BasePasswordManagementService; -import org.apereo.cas.pm.PasswordManagementService; -import org.apereo.cas.pm.web.flow.PasswordManagementWebflowUtils; -import org.apereo.cas.ticket.InvalidTicketException; -import org.apereo.cas.ticket.TransientSessionTicket; -import org.apereo.cas.web.support.WebUtils; -import org.springframework.webflow.action.AbstractAction; -import org.springframework.webflow.action.EventFactorySupport; -import org.springframework.webflow.execution.Event; -import org.springframework.webflow.execution.RequestContext; - -/** - * A custom verify passwod rest action which deals with expired (removed from registry) tickets. - */ -@Slf4j -@RequiredArgsConstructor -public class CustomVerifyPasswordResetRequestAction extends AbstractAction { - private final CasConfigurationProperties casProperties; - private final PasswordManagementService passwordManagementService; - private final CentralAuthenticationService centralAuthenticationService; - - @Override - protected Event doExecute(final RequestContext requestContext) { - - val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); - val transientTicket = request.getParameter(PasswordManagementWebflowUtils.REQUEST_PARAMETER_NAME_PASSWORD_RESET_TOKEN); - - if (StringUtils.isBlank(transientTicket)) { - LOGGER.error("Password reset token is missing"); - return error(); - } - - TransientSessionTicket tst = null; - try { - tst = this.centralAuthenticationService.getTicket(transientTicket, TransientSessionTicket.class); - } catch (final InvalidTicketException e) {} - if (tst == null) { - LOGGER.error("Unable to locate token [{}] in the ticket registry", transientTicket); - return error(); - } - - val token = tst.getProperties().get(PasswordManagementWebflowUtils.FLOWSCOPE_PARAMETER_NAME_TOKEN).toString(); - val username = passwordManagementService.parseToken(token); - if (StringUtils.isBlank(username)) { - LOGGER.error("Password reset token could not be verified"); - return error(); - } - this.centralAuthenticationService.deleteTicket(tst.getId()); - - PasswordManagementWebflowUtils.putPasswordResetToken(requestContext, token); - val pm = casProperties.getAuthn().getPm(); - if (pm.getReset().isSecurityQuestionsEnabled()) { - val questions = BasePasswordManagementService - .canonicalizeSecurityQuestions(passwordManagementService.getSecurityQuestions(username)); - if (questions.isEmpty()) { - LOGGER.error("No security questions could be found for [{}]", username); - return error(); - } - PasswordManagementWebflowUtils.putPasswordResetSecurityQuestions(requestContext, questions); - } else { - LOGGER.debug("Security questions are not enabled"); - } - - PasswordManagementWebflowUtils.putPasswordResetUsername(requestContext, username); - PasswordManagementWebflowUtils.putPasswordResetSecurityQuestionsEnabled(requestContext, pm.getReset().isSecurityQuestionsEnabled()); - - if (pm.getReset().isSecurityQuestionsEnabled()) { - return success(); - } - return new EventFactorySupport().event(this, "questionsDisabled"); - } -} diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherAction.java index c16043b0a..18ae0d8ef 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherAction.java @@ -36,7 +36,7 @@ */ package fr.gouv.vitamui.cas.webflow.actions; -import fr.gouv.vitamui.cas.provider.SamlIdentityProviderDto; +import fr.gouv.vitamui.cas.provider.Pac4jClientIdentityProviderDto; import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.cas.util.Constants; import fr.gouv.vitamui.cas.util.Utils; @@ -133,14 +133,14 @@ public class DispatcherAction extends AbstractAction { val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); boolean isInternal; - val provider = (SamlIdentityProviderDto) identityProviderHelper.findByUserIdentifier(providersService.getProviders(), dispatchedUser).orElse(null); + val provider = (Pac4jClientIdentityProviderDto) identityProviderHelper.findByUserIdentifier(providersService.getProviders(), dispatchedUser).orElse(null); if (provider != null) { isInternal = provider.getInternal(); } else { return new Event(this, BAD_CONFIGURATION); } val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext); - val webContext = new JEEContext(request, response, sessionStore); + val webContext = new JEEContext(request, response); if (isInternal) { sessionStore.set(webContext, Constants.SURROGATE, null); LOGGER.debug("Redirect the user to the password page..."); @@ -153,7 +153,7 @@ public class DispatcherAction extends AbstractAction { sessionStore.set(webContext, Constants.SURROGATE, surrogate); } - return utils.performClientRedirection(this, provider.getSaml2Client(), requestContext); + return utils.performClientRedirection(this, provider.getClient(), requestContext); } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionAction.java index 9d24bf8ed..44e835c8b 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionAction.java @@ -36,7 +36,6 @@ */ package fr.gouv.vitamui.cas.webflow.actions; -import fr.gouv.vitamui.cas.util.Constants; import fr.gouv.vitamui.cas.util.Utils; import fr.gouv.vitamui.commons.api.logger.VitamUILogger; import fr.gouv.vitamui.commons.api.logger.VitamUILoggerFactory; @@ -44,7 +43,6 @@ import fr.gouv.vitamui.commons.rest.client.ExternalHttpContext; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.Setter; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; import org.apereo.cas.CentralAuthenticationService; @@ -55,9 +53,9 @@ import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.authentication.principal.Service; import org.apereo.cas.authentication.principal.SimpleWebApplicationServiceImpl; import org.apereo.cas.configuration.CasConfigurationProperties; -import org.apereo.cas.configuration.model.core.logout.LogoutProperties; import org.apereo.cas.logout.LogoutManager; -import org.apereo.cas.logout.slo.SingleLogoutRequest; +import org.apereo.cas.logout.SingleLogoutExecutionRequest; +import org.apereo.cas.logout.slo.SingleLogoutRequestContext; import org.apereo.cas.services.RegisteredService; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.ticket.InvalidTicketException; @@ -65,23 +63,16 @@ import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.ticket.TicketGrantingTicketImpl; import org.apereo.cas.ticket.expiration.NeverExpiresExpirationPolicy; import org.apereo.cas.web.cookie.CasCookieBuilder; -import org.apereo.cas.web.flow.logout.FrontChannelLogoutAction; import org.apereo.cas.web.flow.logout.TerminateSessionAction; import org.apereo.cas.web.support.WebUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.webflow.execution.Action; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.xml.bind.DatatypeConverter; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.*; import static fr.gouv.vitamui.commons.api.CommonConstants.*; @@ -91,44 +82,37 @@ import static fr.gouv.vitamui.commons.api.CommonConstants.*; * * */ -@Setter @Getter public class GeneralTerminateSessionAction extends TerminateSessionAction { private static final VitamUILogger LOGGER = VitamUILoggerFactory.getInstance(GeneralTerminateSessionAction.class); - @Autowired - private Utils utils; + private final Utils utils; - @Autowired - private CasExternalRestClient casExternalRestClient; + private final CasExternalRestClient casExternalRestClient; - @Autowired - private LogoutManager logoutManager; + private final ServicesManager servicesManager; - @Autowired - private ServicesManager servicesManager; + private final CasConfigurationProperties casProperties; - @Autowired - private CasConfigurationProperties casProperties; - - @Autowired - private FrontChannelLogoutAction frontChannelLogoutAction; - - @Value("${theme.vitam-logo:#{null}}") - private String vitamLogoPath; - - @Value("${theme.vitamui-logo-large:#{null}}") - private String vitamuiLargeLogoPath; - - @Value("${theme.vitamui-favicon:#{null}}") - private String vitamuiFaviconPath; + private final Action frontChannelLogoutAction; public GeneralTerminateSessionAction(final CentralAuthenticationService centralAuthenticationService, final CasCookieBuilder ticketGrantingTicketCookieGenerator, final CasCookieBuilder warnCookieGenerator, - final LogoutProperties logoutProperties) { - super(centralAuthenticationService, ticketGrantingTicketCookieGenerator, warnCookieGenerator, logoutProperties); + final LogoutManager logoutManager, + final ConfigurableApplicationContext applicationContext, + final Utils utils, + final CasExternalRestClient casExternalRestClient, + final ServicesManager servicesManager, + final CasConfigurationProperties casProperties, + final Action frontChannelLogoutAction) { + super(centralAuthenticationService, ticketGrantingTicketCookieGenerator, warnCookieGenerator, casProperties.getLogout(), logoutManager, applicationContext); + this.utils = utils; + this.casExternalRestClient = casExternalRestClient; + this.servicesManager = servicesManager; + this.casProperties = casProperties; + this.frontChannelLogoutAction = frontChannelLogoutAction; } @Override @SneakyThrows @@ -176,58 +160,25 @@ public class GeneralTerminateSessionAction extends TerminateSessionAction { // fallback cases: // no CAS cookie -> general logout if (tgtId == null) { - final List<SingleLogoutRequest> logoutRequests = performGeneralLogout("nocookie"); + final List<SingleLogoutRequestContext> logoutRequests = performGeneralLogout("nocookie"); WebUtils.putLogoutRequests(context, logoutRequests); // no ticket or expired -> general logout } else if (ticket == null || ticket.isExpired()) { - final List<SingleLogoutRequest> logoutRequests = performGeneralLogout(tgtId); + final List<SingleLogoutRequestContext> logoutRequests = performGeneralLogout(tgtId); WebUtils.putLogoutRequests(context, logoutRequests); } // if we are in the login webflow, compute the logout URLs if ("login".equals(context.getFlowExecutionContext().getDefinition().getId())) { logger.debug("Computing front channel logout URLs"); - frontChannelLogoutAction.doExecute(context); - } - - final MutableAttributeMap<Object> flowScope = context.getFlowScope(); - if (vitamLogoPath != null) { - try { - final Path logoFile = Paths.get(vitamLogoPath); - final String logo = DatatypeConverter.printBase64Binary(Files.readAllBytes(logoFile)); - flowScope.put(Constants.VITAM_LOGO, logo); - } - catch (final IOException e) { - LOGGER.warn("Can't find vitam logo", e); - } - } - if (vitamuiLargeLogoPath != null) { - try { - final Path logoFile = Paths.get(vitamuiLargeLogoPath); - final String logo = DatatypeConverter.printBase64Binary(Files.readAllBytes(logoFile)); - flowScope.put(Constants.VITAM_UI_LARGE_LOGO, logo); - } - catch (final IOException e) { - LOGGER.warn("Can't find vitam ui large logo", e); - } - } - - if (vitamuiFaviconPath != null) { - try { - final Path faviconFile = Paths.get(vitamuiFaviconPath); - final String favicon = DatatypeConverter.printBase64Binary(Files.readAllBytes(faviconFile)); - flowScope.put(Constants.VITAM_UI_FAVICON, favicon); - } - catch (final IOException e) { - LOGGER.warn("Can't find vitam ui favicon", e); - } + frontChannelLogoutAction.execute(context); } return event; } - protected List<SingleLogoutRequest> performGeneralLogout(final String tgtId) { + protected List<SingleLogoutRequestContext> performGeneralLogout(final String tgtId) { try { final Map<String, AuthenticationHandlerExecutionResult> successes = new HashMap<>(); @@ -254,7 +205,10 @@ public class GeneralTerminateSessionAction extends TerminateSessionAction { } } - return logoutManager.performLogout(fakeTgt); + return logoutManager.performLogout( + SingleLogoutExecutionRequest.builder() + .ticketGrantingTicket(fakeTgt) + .build()); } catch (final RuntimeException e) { logger.error("Unable to perform general logout", e); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/I18NSendPasswordResetInstructionsAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/I18NSendPasswordResetInstructionsAction.java index 7e85426f5..7310ad8e6 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/I18NSendPasswordResetInstructionsAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/I18NSendPasswordResetInstructionsAction.java @@ -37,15 +37,18 @@ package fr.gouv.vitamui.cas.webflow.actions; import org.apache.commons.lang3.StringUtils; +import org.apereo.cas.audit.AuditActionResolvers; +import org.apereo.cas.audit.AuditResourceResolvers; +import org.apereo.cas.audit.AuditableActions; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; import org.apereo.cas.configuration.CasConfigurationProperties; +import org.apereo.cas.notifications.CommunicationsManager; import org.apereo.cas.pm.PasswordManagementService; import org.apereo.cas.pm.web.flow.actions.SendPasswordResetInstructionsAction; import org.apereo.cas.ticket.TicketFactory; import org.apereo.cas.ticket.registry.TicketRegistry; -import org.apereo.cas.util.io.CommunicationsManager; import org.apereo.cas.web.support.WebUtils; -import org.springframework.beans.factory.annotation.Value; +import org.apereo.inspektr.audit.annotation.Audit; import org.springframework.context.HierarchicalMessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.webflow.core.collection.MutableAttributeMap; @@ -78,8 +81,7 @@ public class I18NSendPasswordResetInstructionsAction extends SendPasswordResetIn private final Utils utils; - @Value("${theme.vitamui-platform-name:VITAM-UI}") - private String vitamuiPlatformName; + private final String vitamuiPlatformName; public I18NSendPasswordResetInstructionsAction(final CasConfigurationProperties casProperties, final CommunicationsManager communicationsManager, @@ -89,27 +91,31 @@ public class I18NSendPasswordResetInstructionsAction extends SendPasswordResetIn final HierarchicalMessageSource messageSource, final ProvidersService providersService, final IdentityProviderHelper identityProviderHelper, - final Utils utils) { - super(casProperties, communicationsManager, passwordManagementService, ticketRegistry, ticketFactory); + final Utils utils, + final String vitamuiPlatformName) { + super(casProperties, communicationsManager, passwordManagementService, ticketRegistry, ticketFactory, null); this.messageSource = messageSource; this.providersService = providersService; this.identityProviderHelper = identityProviderHelper; this.utils = utils; + this.vitamuiPlatformName = vitamuiPlatformName; } + @Audit(action = AuditableActions.REQUEST_CHANGE_PASSWORD, + principalResolverName = "REQUEST_CHANGE_PASSWORD_PRINCIPAL_RESOLVER", + actionResolverName = AuditActionResolvers.REQUEST_CHANGE_PASSWORD_ACTION_RESOLVER, + resourceResolverName = AuditResourceResolvers.REQUEST_CHANGE_PASSWORD_RESOURCE_RESOLVER) @Override protected Event doExecute(final RequestContext requestContext) { - communicationsManager.validate(); - if (!communicationsManager.isMailSenderDefined()) { + if (!communicationsManager.isMailSenderDefined() && !communicationsManager.isSmsSenderDefined()) { return getErrorEvent("contact.failed", "Unable to send email as no mail sender is defined", requestContext); } + // CUSTO: try to get the username from the credentials also (after a password expiration) val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); request.removeAttribute(PmTransientSessionTicketExpirationPolicyBuilder.PM_EXPIRATION_IN_MINUTES_ATTRIBUTE); String username = request.getParameter("username"); - // added from CAS: if (StringUtils.isBlank(username)) { - // try to get the username from the credentials also (after a password expiration) final MutableAttributeMap<Object> flowScope = requestContext.getFlowScope(); final Object credential = flowScope.get("credential"); if (credential instanceof UsernamePasswordCredential) { @@ -117,13 +123,13 @@ public class I18NSendPasswordResetInstructionsAction extends SendPasswordResetIn username = usernamePasswordCredential.getUsername(); } } + val query = buildPasswordManagementQuery(requestContext); if (StringUtils.isBlank(username)) { - LOGGER.warn("No username parameter is provided"); return getErrorEvent("username.required", "No username is provided", requestContext); } - // changed from CAS: - final String email = passwordManagementService.findEmail(username); + val email = passwordManagementService.findEmail(query); + // CUSTO: only retrieve email (and not phone) and force success event (instead of error) when failure if (StringUtils.isBlank(email)) { LOGGER.warn("No recipient is provided; nonetheless, we return to the success page"); return success(); @@ -133,23 +139,26 @@ public class I18NSendPasswordResetInstructionsAction extends SendPasswordResetIn } val service = WebUtils.getService(requestContext); - val url = buildPasswordResetUrl(username, passwordManagementService, casProperties, service); + val url = buildPasswordResetUrl(query.getUsername(), passwordManagementService, casProperties, service); if (StringUtils.isNotBlank(url)) { val pm = casProperties.getAuthn().getPm(); - LOGGER.debug("Generated password reset URL [{}]; Link is only active for the next [{}] minute(s)", utils.sanitizePasswordResetUrl(url), - pm.getReset().getExpirationMinutes()); - if (sendPasswordResetEmailToAccount(email, url)) { - return success(); + LOGGER.debug("Generated password reset URL [{}]; Link is only active for the next [{}] minute(s)", + url, pm.getReset().getExpirationMinutes()); + // CUSTO: only send email (and not SMS) + val sendEmail = sendPasswordResetEmailToAccount(query.getUsername(), email, url, requestContext); + if (sendEmail) { + return success(url); } } else { LOGGER.error("No password reset URL could be built and sent to [{}]", email); } LOGGER.error("Failed to notify account [{}]", email); - return getErrorEvent("contact.failed", "Failed to send the password reset link to the given email address or phone number", requestContext); + return getErrorEvent("contact.failed", "Failed to send the password reset link via email address or phone", requestContext); } @Override - protected boolean sendPasswordResetEmailToAccount(final String to, final String url) { + protected boolean sendPasswordResetEmailToAccount(final String username, final String to, final String url, + final RequestContext requestContext) { final PmMessageToSend messageToSend = PmMessageToSend.buildMessage(messageSource, "", "", String.valueOf(casProperties.getAuthn().getPm().getReset().getExpirationMinutes()), url, vitamuiPlatformName, LocaleContextHolder.getLocale()); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomCasSimpleMultifactorWebflowConfigurer.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomCasSimpleMultifactorWebflowConfigurer.java new file mode 100644 index 000000000..3b132ba0f --- /dev/null +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomCasSimpleMultifactorWebflowConfigurer.java @@ -0,0 +1,125 @@ +/** + * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2020) + * and the signatories of the "VITAM - Accord du Contributeur" agreement. + * + * contact@programmevitam.fr + * + * This software is a computer program whose purpose is to implement + * implement a digital archiving front-office system for the secure and + * efficient high volumetry VITAM solution. + * + * This software is governed by the CeCILL-C license under French law and + * abiding by the rules of distribution of free software. You can use, + * modify and/ or redistribute the software under the terms of the CeCILL-C + * license as circulated by CEA, CNRS and INRIA at the following URL + * "http://www.cecill.info". + * + * As a counterpart to the access to the source code and rights to copy, + * modify and redistribute granted by the license, users are provided only + * with a limited warranty and the software's author, the holder of the + * economic rights, and the successive licensors have only limited + * liability. + * + * In this respect, the user's attention is drawn to the risks associated + * with loading, using, modifying and/or developing or reproducing the + * software by the user in light of its specific status of free software, + * that may mean that it is complicated to manipulate, and that also + * therefore means that it is reserved for developers and experienced + * professionals having in-depth computer knowledge. Users are therefore + * encouraged to load and test the software's suitability as regards their + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package fr.gouv.vitamui.cas.webflow.configurer; + +import lombok.val; +import org.apereo.cas.configuration.CasConfigurationProperties; +import org.apereo.cas.mfa.simple.CasSimpleMultifactorTokenCredential; +import org.apereo.cas.util.CollectionUtils; +import org.apereo.cas.web.flow.CasWebflowConstants; +import org.apereo.cas.web.flow.configurer.AbstractCasMultifactorWebflowConfigurer; +import org.apereo.cas.web.flow.configurer.CasMultifactorWebflowCustomizer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.engine.builder.support.FlowBuilderServices; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Custom webflow for simple MFA. + */ +public class CustomCasSimpleMultifactorWebflowConfigurer extends AbstractCasMultifactorWebflowConfigurer { + + public static final String MFA_SIMPLE_EVENT_ID = "mfa-simple"; + + public CustomCasSimpleMultifactorWebflowConfigurer(final FlowBuilderServices flowBuilderServices, + final FlowDefinitionRegistry loginFlowDefinitionRegistry, + final FlowDefinitionRegistry flowDefinitionRegistry, + final ConfigurableApplicationContext applicationContext, + final CasConfigurationProperties casProperties, + final List<CasMultifactorWebflowCustomizer> mfaFlowCustomizers) { + super(flowBuilderServices, loginFlowDefinitionRegistry, applicationContext, + casProperties, Optional.of(flowDefinitionRegistry), mfaFlowCustomizers); + } + + @Override + protected void doInitialize() { + multifactorAuthenticationFlowDefinitionRegistries.forEach(registry -> { + val flow = getFlow(registry, MFA_SIMPLE_EVENT_ID); + createFlowVariable(flow, CasWebflowConstants.VAR_ID_CREDENTIAL, CasSimpleMultifactorTokenCredential.class); + flow.getStartActionList().add(createEvaluateAction(CasWebflowConstants.ACTION_ID_INITIAL_FLOW_SETUP)); + + val initLoginFormState = createActionState(flow, CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM, + createEvaluateAction(CasWebflowConstants.ACTION_ID_INIT_LOGIN_ACTION)); + createTransitionForState(initLoginFormState, CasWebflowConstants.TRANSITION_ID_SUCCESS, "sendSimpleToken"); + setStartState(flow, initLoginFormState); + createEndState(flow, CasWebflowConstants.STATE_ID_SUCCESS); + createEndState(flow, CasWebflowConstants.STATE_ID_UNAVAILABLE); + + val sendSimpleToken = createActionState(flow, "sendSimpleToken", + createEvaluateAction("mfaSimpleMultifactorSendTokenAction")); + createTransitionForState(sendSimpleToken, CasWebflowConstants.TRANSITION_ID_ERROR, CasWebflowConstants.STATE_ID_UNAVAILABLE); + createTransitionForState(sendSimpleToken, CasWebflowConstants.TRANSITION_ID_SUCCESS, CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM); + // CUSTO: + createTransitionForState(sendSimpleToken, "missingPhone", "missingPhone"); + createViewState(flow, "missingPhone", "casSmsMissingPhoneView"); + // + + val setPrincipalAction = createSetAction("viewScope.principal", "conversationScope.authentication.principal"); + val propertiesToBind = CollectionUtils.wrapList("token"); + val binder = createStateBinderConfiguration(propertiesToBind); + val viewLoginFormState = createViewState(flow, CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM, + "simple-mfa/casSimpleMfaLoginView", binder); + createStateModelBinding(viewLoginFormState, CasWebflowConstants.VAR_ID_CREDENTIAL, CasSimpleMultifactorTokenCredential.class); + viewLoginFormState.getEntryActionList().add(setPrincipalAction); + + // CUSTO: instead of CasWebflowConstants.STATE_ID_REAL_SUBMIT, send to intermediateSubmit + createTransitionForState(viewLoginFormState, CasWebflowConstants.TRANSITION_ID_SUBMIT, + "intermediateSubmit", Map.of("bind", Boolean.TRUE, "validate", Boolean.TRUE)); + createTransitionForState(viewLoginFormState, CasWebflowConstants.TRANSITION_ID_RESEND, "sendSimpleToken", + Map.of("bind", Boolean.FALSE, "validate", Boolean.FALSE)); + + // CUSTO: + val intermediateSubmit = createActionState(flow, "intermediateSubmit", createEvaluateAction("checkMfaTokenAction")); + createTransitionForState(intermediateSubmit, CasWebflowConstants.TRANSITION_ID_SUCCESS, CasWebflowConstants.STATE_ID_REAL_SUBMIT); + createTransitionForState(intermediateSubmit, CasWebflowConstants.TRANSITION_ID_ERROR, "codeExpired"); + val codeExpired = createViewState(flow, "codeExpired", "casSmsCodeExpiredView"); + createTransitionForState(codeExpired, "resend", CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); + // + + val realSubmitState = createActionState(flow, CasWebflowConstants.STATE_ID_REAL_SUBMIT, + createEvaluateAction(CasWebflowConstants.ACTION_ID_OTP_AUTHENTICATION_ACTION)); + createTransitionForState(realSubmitState, CasWebflowConstants.TRANSITION_ID_SUCCESS, CasWebflowConstants.STATE_ID_SUCCESS); + createTransitionForState(realSubmitState, CasWebflowConstants.TRANSITION_ID_ERROR, CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM); + }); + + registerMultifactorProviderAuthenticationWebflow(getLoginFlow(), MFA_SIMPLE_EVENT_ID, + casProperties.getAuthn().getMfa().getSimple().getId()); + } +} diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomLoginWebflowConfigurer.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomLoginWebflowConfigurer.java index 59ab31491..a3f4cbcb3 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomLoginWebflowConfigurer.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/configurer/CustomLoginWebflowConfigurer.java @@ -36,10 +36,7 @@ */ package fr.gouv.vitamui.cas.webflow.configurer; -import javax.security.auth.login.AccountLockedException; -import javax.security.auth.login.AccountNotFoundException; -import javax.security.auth.login.CredentialExpiredException; -import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.*; import fr.gouv.vitamui.cas.webflow.actions.DispatcherAction; import lombok.val; @@ -56,7 +53,7 @@ import org.apereo.cas.ticket.UnsatisfiedAuthenticationPolicyException; import org.apereo.cas.util.CollectionUtils; import org.apereo.cas.web.flow.CasWebflowConstants; import org.apereo.cas.web.flow.configurer.DefaultLoginWebflowConfigurer; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; import org.springframework.webflow.engine.ActionState; import org.springframework.webflow.engine.Flow; @@ -84,16 +81,8 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer private static final String BAD_CONFIGURATION_VIEW = "casAccountBadConfigurationView"; - public static final String DIRECT_RESULT = "direct"; - - private static final String DIRECT_ACTION = "directRedirection"; - - public static final String INDIRECT_RESULT = "indirect"; - - private static final String INDIRECT_ACTION = "indirectRedirection"; - public CustomLoginWebflowConfigurer(final FlowBuilderServices flowBuilderServices, final FlowDefinitionRegistry flowDefinitionRegistry, - final ApplicationContext applicationContext, final CasConfigurationProperties casProperties) { + final ConfigurableApplicationContext applicationContext, final CasConfigurationProperties casProperties) { super(flowBuilderServices, flowDefinitionRegistry, applicationContext, casProperties); } @@ -106,7 +95,7 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer CasWebflowConstants.STATE_ID_GATEWAY_REQUEST_CHECK); createTransitionForState(action, CasWebflowConstants.TRANSITION_ID_TICKET_GRANTING_TICKET_INVALID, CasWebflowConstants.STATE_ID_TERMINATE_SESSION); - // instead of STATE_ID_HAS_SERVICE_CHECK, send to STATE_ID_TRIGGER_CHANGE_PASSWORD + // CUSTO: instead of STATE_ID_HAS_SERVICE_CHECK, send to STATE_ID_TRIGGER_CHANGE_PASSWORD createTransitionForState(action, CasWebflowConstants.TRANSITION_ID_TICKET_GRANTING_TICKET_VALID, STATE_ID_TRIGGER_CHANGE_PASSWORD); @@ -115,7 +104,7 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer private void createTriggerChangePasswordAction(final Flow flow) { final ActionState action = createActionState(flow, STATE_ID_TRIGGER_CHANGE_PASSWORD, "triggerChangePasswordAction"); - createTransitionForState(action, TriggerChangePasswordAction.EVENT_ID_CHANGE_PASSWORD, CasWebflowConstants.VIEW_ID_MUST_CHANGE_PASSWORD); + createTransitionForState(action, TriggerChangePasswordAction.EVENT_ID_CHANGE_PASSWORD, CasWebflowConstants.STATE_ID_MUST_CHANGE_PASSWORD); createTransitionForState(action, TriggerChangePasswordAction.EVENT_ID_CONTINUE, CasWebflowConstants.STATE_ID_HAS_SERVICE_CHECK); } @@ -123,21 +112,27 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer protected void createHandleAuthenticationFailureAction(final Flow flow) { val handler = createActionState(flow, CasWebflowConstants.STATE_ID_HANDLE_AUTHN_FAILURE, CasWebflowConstants.ACTION_ID_AUTHENTICATION_EXCEPTION_HANDLER); - createTransitionForState(handler, AccountDisabledException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_ACCOUNT_DISABLED); - createTransitionForState(handler, AccountLockedException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_ACCOUNT_LOCKED); - // custo: - createTransitionForState(handler, AccountPasswordMustChangeException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_SEND_RESET_PASSWORD_ACCT_INFO); - createTransitionForState(handler, CredentialExpiredException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_EXPIRED_PASSWORD); - createTransitionForState(handler, InvalidLoginLocationException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_INVALID_WORKSTATION); - createTransitionForState(handler, InvalidLoginTimeException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_INVALID_AUTHENTICATION_HOURS); + createTransitionForState(handler, AccountDisabledException.class.getSimpleName(), CasWebflowConstants.STATE_ID_ACCOUNT_DISABLED); + createTransitionForState(handler, AccountLockedException.class.getSimpleName(), CasWebflowConstants.STATE_ID_ACCOUNT_LOCKED); + createTransitionForState(handler, AccountExpiredException.class.getSimpleName(), CasWebflowConstants.STATE_ID_EXPIRED_PASSWORD); + createTransitionForState(handler, AccountLockedException.class.getSimpleName(), CasWebflowConstants.STATE_ID_ACCOUNT_LOCKED); + // CUSTO: instead of STATE_ID_MUST_CHANGE_PASSWORD, send to STATE_ID_SEND_RESET_PASSWORD_ACCT_INFO + createTransitionForState(handler, AccountPasswordMustChangeException.class.getSimpleName(), CasWebflowConstants.STATE_ID_SEND_RESET_PASSWORD_ACCT_INFO); + createTransitionForState(handler, CredentialExpiredException.class.getSimpleName(), CasWebflowConstants.STATE_ID_EXPIRED_PASSWORD); + createTransitionForState(handler, InvalidLoginLocationException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INVALID_WORKSTATION); + createTransitionForState(handler, InvalidLoginTimeException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INVALID_AUTHENTICATION_HOURS); createTransitionForState(handler, FailedLoginException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); createTransitionForState(handler, AccountNotFoundException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); - createTransitionForState(handler, UnauthorizedServiceForPrincipalException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); + createTransitionForState(handler, UnauthorizedServiceForPrincipalException.class.getSimpleName(), + CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); createTransitionForState(handler, PrincipalException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); - createTransitionForState(handler, UnsatisfiedAuthenticationPolicyException.class.getSimpleName(), CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); - createTransitionForState(handler, UnauthorizedAuthenticationException.class.getSimpleName(), CasWebflowConstants.VIEW_ID_AUTHENTICATION_BLOCKED); + createTransitionForState(handler, UnsatisfiedAuthenticationPolicyException.class.getSimpleName(), + CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); + createTransitionForState(handler, UnauthorizedAuthenticationException.class.getSimpleName(), + CasWebflowConstants.STATE_ID_AUTHENTICATION_BLOCKED); createTransitionForState(handler, CasWebflowConstants.STATE_ID_SERVICE_UNAUTHZ_CHECK, CasWebflowConstants.STATE_ID_SERVICE_UNAUTHZ_CHECK); createStateDefaultTransition(handler, CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); + handler.getEntryActionList().add(createEvaluateAction(CasWebflowConstants.ACTION_ID_CLEAR_WEBFLOW_CREDENTIALS)); } @Override @@ -145,7 +140,7 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer val propertiesToBind = CollectionUtils.wrapList("username"); val binder = createStateBinderConfiguration(propertiesToBind); - val state = createViewState(flow, CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM, "casLoginView", binder); + val state = createViewState(flow, CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM, "login/casLoginView", binder); state.getRenderActionList().add(createEvaluateAction(CasWebflowConstants.ACTION_ID_RENDER_LOGIN_FORM)); createStateModelBinding(state, CasWebflowConstants.VAR_ID_CREDENTIAL, UsernamePasswordCredential.class); @@ -163,7 +158,7 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer val action = createActionState(flow, INTERMEDIATE_SUBMIT, "dispatcherAction"); createTransitionForState(action, CasWebflowConstants.TRANSITION_ID_SUCCESS, VIEW_PWD_FORM); createTransitionForState(action, CasWebflowConstants.TRANSITION_ID_STOP, CasWebflowConstants.STATE_ID_STOP_WEBFLOW); - createTransitionForState(action, DispatcherAction.DISABLED, CasWebflowConstants.VIEW_ID_ACCOUNT_DISABLED); + createTransitionForState(action, DispatcherAction.DISABLED, CasWebflowConstants.STATE_ID_ACCOUNT_DISABLED); createTransitionForState(action, DispatcherAction.BAD_CONFIGURATION, BAD_CONFIGURATION_VIEW); createEndState(flow, BAD_CONFIGURATION_VIEW, BAD_CONFIGURATION_VIEW); @@ -183,6 +178,6 @@ public class CustomLoginWebflowConfigurer extends DefaultLoginWebflowConfigurer attributes.put("validate", Boolean.TRUE); attributes.put("history", History.INVALIDATE); - createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_RESET_PASSWORD, CasWebflowConstants.VIEW_ID_SEND_RESET_PASSWORD_ACCT_INFO); + createTransitionForState(state, CasWebflowConstants.TRANSITION_ID_RESET_PASSWORD, CasWebflowConstants.STATE_ID_SEND_RESET_PASSWORD_ACCT_INFO); } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CertificateParser.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CertificateParser.java new file mode 100644 index 000000000..b90f56a09 --- /dev/null +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CertificateParser.java @@ -0,0 +1,94 @@ +/** + * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2020) + * and the signatories of the "VITAM - Accord du Contributeur" agreement. + * + * contact@programmevitam.fr + * + * This software is a computer program whose purpose is to implement + * implement a digital archiving front-office system for the secure and + * efficient high volumetry VITAM solution. + * + * This software is governed by the CeCILL-C license under French law and + * abiding by the rules of distribution of free software. You can use, + * modify and/ or redistribute the software under the terms of the CeCILL-C + * license as circulated by CEA, CNRS and INRIA at the following URL + * "http://www.cecill.info". + * + * As a counterpart to the access to the source code and rights to copy, + * modify and redistribute granted by the license, users are provided only + * with a limited warranty and the software's author, the holder of the + * economic rights, and the successive licensors have only limited + * liability. + * + * In this respect, the user's attention is drawn to the risks associated + * with loading, using, modifying and/or developing or reproducing the + * software by the user in light of its specific status of free software, + * that may mean that it is complicated to manipulate, and that also + * therefore means that it is reserved for developers and experienced + * professionals having in-depth computer knowledge. Users are therefore + * encouraged to load and test the software's suitability as regards their + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package fr.gouv.vitamui.cas.x509; + +import lombok.val; +import org.apache.commons.lang.StringUtils; + +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.regex.Pattern; + +/** + * Certificate parser + */ +public class CertificateParser { + + private CertificateParser() {} + + public static String extract(final X509Certificate cert, final X509AttributeMapping mapping) throws CertificateParsingException { + val name = mapping.getName(); + String value = null; + if (X509CertificateAttributes.ISSUER_DN.name().equalsIgnoreCase(name)) { + value = cert.getIssuerDN().getName(); + } else if (X509CertificateAttributes.SUBJECT_DN.name().equalsIgnoreCase(name)) { + value = cert.getSubjectDN().getName(); + } else if (X509CertificateAttributes.SUBJECT_ALTERNATE_NAME.name().equalsIgnoreCase(name)) { + val altNames = cert.getSubjectAlternativeNames(); + if (altNames != null && altNames.size() > 0) { + val altName = altNames.iterator().next(); + if (altName != null && altName.size() == 2) { + value = (String) altName.get(1); + } + } + } + if (value == null) { + throw new CertificateParsingException("Cannot find X509 value for: " + name); + } + val parsing = mapping.getParsing(); + val expansion = mapping.getExpansion(); + if (StringUtils.isNotBlank(parsing)) { + val pattern = Pattern.compile(parsing); + val matcher = pattern.matcher(value); + if (matcher.matches()) { + val groupCount = matcher.groupCount(); + if (groupCount == 0) { + throw new CertificateParsingException("Parsing fails for X509 value: " + value); + } + if (StringUtils.isBlank(expansion)) { + value = matcher.group(1); + } else { + value = expansion; + for (int i = 0; i < groupCount; i++) { + value = value.replace("{" + i + "}", matcher.group(i + 1)); + } + } + } + } + return value; + } +} diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CustomRequestHeaderX509CertificateExtractor.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CustomRequestHeaderX509CertificateExtractor.java new file mode 100644 index 000000000..3f04e0229 --- /dev/null +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/CustomRequestHeaderX509CertificateExtractor.java @@ -0,0 +1,126 @@ +/** + * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2020) + * and the signatories of the "VITAM - Accord du Contributeur" agreement. + * + * contact@programmevitam.fr + * + * This software is a computer program whose purpose is to implement + * implement a digital archiving front-office system for the secure and + * efficient high volumetry VITAM solution. + * + * This software is governed by the CeCILL-C license under French law and + * abiding by the rules of distribution of free software. You can use, + * modify and/ or redistribute the software under the terms of the CeCILL-C + * license as circulated by CEA, CNRS and INRIA at the following URL + * "http://www.cecill.info". + * + * As a counterpart to the access to the source code and rights to copy, + * modify and redistribute granted by the license, users are provided only + * with a limited warranty and the software's author, the holder of the + * economic rights, and the successive licensors have only limited + * liability. + * + * In this respect, the user's attention is drawn to the risks associated + * with loading, using, modifying and/or developing or reproducing the + * software by the user in light of its specific status of free software, + * that may mean that it is complicated to manipulate, and that also + * therefore means that it is reserved for developers and experienced + * professionals having in-depth computer knowledge. Users are therefore + * encouraged to load and test the software's suitability as regards their + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package fr.gouv.vitamui.cas.x509; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apereo.cas.adaptors.x509.authentication.X509CertificateExtractor; + +import javax.servlet.http.HttpServletRequest; +import java.io.ByteArrayInputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Base64; + +/** + * Custom certificate extractor from the request. + */ +@Slf4j +public class CustomRequestHeaderX509CertificateExtractor implements X509CertificateExtractor { + + private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + private static final String END_CERT = "-----END CERTIFICATE-----"; + + private final String customCertificateHeader; + + private final boolean x509AuthnMandatory; + + public CustomRequestHeaderX509CertificateExtractor(final String customCertificateHeader, final boolean x509AuthnMandatory) { + this.customCertificateHeader = customCertificateHeader; + this.x509AuthnMandatory = x509AuthnMandatory; + } + + @Override + public X509Certificate[] extract(final HttpServletRequest request) { + final X509Certificate[] certs = internalExtract(request); + if (x509AuthnMandatory && certs == null) { + throw new RuntimeException("Client certificate is mandatory!"); + } + return certs; + } + + protected X509Certificate[] internalExtract(final HttpServletRequest request) { + final String certHeader = request.getHeader(customCertificateHeader); + if (StringUtils.isBlank(certHeader)) { + LOGGER.debug("Certificates not found via custom header: {}", customCertificateHeader); + return null; + } + + X509Certificate cert = null; + try { + cert = parseCertificateGeneratedByNginx(certHeader); + } catch (final Exception e) { + LOGGER.debug("Nginx parsing exception: {}", e.getMessage()); + try { + cert = parseCertificateGeneratedByApache(certHeader); + } catch (final Exception e2) { + LOGGER.debug("Apache parsing exception: {}", e2.getMessage()); + } + } + if (cert == null) { + LOGGER.error("Cannot parse certificate from Apache and Nginx"); + return null; + } + + final X509Certificate[] certificates = new X509Certificate[1]; + certificates[0] = cert; + + LOGGER.debug("[{}] Certificate(s) found via custom header: [{}]", certificates.length, Arrays.toString(certificates)); + return certificates; + } + + protected X509Certificate parseCertificateGeneratedByNginx(final String header) throws CertificateException { + final String data = header.replaceAll("\t", "\n"); + + final String decoded = URLDecoder.decode(data, StandardCharsets.UTF_8); + final String cert = decoded.replace(BEGIN_CERT, "").replace(END_CERT, "").replaceAll(" ","+").replaceAll("\\n", ""); + + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(cert))); + } + + protected X509Certificate parseCertificateGeneratedByApache(final String header) throws CertificateException { + final String cert = header.replace(BEGIN_CERT, "").replace(END_CERT, "").replaceAll(" ", "").replaceAll("\\n", ""); + + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(cert))); + } +} diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/NoOpAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/X509AttributeMapping.java similarity index 75% rename from cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/NoOpAction.java rename to cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/X509AttributeMapping.java index 7f80f6f16..3418246ed 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/NoOpAction.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/X509AttributeMapping.java @@ -34,26 +34,34 @@ * The fact that you are presently reading this means that you have had * knowledge of the CeCILL-C license and that you accept its terms. */ -package fr.gouv.vitamui.cas.webflow.actions; - -import lombok.RequiredArgsConstructor; -import org.springframework.webflow.action.AbstractAction; -import org.springframework.webflow.execution.Event; -import org.springframework.webflow.execution.RequestContext; +package fr.gouv.vitamui.cas.x509; /** - * A no-op action returning a specific result. + * Attribute mapping definition for X509 authentication */ -@RequiredArgsConstructor -public class NoOpAction extends AbstractAction { +public class X509AttributeMapping { + + private final String name; + + private final String parsing; - private final String eventId; + private final String expansion; + + public X509AttributeMapping(final String name, final String parsing, final String expansion) { + this.name = name; + this.parsing = parsing; + this.expansion = expansion; + } + + public String getName() { + return name; + } + + public String getParsing() { + return parsing; + } - @Override - protected Event doExecute(final RequestContext requestContext) { - if (eventId != null) { - return getEventFactorySupport().event(this, eventId); - } - return null; + public String getExpansion() { + return expansion; } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/VitamStatusCode.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/X509CertificateAttributes.java similarity index 55% rename from cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/VitamStatusCode.java rename to cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/X509CertificateAttributes.java index b87103492..5fa843eeb 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/util/VitamStatusCode.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/x509/X509CertificateAttributes.java @@ -34,70 +34,11 @@ * The fact that you are presently reading this means that you have had * knowledge of the CeCILL-C license and that you accept its terms. */ -package fr.gouv.vitamui.cas.util; +package fr.gouv.vitamui.cas.x509; /** - * Enum StatusCode - * - * different constants status code for workflow , action handler and process + * X509 certificate attributes */ -public enum VitamStatusCode { - - /** - * UNKNOWN : indicates that the workflow or the action handler or the process is in unknown status! - */ - UNKNOWN, - - /** - * STARTED : indicates that the workflow or the action handler or the process has been started - */ - STARTED, - - /** - * ALREADY_EXECUTED : indicates that a particular step / action has already been processed - */ - ALREADY_EXECUTED, - /** - * OK : indicates the successful without warning - */ - OK, - - /** - * WARNING : indicates successful with a general warning. Warning are often useful in preventing future Action - * problems - */ - WARNING, - - /** - * KO : indicates the failed execution of the action - */ - KO, - - /** - * FATAL : indicates a critical error such as technical Exception ( runtime exception, illegal argument exception, - * null pointer exception ...) - */ - FATAL; - - /** - * @return Status Level - */ - public int getStatusLevel() { - return ordinal(); - } - - /** - * @return True if the status is greater or equal to OK - */ - public boolean isGreaterOrEqualToKo() { - return compareTo(KO) >= 0; - } - - /** - * @return True if the status is greater or equal to FATAL - */ - public boolean isGreaterOrEqualToFatal() { - return compareTo(FATAL) >= 0; - } - +public enum X509CertificateAttributes { + ISSUER_DN, SUBJECT_DN, SUBJECT_ALTERNATE_NAME } diff --git a/cas/cas-server/src/main/java/org/apereo/cas/CasEmbeddedContainerUtils.java b/cas/cas-server/src/main/java/org/apereo/cas/CasEmbeddedContainerUtils.java index 18cc916e6..2c19e8b85 100644 --- a/cas/cas-server/src/main/java/org/apereo/cas/CasEmbeddedContainerUtils.java +++ b/cas/cas-server/src/main/java/org/apereo/cas/CasEmbeddedContainerUtils.java @@ -1,40 +1,65 @@ +/** + * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2020) + * and the signatories of the "VITAM - Accord du Contributeur" agreement. + * + * contact@programmevitam.fr + * + * This software is a computer program whose purpose is to implement + * implement a digital archiving front-office system for the secure and + * efficient high volumetry VITAM solution. + * + * This software is governed by the CeCILL-C license under French law and + * abiding by the rules of distribution of free software. You can use, + * modify and/ or redistribute the software under the terms of the CeCILL-C + * license as circulated by CEA, CNRS and INRIA at the following URL + * "http://www.cecill.info". + * + * As a counterpart to the access to the source code and rights to copy, + * modify and redistribute granted by the license, users are provided only + * with a limited warranty and the software's author, the holder of the + * economic rights, and the successive licensors have only limited + * liability. + * + * In this respect, the user's attention is drawn to the risks associated + * with loading, using, modifying and/or developing or reproducing the + * software by the user in light of its specific status of free software, + * that may mean that it is complicated to manipulate, and that also + * therefore means that it is reserved for developers and experienced + * professionals having in-depth computer knowledge. Users are therefore + * encouraged to load and test the software's suitability as regards their + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ package org.apereo.cas; +import org.apereo.cas.util.logging.LoggingInitialization; +import org.apereo.cas.util.spring.boot.AbstractCasBanner; + import lombok.experimental.UtilityClass; import lombok.val; -import org.apereo.cas.util.spring.boot.AbstractCasBanner; +import org.apache.commons.lang3.StringUtils; import org.springframework.boot.Banner; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.jfr.FlightRecorderApplicationStartup; -import java.util.HashMap; -import java.util.Map; +import java.util.Optional; /** - * Copy/pasted from the original CAS class, directly using a custom banner (avoid the search by reflection). + * Copy/pasted from the original CAS class with no reflection to avoid unwanted logs and to retrieve the custom banner. */ @UtilityClass public class CasEmbeddedContainerUtils { - /** - * Property to dictate to the environment whether embedded container is running CAS. - */ - public static final String EMBEDDED_CONTAINER_CONFIG_ACTIVE = "CasEmbeddedContainerConfigurationActive"; - - /** - * Gets runtime properties. - * - * @param embeddedContainerActive the embedded container active - * @return the runtime properties - */ - public static Map<String, Object> getRuntimeProperties(final Boolean embeddedContainerActive) { - val properties = new HashMap<String, Object>(); - properties.put(EMBEDDED_CONTAINER_CONFIG_ACTIVE, embeddedContainerActive); - return properties; + private static final int APPLICATION_EVENTS_CAPACITY = 5_000; + + public static Optional<LoggingInitialization> getLoggingInitialization() { + return Optional.empty(); } - /** - * Gets cas banner instance. - * - * @return the cas banner instance - */ public static Banner getCasBannerInstance() { return new CustomCasBanner(); } @@ -53,4 +78,15 @@ public class CasEmbeddedContainerUtils { " \n"; } } + + public static ApplicationStartup getApplicationStartup() { + val type = StringUtils.defaultIfBlank(System.getProperty("CAS_APP_STARTUP"), "default"); + if (StringUtils.equalsIgnoreCase("jfr", type)) { + return new FlightRecorderApplicationStartup(); + } + if (StringUtils.equalsIgnoreCase("buffering", type)) { + return new BufferingApplicationStartup(APPLICATION_EVENTS_CAPACITY); + } + return ApplicationStartup.DEFAULT; + } } diff --git a/cas/cas-server/src/main/java/org/apereo/cas/config/HazelcastTicketRegistryTicketCatalogConfiguration.java b/cas/cas-server/src/main/java/org/apereo/cas/config/HazelcastTicketRegistryTicketCatalogConfiguration.java deleted file mode 100644 index bd5f88e3b..000000000 --- a/cas/cas-server/src/main/java/org/apereo/cas/config/HazelcastTicketRegistryTicketCatalogConfiguration.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.apereo.cas.config; - -import org.apereo.cas.configuration.CasConfigurationProperties; - -import org.apereo.cas.ticket.TicketCatalog; -import org.apereo.cas.ticket.TicketDefinition; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -/** - * Override the transient session ticket map timeout. - */ -@Configuration(value = "hazelcastTicketRegistryTicketMetadataCatalogConfiguration", proxyBeanMethods = false) -@EnableConfigurationProperties(CasConfigurationProperties.class) -public class HazelcastTicketRegistryTicketCatalogConfiguration extends BaseTicketDefinitionBuilderSupportConfiguration { - - private final CasConfigurationProperties casProperties; - - public HazelcastTicketRegistryTicketCatalogConfiguration(final CasConfigurationProperties casProperties) { - super(casProperties, new CasTicketCatalogConfigurationValuesProvider() {}); - this.casProperties = casProperties; - } - - @Override - protected void buildAndRegisterTransientSessionTicketDefinition(final TicketCatalog plan, final TicketDefinition metadata) { - final CasTicketCatalogConfigurationValuesProvider configurationValuesProvider = new CasTicketCatalogConfigurationValuesProvider() {}; - metadata.getProperties().setStorageName(configurationValuesProvider.getTransientSessionStorageName().apply(casProperties)); - // changed from CAS: set one day of cache - metadata.getProperties().setStorageTimeout(86400); - super.buildAndRegisterTransientSessionTicketDefinition(plan, metadata); - } -} diff --git a/cas/cas-server/src/main/java/org/apereo/cas/web/flow/resolver/impl/DefaultCasDelegatingWebflowEventResolver.java b/cas/cas-server/src/main/java/org/apereo/cas/web/flow/resolver/impl/DefaultCasDelegatingWebflowEventResolver.java new file mode 100644 index 000000000..e62c60119 --- /dev/null +++ b/cas/cas-server/src/main/java/org/apereo/cas/web/flow/resolver/impl/DefaultCasDelegatingWebflowEventResolver.java @@ -0,0 +1,245 @@ +/** + * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2019-2020) + * and the signatories of the "VITAM - Accord du Contributeur" agreement. + * + * contact@programmevitam.fr + * + * This software is a computer program whose purpose is to implement + * implement a digital archiving front-office system for the secure and + * efficient high volumetry VITAM solution. + * + * This software is governed by the CeCILL-C license under French law and + * abiding by the rules of distribution of free software. You can use, + * modify and/ or redistribute the software under the terms of the CeCILL-C + * license as circulated by CEA, CNRS and INRIA at the following URL + * "http://www.cecill.info". + * + * As a counterpart to the access to the source code and rights to copy, + * modify and redistribute granted by the license, users are provided only + * with a limited warranty and the software's author, the holder of the + * economic rights, and the successive licensors have only limited + * liability. + * + * In this respect, the user's attention is drawn to the risks associated + * with loading, using, modifying and/or developing or reproducing the + * software by the user in light of its specific status of free software, + * that may mean that it is complicated to manipulate, and that also + * therefore means that it is reserved for developers and experienced + * professionals having in-depth computer knowledge. Users are therefore + * encouraged to load and test the software's suitability as regards their + * requirements in conditions enabling the security of their systems and/or + * data to be ensured and, more generally, to use and operate it in the + * same conditions as regards security. + * + * The fact that you are presently reading this means that you have had + * knowledge of the CeCILL-C license and that you accept its terms. + */ +package org.apereo.cas.web.flow.resolver.impl; + +import org.apereo.cas.adaptors.x509.authentication.principal.X509CertificateCredential; +import org.apereo.cas.audit.AuditableContext; +import org.apereo.cas.authentication.AuthenticationException; +import org.apereo.cas.authentication.Credential; +import org.apereo.cas.authentication.principal.Service; +import org.apereo.cas.authentication.principal.WebApplicationService; +import org.apereo.cas.services.RegisteredService; +import org.apereo.cas.ticket.AbstractTicketException; +import org.apereo.cas.util.CollectionUtils; +import org.apereo.cas.util.LoggingUtils; +import org.apereo.cas.util.function.FunctionUtils; +import org.apereo.cas.web.flow.CasWebflowConstants; +import org.apereo.cas.web.flow.resolver.CasDelegatingWebflowEventResolver; +import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver; +import org.apereo.cas.web.support.WebUtils; + +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Copy/paste of the original CAS class with a customisation to return an error for a failed X509 login process when mandatory. + */ +@Slf4j +public class DefaultCasDelegatingWebflowEventResolver extends AbstractCasWebflowEventResolver implements CasDelegatingWebflowEventResolver { + + private final List<CasWebflowEventResolver> orderedResolvers = new ArrayList<>(0); + + private final CasWebflowEventResolver selectiveResolver; + + // + // CUSTO + @Value("${vitamui.authn.x509.mandatory:false}") + private boolean x509AuthnMandatory; + // + // + + public DefaultCasDelegatingWebflowEventResolver(final CasWebflowEventResolutionConfigurationContext configurationContext, + final CasWebflowEventResolver selectiveResolver) { + super(configurationContext); + this.selectiveResolver = selectiveResolver; + } + + @Override + public Set<Event> resolveInternal(final RequestContext context) { + val credential = getCredentialFromContext(context); + val service = WebUtils.getService(context); + try { + + if (credential != null) { + val builder = getConfigurationContext().getAuthenticationSystemSupport() + .handleInitialAuthenticationTransaction(service, credential); + builder.getInitialAuthentication().ifPresent(authn -> { + WebUtils.putAuthenticationResultBuilder(builder, context); + WebUtils.putAuthentication(authn, context); + }); + } + + val registeredService = determineRegisteredServiceForEvent(context, service); + LOGGER.trace("Attempting to resolve candidate authentication events for service [{}]", service); + val resolvedEvents = resolveCandidateAuthenticationEvents(context, service, registeredService); + if (!resolvedEvents.isEmpty()) { + LOGGER.trace("Authentication events resolved for [{}] are [{}]. Selecting final event...", service, resolvedEvents); + WebUtils.putResolvedEventsAsAttribute(context, resolvedEvents); + val finalResolvedEvent = this.selectiveResolver.resolveSingle(context); + LOGGER.debug("The final authentication event resolved for [{}] is [{}]", service, finalResolvedEvent); + if (finalResolvedEvent != null) { + return CollectionUtils.wrapSet(finalResolvedEvent); + } + } else { + LOGGER.trace("No candidate authentication events were resolved for service [{}]", service); + } + + val builder = WebUtils.getAuthenticationResultBuilder(context); + if (builder == null) { + val msg = "Unable to locate authentication object in the webflow context"; + throw new IllegalArgumentException(new AuthenticationException(msg)); + } + return CollectionUtils.wrapSet(grantTicketGrantingTicketToAuthenticationResult(context, builder, service)); + } catch (final Exception exception) { + var event = returnAuthenticationExceptionEventIfNeeded(exception, credential, service); + if (event == null) { + FunctionUtils.doIf(LOGGER.isDebugEnabled(), + e -> LOGGER.debug(exception.getMessage(), exception), + e -> LoggingUtils.warn(LOGGER, exception.getMessage(), exception)) + .accept(exception); + event = newEvent(CasWebflowConstants.TRANSITION_ID_ERROR, exception); + } + val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return CollectionUtils.wrapSet(event); + } + } + + @Override + public void addDelegate(final CasWebflowEventResolver r) { + if (r != null) { + orderedResolvers.add(r); + } + } + + @Override + public void addDelegate(final CasWebflowEventResolver r, final int index) { + if (r != null) { + orderedResolvers.add(index, r); + } + } + + /** + * Resolve candidate authentication events set. + * + * @param context the context + * @param service the service + * @param registeredService the registered service + * @return the set + */ + protected Collection<Event> resolveCandidateAuthenticationEvents(final RequestContext context, + final Service service, + final RegisteredService registeredService) { + return this.orderedResolvers + .stream() + .map(resolver -> { + LOGGER.debug("Resolving candidate authentication event for service [{}] using [{}]", service, resolver.getName()); + return resolver.resolveSingle(context); + }) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(Event::getId)) + .collect(Collectors.toList()); + } + + private RegisteredService determineRegisteredServiceForEvent(final RequestContext context, final Service service) { + if (service == null) { + return null; + } + LOGGER.trace("Locating authentication event in the request context..."); + val authn = WebUtils.getAuthentication(context); + if (authn == null) { + val msg = "Unable to locate authentication object in the webflow context"; + throw new IllegalArgumentException(new AuthenticationException(msg)); + } + LOGGER.trace("Locating service [{}] in service registry to determine authentication policy", service); + val registeredService = getConfigurationContext().getServicesManager().findServiceBy(service); + LOGGER.trace("Enforcing access strategy policies for registered service [{}] and principal [{}]", + registeredService, authn.getPrincipal()); + val unauthorizedRedirectUrl = registeredService.getAccessStrategy().getUnauthorizedRedirectUrl(); + if (unauthorizedRedirectUrl != null) { + WebUtils.putUnauthorizedRedirectUrlIntoFlowScope(context, unauthorizedRedirectUrl); + } + + val audit = AuditableContext.builder() + .service(service) + .authentication(authn) + .registeredService(registeredService) + .build(); + val result = getConfigurationContext().getRegisteredServiceAccessStrategyEnforcer().execute(audit); + result.throwExceptionIfNeeded(); + return registeredService; + } + + private Event returnAuthenticationExceptionEventIfNeeded(final Exception exception, + final Credential credential, + final WebApplicationService service) { + + // + // CUSTO + if (x509AuthnMandatory) { + if (credential instanceof X509CertificateCredential) { + throw new IllegalArgumentException("Authentication failure for mandatory X509 login"); + } + } + // + // + + val result = (exception instanceof AuthenticationException || exception instanceof AbstractTicketException) + ? Optional.of(exception) + : (exception.getCause() instanceof AuthenticationException || exception.getCause() instanceof AbstractTicketException) + ? Optional.of(exception.getCause()) + : Optional.empty(); + return result + .map(Exception.class::cast) + .map(ex -> { + FunctionUtils.doIf(LOGGER.isDebugEnabled(), + e -> LOGGER.debug(ex.getMessage(), ex), + e -> LOGGER.warn(ex.getMessage())) + .accept(exception); + val attributes = new LocalAttributeMap<Serializable>(CasWebflowConstants.TRANSITION_ID_ERROR, ex); + attributes.put(Credential.class.getName(), credential); + attributes.put(WebApplicationService.class.getName(), service); + return newEvent(CasWebflowConstants.TRANSITION_ID_AUTHENTICATION_FAILURE, attributes); + }) + .orElse(null); + } +} diff --git a/cas/cas-server/src/main/java/org/pac4j/core/client/Clients.java b/cas/cas-server/src/main/java/org/pac4j/core/client/Clients.java deleted file mode 100644 index 943b3a422..000000000 --- a/cas/cas-server/src/main/java/org/pac4j/core/client/Clients.java +++ /dev/null @@ -1,234 +0,0 @@ -package org.pac4j.core.client; - -import java.util.*; - -import org.pac4j.core.authorization.generator.AuthorizationGenerator; -import org.pac4j.core.exception.TechnicalException; -import org.pac4j.core.http.ajax.AjaxRequestResolver; -import org.pac4j.core.http.callback.CallbackUrlResolver; -import org.pac4j.core.http.url.UrlResolver; -import org.pac4j.core.util.CommonHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Backported from pac4j v5.1. - */ -@SuppressWarnings({ "unchecked" }) -public class Clients { - - private static final Logger LOGGER = LoggerFactory.getLogger(Clients.class); - - private List<Client> clients; - - private Map<String, Client> clientsMap; - - private Integer oldClientsHash; - - private String callbackUrl; - - private AjaxRequestResolver ajaxRequestResolver; - - private UrlResolver urlResolver; - - private CallbackUrlResolver callbackUrlResolver; - - private List<AuthorizationGenerator> authorizationGenerators = new ArrayList<>(); - - private String defaultSecurityClients; - - public Clients() { - } - - public Clients(final String callbackUrl, final List<Client> clients) { - setCallbackUrl(callbackUrl); - setClients(clients); - } - - public Clients(final String callbackUrl, final Client... clients) { - setCallbackUrl(callbackUrl); - setClients(clients); - } - - public Clients(final List<Client> clients) { - setClients(clients); - } - - public Clients(final Client... clients) { - setClients(clients); - } - - /** - * Populate the resolvers, callback URL and authz generators in the Client - * if defined in Clients and not already in the Client itself. And check the client name. - */ - protected void init() { - // the clients list has changed or has not been initialized yet - if (oldClientsHash == null || oldClientsHash.intValue() != clients.hashCode()) { - synchronized (this) { - if (oldClientsHash == null || oldClientsHash.intValue() != clients.hashCode()) { - clientsMap = new HashMap<>(); - for (final var client : this.clients) { - final var name = client.getName(); - CommonHelper.assertNotBlank("name", name); - final var lowerTrimmedName = name.toLowerCase().trim(); - if (clientsMap.containsKey(lowerTrimmedName)) { - throw new TechnicalException("Duplicate name in clients: " + name); - } - clientsMap.put(lowerTrimmedName, client); - if (client instanceof IndirectClient) { - final var indirectClient = (IndirectClient) client; - if (this.callbackUrl != null && indirectClient.getCallbackUrl() == null) { - indirectClient.setCallbackUrl(this.callbackUrl); - } - if (this.urlResolver != null && indirectClient.getUrlResolver() == null) { - indirectClient.setUrlResolver(this.urlResolver); - } - if (this.callbackUrlResolver != null && indirectClient.getCallbackUrlResolver() == null) { - indirectClient.setCallbackUrlResolver(this.callbackUrlResolver); - } - if (this.ajaxRequestResolver != null && indirectClient.getAjaxRequestResolver() == null) { - indirectClient.setAjaxRequestResolver(this.ajaxRequestResolver); - } - } - final var baseClient = (BaseClient) client; - if (!authorizationGenerators.isEmpty()) { - baseClient.addAuthorizationGenerators(this.authorizationGenerators); - } - } - this.oldClientsHash = this.clients.hashCode(); - } - } - } - } - - /** - * Return the right client according to the specific name. - * - * @param name name of the client - * @return the right client - */ - public Optional<Client> findClient(final String name) { - CommonHelper.assertNotBlank("name", name); - init(); - - final var foundClient = clientsMap.get(name.toLowerCase().trim()); - LOGGER.debug("Found client: {} for name: {}", foundClient, name); - return Optional.ofNullable(foundClient); - } - - /** - * Use {@link #findClient(String)} instead. - */ - @Deprecated - public <C extends Client> Optional<C> findClient(final Class<C> clazz) { - CommonHelper.assertNotNull("clazz", clazz); - init(); - - C foundClient = null; - for (final var client : getClients()) { - if (clazz.isAssignableFrom(client.getClass())) { - foundClient = (C) client; - break; - } - } - LOGGER.debug("Found client: {} for class: {}", foundClient, clazz); - return Optional.ofNullable(foundClient); - } - - /** - * Find all the clients (initialized). - * - * @return all the clients (initialized) - */ - public List<Client> findAllClients() { - init(); - - return getClients(); - } - - public String getCallbackUrl() { - return this.callbackUrl; - } - - public void setCallbackUrl(final String callbackUrl) { - this.callbackUrl = callbackUrl; - } - - public void setClients(final List<Client> clients) { - CommonHelper.assertNotNull("clients", clients); - this.clients = clients; - } - - public void setClients(final Client... clients) { - CommonHelper.assertNotNull("clients", clients); - setClients(new ArrayList<>(Arrays.asList(clients))); - } - - public List<Client> getClients() { - return this.clients; - } - - public AjaxRequestResolver getAjaxRequestResolver() { - return ajaxRequestResolver; - } - - public void setAjaxRequestResolver(final AjaxRequestResolver ajaxRequestResolver) { - this.ajaxRequestResolver = ajaxRequestResolver; - } - - public CallbackUrlResolver getCallbackUrlResolver() { - return callbackUrlResolver; - } - - public void setCallbackUrlResolver(final CallbackUrlResolver callbackUrlResolver) { - this.callbackUrlResolver = callbackUrlResolver; - } - - public List<AuthorizationGenerator> getAuthorizationGenerators() { - return this.authorizationGenerators; - } - - public void setAuthorizationGenerators(final List<AuthorizationGenerator> authorizationGenerators) { - CommonHelper.assertNotNull("authorizationGenerators", authorizationGenerators); - this.authorizationGenerators = authorizationGenerators; - } - - public void setAuthorizationGenerators(final AuthorizationGenerator... authorizationGenerators) { - CommonHelper.assertNotNull("authorizationGenerators", authorizationGenerators); - this.authorizationGenerators = Arrays.asList(authorizationGenerators); - } - - public void setAuthorizationGenerator(final AuthorizationGenerator authorizationGenerator) { - addAuthorizationGenerator(authorizationGenerator); - } - - public void addAuthorizationGenerator(final AuthorizationGenerator authorizationGenerator) { - CommonHelper.assertNotNull("authorizationGenerator", authorizationGenerator); - this.authorizationGenerators.add(authorizationGenerator); - } - - public String getDefaultSecurityClients() { - return defaultSecurityClients; - } - - public void setDefaultSecurityClients(final String defaultSecurityClients) { - this.defaultSecurityClients = defaultSecurityClients; - } - - public UrlResolver getUrlResolver() { - return urlResolver; - } - - public void setUrlResolver(final UrlResolver urlResolver) { - this.urlResolver = urlResolver; - } - - @Override - public String toString() { - return CommonHelper.toNiceString(this.getClass(), "callbackUrl", this.callbackUrl, "clients", getClients(), - "ajaxRequestResolver", ajaxRequestResolver, "callbackUrlResolver", callbackUrlResolver, - "authorizationGenerators", authorizationGenerators, "defaultSecurityClients", defaultSecurityClients, - "urlResolver", this.urlResolver); - } -} diff --git a/cas/cas-server/src/main/resources/application.properties b/cas/cas-server/src/main/resources/application.properties index d38b16389..682fbe1e9 100644 --- a/cas/cas-server/src/main/resources/application.properties +++ b/cas/cas-server/src/main/resources/application.properties @@ -11,9 +11,9 @@ server.ssl.enabled=true server.port=8443 server.servlet.context-path=/cas server.max-http-header-size=2097152 +## CUSTO: NATIVE -> NONE server.forward-headers-strategy=NONE -server.connection-timeout=PT20S -# CUSTO: ALWAYS -> NEVER +## CUSTO: ALWAYS -> NEVER server.error.include-stacktrace=NEVER server.compression.enabled=true @@ -22,17 +22,21 @@ server.compression.mime-types=application/javascript,application/json,applicatio ## # CAS Web Application Embedded Tomcat Configuration # -server.tomcat.max-http-post-size=2097152 +server.tomcat.max-http-form-post-size=2097152 server.tomcat.basedir=build/tomcat +server.tomcat.connection-timeout=PT20S server.tomcat.accesslog.enabled=true server.tomcat.accesslog.pattern=%t %a "%r" %s (%D ms) server.tomcat.accesslog.suffix=.log -server.tomcat.min-spare-threads=10 -server.tomcat.max-threads=200 -server.tomcat.port-header=X-Forwarded-Port -server.tomcat.protocol-header=X-Forwarded-Proto -server.tomcat.protocol-header-https-value=https -server.tomcat.remote-ip-header=X-FORWARDED-FOR +server.tomcat.background-processor-delay=0s +server.tomcat.threads.min-spare=10 +server.tomcat.threads.max=200 + +server.tomcat.remoteip.port-header=X-Forwarded-Port +server.tomcat.remoteip.protocol-header=X-Forwarded-Proto +server.tomcat.remoteip.protocol-header-https-value=https +server.tomcat.remoteip.remote-ip-header=X-FORWARDED-FOR + server.tomcat.uri-encoding=UTF-8 server.tomcat.additional-tld-skip-patterns=*.jar @@ -44,9 +48,9 @@ spring.jmx.enabled=false ## # CAS Web Application Http Encoding Configuration # -spring.http.encoding.charset=UTF-8 -spring.http.encoding.enabled=true -spring.http.encoding.force=true +server.servlet.encoding.charset=UTF-8 +server.servlet.encoding.enabled=true +server.servlet.encoding.force=true ## # Allow configuration classes to override bean definitions from Spring Boot @@ -59,13 +63,15 @@ spring.main.lazy-initialization=false # spring.cloud.bus.enabled=false -# Indicates that systemPropertiesOverride can be used. Set to false to prevent users from changing the default accidentally. Default true. +# Indicates that systemPropertiesOverride can be used. Set to false to +# prevent users from changing the default accidentally. Default true. spring.cloud.config.allow-override=true # External properties should override system properties. spring.cloud.config.override-system-properties=false -# When allowOverride is true, external properties should take lowest priority, and not override any existing property sources (including local config files). +# When allowOverride is true, external properties should take lowest priority, +# and not override any existing property sources (including local config files). spring.cloud.config.override-none=false # spring.cloud.bus.refresh.enabled=true @@ -85,12 +91,6 @@ management.endpoints.web.base-path=/actuator management.endpoints.web.exposure.include=info,health,status,configurationMetadata management.endpoints.jmx.exposure.exclude=* -management.metrics.export.atlas.enabled=false -management.metrics.export.graphite.enabled=false -management.metrics.export.influx.enabled=false -management.metrics.export.newrelic.enabled=false -management.metrics.export.signalfx.enabled=false -management.metrics.export.wavefront.enabled=false # management.endpoints.web.exposure.include=* # management.endpoints.web.path-mapping.health=status @@ -104,7 +104,7 @@ spring.security.user.name=casuser # spring.security.user.roles= # Define a CAS-specific "WARN" status code and its order -management.health.status.order=WARN,DOWN,OUT_OF_SERVICE,UNKNOWN,UP +management.endpoint.health.status.order=WARN,DOWN,OUT_OF_SERVICE,UNKNOWN,UP # Define health indicator behavior (requires cas-server-core-monitor) management.health.memoryHealthIndicator.enabled=true @@ -118,13 +118,9 @@ spring.cloud.discovery.client.composite-indicator.enabled=false ## # CAS Web Application Session Configuration # -# 4 (hours) * 60 (minutes) * 60 (seconds) -server.servlet.session.timeout=PT14400S +server.servlet.session.timeout=PT300S server.servlet.session.cookie.http-only=true -server.servlet.session.cookie.secure=true server.servlet.session.tracking-modes=COOKIE -cas.ticket.tgt.hardTimeout.timeToKillInSeconds=14400 - ## # CAS Thymeleaf View Configuration # @@ -142,7 +138,7 @@ server.servlet.context-parameters.isLog4jAutoInitializationDisabled=true ## # CAS Metrics Configuration # -management.metrics.web.server.auto-time-requests=true +management.metrics.web.server.request.autotime.enabled=true management.metrics.export.atlas.enabled=false management.metrics.export.datadog.enabled=false @@ -164,6 +160,14 @@ management.metrics.enable.process.cpu=true management.metrics.enable.process.uptime=true management.metrics.enable.process.start.time=true +## +# CAS Swagger Configuration +# +springdoc.show-actuator=true +springdoc.model-and-view-allowed=true +springdoc.writer-with-default-pretty-printer=true +springdoc.swagger-ui.display-request-duration=true + ## # CAS AspectJ Configuration # @@ -171,17 +175,35 @@ spring.aop.auto=true spring.aop.proxy-target-class=true ## -# CAS View Settings +# CAS Authentication Credentials # -cas.view.cas2.v3ForwardCompatible=true +cas.authn.accept.enabled=true +cas.authn.accept.users=casuser::Mellon +cas.authn.accept.name=Static Credentials +## +# CAS Template Configuration +# +spring.groovy.template.enabled=false + +# CAS doesn't rely on this, Spring Boot will warn it is on if not set +spring.jpa.open-in-view=false +## +# Excluded auto-configuration classes +# +spring.autoconfigure.exclude= \ + org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ + org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\ + org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration + +# CUSTO: ## # Theme logo path and color hex. # May be overridden by YAML config # -# theme.vitam-logo=/path/to/vitam-logo/vitam-logo.png # theme.vitamui-logo-large=/path/to/vitam-logo/vitamui-logo-large.png +# theme.vitamui-favicon theme.vitamui-platform-name=VITAM-UI theme.primary=#702382 @@ -190,11 +212,3 @@ theme.background=#0F0D2D; # Inline-CSS added in every html template, on body tag theme.body.style=--vitamui-primary:${theme.primary};--vitamui-secondary:${theme.secondary};--vitamui-background:${theme.background}; - - - -## -# CAS Authentication Credentials -# -cas.authn.accept.users=casuser::Mellon -cas.authn.accept.name=Static Credentials diff --git a/cas/cas-server/src/main/resources/bootstrap.properties b/cas/cas-server/src/main/resources/bootstrap.properties index 40c0c886b..a24676691 100644 --- a/cas/cas-server/src/main/resources/bootstrap.properties +++ b/cas/cas-server/src/main/resources/bootstrap.properties @@ -4,6 +4,7 @@ # Name of the application for which environment settings and properties should be fetched. # This should map to a cas.yml or cas.properties file. spring.application.name=cas +# CUSTO: spring.profiles.active=default # Define where the configuration server is running @@ -26,4 +27,4 @@ health.config.enabled=true # If you wish to change the configuration directory, it's best to not # overlay this file, but specify the directory location via command-line # parameters or system properties via -D. -# cas.standalone.configurationDirectory=/etc/cas/config +# cas.standalone.configuration-directory=/etc/cas/config diff --git a/cas/cas-server/src/main/resources/overriden_messages.properties b/cas/cas-server/src/main/resources/overriden_messages.properties index f899e2cca..f67dcdbd2 100644 --- a/cas/cas-server/src/main/resources/overriden_messages.properties +++ b/cas/cas-server/src/main/resources/overriden_messages.properties @@ -1,3 +1,6 @@ +webjars.jqueryui.js=/webjars/jquery-ui/1.12.1/jquery-ui.min.js +webjars.fontawesomemin.css=/webjars/font-awesome/5.11.2/css/all.min.css + screen.accountlocked.heading=Your account has been locked for the next 20 minutes screen.accountlocked.message=Please wait before trying again diff --git a/cas/cas-server/src/main/resources/templates/casPwdView.html b/cas/cas-server/src/main/resources/templates/casPwdView.html index 0d3904fe8..c59470966 100644 --- a/cas/cas-server/src/main/resources/templates/casPwdView.html +++ b/cas/cas-server/src/main/resources/templates/casPwdView.html @@ -11,9 +11,8 @@ <link th:href="@{/css/cas.css}" rel="stylesheet"/> <script type="text/javascript" th:src="@{#{webjars.zxcvbn.js}}"></script> - <script type="text/javascript" th:src="@{#{webjars.jquerymin.js}}"></script> + <script type="text/javascript" th:src="@{#{webjars.jquery.js}}"></script> <script type="text/javascript" th:src="@{#{webjars.jqueryui.js}}"></script> - <script src="//www.google.com/recaptcha/api.js" async defer th:if="${recaptchaSiteKey}"></script> </head> <body th:styleappend="${@environment.getProperty('theme.body.style')}"> diff --git a/cas/cas-server/src/main/resources/templates/casPac4jStopWebflow.html b/cas/cas-server/src/main/resources/templates/delegated-authn/casDelegatedAuthnStopWebflow.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casPac4jStopWebflow.html rename to cas/cas-server/src/main/resources/templates/delegated-authn/casDelegatedAuthnStopWebflow.html diff --git a/cas/cas-server/src/main/resources/templates/casServiceErrorView.html b/cas/cas-server/src/main/resources/templates/error/casServiceErrorView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casServiceErrorView.html rename to cas/cas-server/src/main/resources/templates/error/casServiceErrorView.html diff --git a/cas/cas-server/src/main/resources/templates/fragments/scripts.html b/cas/cas-server/src/main/resources/templates/fragments/scripts.html index 5a764724c..4a11e81c4 100644 --- a/cas/cas-server/src/main/resources/templates/fragments/scripts.html +++ b/cas/cas-server/src/main/resources/templates/fragments/scripts.html @@ -1,9 +1,9 @@ <script type="text/javascript" th:src="@{#{webjars.jqueryui.js}}"></script> -<script src="//www.google.com/recaptcha/api.js" async defer th:if="${recaptchaSiteKey != null and recaptchaVersion=='v2'}"></script> -<script th:src="${'//www.google.com/recaptcha/api.js?render=' + recaptchaSiteKey}" th:if="${recaptchaSiteKey != null and recaptchaVersion == 'v3'}"></script> <script type="text/javascript" th:src="@{#{webjars.headmin.js}}"></script> -<script type="text/javascript" th:src="@{${#themes.code('cas.javascript.file')}}"></script> +<span th:remove="tag" th:each="file : ${#strings.arraySplit(#themes.code('cas.standard.js.file'), ',')}"> + <script type="text/javascript" th:src="@{${file}}"></script> +</span> <script th:inline="javascript"> head.ready(document, function () { @@ -53,25 +53,3 @@ function notifyResourcesAreLoaded(callback) { /*]]>*/ </script> - -<span th:if="${recaptchaVersion=='v2'}" th:remove="tag"> - <script type="text/javascript" th:if="${recaptchaSiteKey != null AND recaptchaInvisible != null AND recaptchaInvisible}" th:inline="javascript"> - function onRecaptchaV2Submit(token) { - $('#fm1').submit(); - } - </script> -</span> - -<span th:if="${recaptchaVersion=='v3'}" th:remove="tag"> - <script type="text/javascript" th:if="${recaptchaSiteKey != null}" th:inline="javascript"> - grecaptcha.ready(function() { - grecaptcha.execute(/*[[${recaptchaSiteKey}]]*/, {action: 'login'}) - .then(function(token) { - $("#g-recaptcha-token").val(token) - }); - }); - </script> -</span> - - - diff --git a/cas/cas-server/src/main/resources/templates/layout.html b/cas/cas-server/src/main/resources/templates/layout.html index dfe7f3965..01d9cdadd 100644 --- a/cas/cas-server/src/main/resources/templates/layout.html +++ b/cas/cas-server/src/main/resources/templates/layout.html @@ -14,7 +14,7 @@ <link rel="icon" type="image/x-icon" th:href="${application.vitamuiFavicon} ? 'data:image/png;base64,' + ${application.vitamuiFavicon} : @{/images/favicon.ico}"/> - <script type="text/javascript" th:src="@{#{webjars.jquerymin.js}}"></script> + <script type="text/javascript" th:src="@{#{webjars.jquery.js}}"></script> </head> diff --git a/cas/cas-server/src/main/resources/templates/casAccountDisabledView.html b/cas/cas-server/src/main/resources/templates/login-error/casAccountDisabledView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casAccountDisabledView.html rename to cas/cas-server/src/main/resources/templates/login-error/casAccountDisabledView.html diff --git a/cas/cas-server/src/main/resources/templates/casAccountLockedView.html b/cas/cas-server/src/main/resources/templates/login-error/casAccountLockedView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casAccountLockedView.html rename to cas/cas-server/src/main/resources/templates/login-error/casAccountLockedView.html diff --git a/cas/cas-server/src/main/resources/templates/casAuthenticationBlockedView.html b/cas/cas-server/src/main/resources/templates/login-error/casAuthenticationBlockedView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casAuthenticationBlockedView.html rename to cas/cas-server/src/main/resources/templates/login-error/casAuthenticationBlockedView.html diff --git a/cas/cas-server/src/main/resources/templates/casExpiredPassView.html b/cas/cas-server/src/main/resources/templates/login-error/casExpiredPassView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casExpiredPassView.html rename to cas/cas-server/src/main/resources/templates/login-error/casExpiredPassView.html diff --git a/cas/cas-server/src/main/resources/templates/casMustChangePassView.html b/cas/cas-server/src/main/resources/templates/login-error/casMustChangePassView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casMustChangePassView.html rename to cas/cas-server/src/main/resources/templates/login-error/casMustChangePassView.html diff --git a/cas/cas-server/src/main/resources/templates/casGenericSuccessView.html b/cas/cas-server/src/main/resources/templates/login/casGenericSuccessView.html similarity index 86% rename from cas/cas-server/src/main/resources/templates/casGenericSuccessView.html rename to cas/cas-server/src/main/resources/templates/login/casGenericSuccessView.html index b6eba3924..cd5ca473e 100644 --- a/cas/cas-server/src/main/resources/templates/casGenericSuccessView.html +++ b/cas/cas-server/src/main/resources/templates/login/casGenericSuccessView.html @@ -14,9 +14,8 @@ <link th:href="@{/css/cas.css}" rel="stylesheet"/> - <script type="text/javascript" th:src="@{#{webjars.jquerymin.js}}"></script> + <script type="text/javascript" th:src="@{#{webjars.jquery.js}}"></script> <script type="text/javascript" th:src="@{#{webjars.jqueryui.js}}"></script> - <script src="//www.google.com/recaptcha/api.js" async defer th:if="${recaptchaSiteKey}"></script> </head> <body class="bg-portal" th:styleappend="${@environment.getProperty('theme.body.style')}"> diff --git a/cas/cas-server/src/main/resources/templates/casLoginView.html b/cas/cas-server/src/main/resources/templates/login/casLoginView.html similarity index 94% rename from cas/cas-server/src/main/resources/templates/casLoginView.html rename to cas/cas-server/src/main/resources/templates/login/casLoginView.html index 987063907..1968e2b93 100644 --- a/cas/cas-server/src/main/resources/templates/casLoginView.html +++ b/cas/cas-server/src/main/resources/templates/login/casLoginView.html @@ -9,9 +9,8 @@ <link rel="icon" type="image/x-icon" th:href="${application.vitamuiFavicon} ? 'data:image/png;base64,' + ${application.vitamuiFavicon} : @{/images/favicon.ico}"> <link th:href="@{/css/cas.css}" rel="stylesheet"/> - <script type="text/javascript" th:src="@{#{webjars.jquerymin.js}}"></script> + <script type="text/javascript" th:src="@{#{webjars.jquery.js}}"></script> <script type="text/javascript" th:src="@{#{webjars.jqueryui.js}}"></script> - <script src="//www.google.com/recaptcha/api.js" async defer th:if="${recaptchaSiteKey}"></script> </head> <body th:styleappend="${@environment.getProperty('theme.body.style')}"> @@ -64,8 +63,8 @@ </div> <div class="form-control" th:if="${!#strings.isEmpty(superUser)}"> - <label for="surrogate" th:utext="#{screen.welcome.label.surrogate}"/> <span th:utext="${surrogate}" /><br /> - <label for="superUser" th:utext="#{screen.welcome.label.superuser}"/> <span th:utext="${superUser}" /><br /> + <label for="surrogate" th:utext="#{screen.welcome.label.surrogate}"/> <span th:text="${surrogate}" /><br /> + <label for="superUser" th:utext="#{screen.welcome.label.superuser}"/> <span th:text="${superUser}" /><br /> <input type="hidden" id="username" name="username" th:value="${surrogate + ',' + superUser}" /> <br><br> diff --git a/cas/cas-server/src/main/resources/templates/casLogoutView.html b/cas/cas-server/src/main/resources/templates/logout/casLogoutView.html similarity index 89% rename from cas/cas-server/src/main/resources/templates/casLogoutView.html rename to cas/cas-server/src/main/resources/templates/logout/casLogoutView.html index 0b456dcab..f88673875 100644 --- a/cas/cas-server/src/main/resources/templates/casLogoutView.html +++ b/cas/cas-server/src/main/resources/templates/logout/casLogoutView.html @@ -9,9 +9,8 @@ <link rel="icon" type="image/x-icon" th:href="${application.vitamuiFavicon} ? 'data:image/png;base64,' + ${application.vitamuiFavicon} : @{/images/favicon.ico}"> <link th:href="@{/css/cas.css}" rel="stylesheet"/> - <script type="text/javascript" th:src="@{#{webjars.jquerymin.js}}"></script> + <script type="text/javascript" th:src="@{#{webjars.jquery.js}}"></script> <script type="text/javascript" th:src="@{#{webjars.jqueryui.js}}"></script> - <script src="//www.google.com/recaptcha/api.js" async defer th:if="${recaptchaSiteKey}"></script> </head> <body th:styleappend="${@environment.getProperty('theme.body.style')}"> <div class="login"> diff --git a/cas/cas-server/src/main/resources/templates/casPropagateLogoutView.html b/cas/cas-server/src/main/resources/templates/logout/casPropagateLogoutView.html similarity index 95% rename from cas/cas-server/src/main/resources/templates/casPropagateLogoutView.html rename to cas/cas-server/src/main/resources/templates/logout/casPropagateLogoutView.html index fa2bb8f65..aafc04861 100644 --- a/cas/cas-server/src/main/resources/templates/casPropagateLogoutView.html +++ b/cas/cas-server/src/main/resources/templates/logout/casPropagateLogoutView.html @@ -14,9 +14,8 @@ <link rel="icon" type="image/x-icon" th:href="${application.vitamuiFavicon} ? 'data:image/png;base64,' + ${application.vitamuiFavicon} : @{/images/favicon.ico}"/> - <script type="text/javascript" th:src="@{#{webjars.jquerymin.js}}"></script> + <script type="text/javascript" th:src="@{#{webjars.jquery.js}}"></script> <script type="text/javascript" th:src="@{#{webjars.jqueryui.js}}"></script> - <script src="//www.google.com/recaptcha/api.js" async defer th:if="${recaptchaSiteKey}"></script> <script th:inline="javascript"> /*<![CDATA[*/ diff --git a/cas/cas-server/src/main/resources/templates/casPasswordUpdateSuccessView.html b/cas/cas-server/src/main/resources/templates/password-reset/casPasswordUpdateSuccessView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casPasswordUpdateSuccessView.html rename to cas/cas-server/src/main/resources/templates/password-reset/casPasswordUpdateSuccessView.html diff --git a/cas/cas-server/src/main/resources/templates/casResetPasswordErrorView.html b/cas/cas-server/src/main/resources/templates/password-reset/casResetPasswordErrorView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casResetPasswordErrorView.html rename to cas/cas-server/src/main/resources/templates/password-reset/casResetPasswordErrorView.html diff --git a/cas/cas-server/src/main/resources/templates/casResetPasswordSendInstructionsView.html b/cas/cas-server/src/main/resources/templates/password-reset/casResetPasswordSendInstructionsView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casResetPasswordSendInstructionsView.html rename to cas/cas-server/src/main/resources/templates/password-reset/casResetPasswordSendInstructionsView.html diff --git a/cas/cas-server/src/main/resources/templates/casResetPasswordSentInstructionsView.html b/cas/cas-server/src/main/resources/templates/password-reset/casResetPasswordSentInstructionsView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casResetPasswordSentInstructionsView.html rename to cas/cas-server/src/main/resources/templates/password-reset/casResetPasswordSentInstructionsView.html diff --git a/cas/cas-server/src/main/resources/templates/casSimpleMfaLoginView.html b/cas/cas-server/src/main/resources/templates/simple-mfa/casSimpleMfaLoginView.html similarity index 100% rename from cas/cas-server/src/main/resources/templates/casSimpleMfaLoginView.html rename to cas/cas-server/src/main/resources/templates/simple-mfa/casSimpleMfaLoginView.html diff --git a/cas/cas-server/src/main/resources/webflow/mfa-simple/mfa-simple-custom-webflow.xml b/cas/cas-server/src/main/resources/webflow/mfa-simple/mfa-simple-custom-webflow.xml deleted file mode 100644 index 412b7296e..000000000 --- a/cas/cas-server/src/main/resources/webflow/mfa-simple/mfa-simple-custom-webflow.xml +++ /dev/null @@ -1,61 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<flow xmlns="http://www.springframework.org/schema/webflow" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://www.springframework.org/schema/webflow - http://www.springframework.org/schema/webflow/spring-webflow.xsd"> - - <var name="credential" class="org.apereo.cas.mfa.simple.CasSimpleMultifactorTokenCredential" /> - <on-start> - <evaluate expression="initialFlowSetupAction" /> - </on-start> - - <action-state id="initializeLoginForm"> - <evaluate expression="initializeLoginAction" /> - <transition on="success" to="sendSimpleToken"/> - </action-state> - - <action-state id="sendSimpleToken"> - <evaluate expression="mfaSimpleMultifactorSendTokenAction" /> - <transition on="success" to="viewLoginForm"/> - <transition on="error" to="unavailable"/> - <!-- custo --> - <transition on="missingPhone" to="missingPhone"/> - </action-state> - - <!-- custo --> - <view-state id="missingPhone" view="casSmsMissingPhoneView" /> - - <view-state id="viewLoginForm" view="casSimpleMfaLoginView" model="credential"> - <binder> - <binding property="token" required="true"/> - </binder> - <on-entry> - <set name="viewScope.principal" value="conversationScope.authentication.principal" /> - </on-entry> - <!-- custo --> - <transition on="submit" bind="true" validate="true" to="intermediateSubmit"/> - <transition on="resend" bind="false" validate="false" to="sendSimpleToken"/> - - </view-state> - - <!-- custo --> - <action-state id="intermediateSubmit"> - <evaluate expression="checkMfaTokenAction" /> - <transition on="success" to="realSubmit"/> - <transition on="error" to="codeExpired"/> - </action-state> - - <!-- custo --> - <view-state id="codeExpired" view="casSmsCodeExpiredView"> - <transition on="resend" to="initializeLoginForm"/> - </view-state> - - <action-state id="realSubmit"> - <evaluate expression="oneTimeTokenAuthenticationWebflowAction" /> - <transition on="success" to="success" /> - <transition on="error" to="viewLoginForm" /> - </action-state> - - <end-state id="success" /> - <end-state id="unavailable" /> -</flow> diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/BaseWebflowActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/BaseWebflowActionTest.java index 5dc4fb86e..5791a4b1b 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/BaseWebflowActionTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/BaseWebflowActionTest.java @@ -64,6 +64,7 @@ public abstract class BaseWebflowActionTest { when(context.getActiveFlow()).thenReturn(flow); when(context.getRequestScope()).thenReturn(flowParameters); + when(context.getFlashScope()).thenReturn(flowParameters); when(context.getConversationScope()).thenReturn(flowParameters); final ServletExternalContext servletExternalContext = mock(ServletExternalContext.class); diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationServiceTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationServiceTest.java index 05df354af..ee95ad49e 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationServiceTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/IamSurrogateAuthenticationServiceTest.java @@ -8,8 +8,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Arrays; -import java.util.HashMap; -import java.util.List; import org.apereo.cas.authentication.principal.DefaultPrincipalFactory; import org.apereo.cas.authentication.principal.Principal; @@ -29,8 +27,6 @@ import fr.gouv.vitamui.iam.common.enums.SubrogationStatusEnum; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; import lombok.val; -import lombok.val; - /** * Tests {@link IamSurrogateAuthenticationService}. */ @@ -90,11 +86,8 @@ public final class IamSurrogateAuthenticationServiceTest { } private Principal principal() { - val attributes = new HashMap<String, List<Object>>(); - attributes.put(UserPrincipalResolver.SUPER_USER_ID_ATTRIBUTE, Arrays.asList(SU_ID)); - val factory = new DefaultPrincipalFactory(); - return factory.createPrincipal("x", attributes); + return factory.createPrincipal(SU_ID); } private SubrogationDto surrogation() { diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolverTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolverTest.java index ec01363bf..e78af99e6 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolverTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/authentication/UserPrincipalResolverTest.java @@ -4,6 +4,7 @@ import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.cas.util.Constants; import fr.gouv.vitamui.cas.util.Utils; import fr.gouv.vitamui.cas.BaseWebflowActionTest; +import fr.gouv.vitamui.cas.x509.X509AttributeMapping; import fr.gouv.vitamui.commons.api.CommonConstants; import fr.gouv.vitamui.commons.api.domain.AddressDto; import fr.gouv.vitamui.commons.api.domain.GroupDto; @@ -19,6 +20,7 @@ import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; import fr.gouv.vitamui.iam.external.client.CasExternalRestClient; import lombok.val; +import org.apereo.cas.adaptors.x509.authentication.principal.X509CertificateCredential; import org.apereo.cas.authentication.SurrogateUsernamePasswordCredential; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; import org.apereo.cas.authentication.principal.ClientCredential; @@ -33,6 +35,8 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; +import java.security.Principal; +import java.security.cert.X509Certificate; import java.util.*; import static fr.gouv.vitamui.commons.api.CommonConstants.IDENTIFIER_ATTRIBUTE; @@ -94,8 +98,10 @@ public final class UserPrincipalResolverTest extends BaseWebflowActionTest { sessionStore = mock(SessionStore.class); identityProviderHelper = mock(IdentityProviderHelper.class); providersService = mock(ProvidersService.class); + val emailMapping = new X509AttributeMapping("subject_dn", null, null); + val identifierMapping = new X509AttributeMapping("issuer_dn", null, null); resolver = new UserPrincipalResolver(principalFactory, casExternalRestClient, utils, sessionStore, - identityProviderHelper, providersService); + identityProviderHelper, providersService, emailMapping, identifierMapping, ""); } @Test @@ -113,6 +119,28 @@ public final class UserPrincipalResolverTest extends BaseWebflowActionTest { assertNull(attributes.get(SUPER_USER_ATTRIBUTE)); } + @Test + public void testResolveX509() { + when(casExternalRestClient.getUser(any(ExternalHttpContext.class), eq(USERNAME), eq(null), eq(Optional.of(IDENTIFIER)), + eq(Optional.of(CommonConstants.AUTH_TOKEN_PARAMETER)))).thenReturn(userProfile(UserStatusEnum.ENABLED)); + val cert = mock(X509Certificate.class); + val subjectDn = mock(Principal.class); + when(subjectDn.getName()).thenReturn(USERNAME); + when(cert.getSubjectDN()).thenReturn(subjectDn); + val issuerDn = mock(Principal.class); + when(issuerDn.getName()).thenReturn(IDENTIFIER); + when(cert.getIssuerDN()).thenReturn(issuerDn); + + val principal = resolver.resolve(new X509CertificateCredential(new X509Certificate[] { cert }), + Optional.of(principalFactory.createPrincipal(USERNAME)), Optional.empty()); + + assertEquals(USERNAME_ID, principal.getId()); + final Map<String, List<Object>> attributes = principal.getAttributes(); + assertEquals(USERNAME, attributes.get(CommonConstants.EMAIL_ATTRIBUTE).get(0)); + assertEquals(Arrays.asList(ROLE_NAME), attributes.get(CommonConstants.ROLES_ATTRIBUTE)); + assertNull(attributes.get(SUPER_USER_ATTRIBUTE)); + } + @Test public void testResolveAuthnDelegation() { val provider = new IdentityProviderDto(); diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementServiceTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementServiceTest.java index 1d1597467..dd035a39b 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementServiceTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/pm/IamPasswordManagementServiceTest.java @@ -61,6 +61,7 @@ import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService; import org.apereo.cas.configuration.model.support.pm.PasswordManagementProperties; import org.apereo.cas.pm.PasswordChangeRequest; +import org.apereo.cas.pm.PasswordManagementQuery; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -128,7 +129,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes private PasswordManagementProperties passwordManagementProperties; - @Value("${cas.authn.pm.policyPattern}") + @Value("${cas.authn.pm.core.policy-pattern}") private String policyPattern; private PasswordConfiguration passwordConfiguration; @@ -144,7 +145,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes identityProviderDto = new IdentityProviderDto(); identityProviderDto.setInternal(true); passwordManagementProperties = new PasswordManagementProperties(); - passwordManagementProperties.setPolicyPattern(encode(policyPattern)); + passwordManagementProperties.getCore().setPolicyPattern(encode(policyPattern)); passwordConfiguration = new PasswordConfiguration(); passwordConfiguration.setCheckOccurrence(true); passwordConfiguration.setOccurrencesCharsNumber(4); @@ -307,7 +308,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(EMAIL), eq(Optional.empty()))) .thenReturn(user(UserStatusEnum.ENABLED)); - assertEquals(EMAIL, service.findEmail(EMAIL)); + assertEquals(EMAIL, service.findEmail(PasswordManagementQuery.builder().username(EMAIL).build())); } @Test @@ -315,7 +316,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(EMAIL), eq(Optional.empty()))) .thenThrow(new BadRequestException("error")); - assertNull(service.findEmail(EMAIL)); + assertNull(service.findEmail(PasswordManagementQuery.builder().username(EMAIL).build())); } @Test @@ -323,7 +324,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(EMAIL), eq(Optional.empty()))) .thenReturn(null); - assertNull(service.findEmail(EMAIL)); + assertNull(service.findEmail(PasswordManagementQuery.builder().username(EMAIL).build())); } @Test @@ -331,7 +332,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(EMAIL), eq(Optional.empty()))) .thenReturn(user(UserStatusEnum.DISABLED)); - assertNull(service.findEmail(EMAIL)); + assertNull(service.findEmail(PasswordManagementQuery.builder().username(EMAIL).build())); } @Test(expected = UnsupportedOperationException.class) @@ -339,7 +340,7 @@ public final class IamPasswordManagementServiceTest extends BaseWebflowActionTes when(casExternalRestClient.getUserByEmail(any(ExternalHttpContext.class), eq(EMAIL), eq(Optional.empty()))) .thenReturn(user(UserStatusEnum.ENABLED)); - service.getSecurityQuestions(EMAIL); + service.getSecurityQuestions(PasswordManagementQuery.builder().username(EMAIL).build()); } private UserDto user(final UserStatusEnum status) { diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/provider/ProvidersServiceTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/provider/ProvidersServiceTest.java index 558d83819..27149c75d 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/provider/ProvidersServiceTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/provider/ProvidersServiceTest.java @@ -7,7 +7,7 @@ import fr.gouv.vitamui.iam.common.dto.common.ProviderEmbeddedOptions; import fr.gouv.vitamui.iam.external.client.IdentityProviderExternalRestClient; import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; -import fr.gouv.vitamui.iam.common.utils.Saml2ClientBuilder; +import fr.gouv.vitamui.iam.common.utils.Pac4jClientBuilder; import lombok.val; import org.junit.Before; import org.junit.Test; @@ -49,15 +49,11 @@ public final class ProvidersServiceTest { @Before public void setUp() { - service = new ProvidersService(); val clients = new Clients(); - service.setClients(clients); - val builder = mock(Saml2ClientBuilder.class); - service.setSaml2ClientBuilder(builder); + val builder = mock(Pac4jClientBuilder.class); restClient = mock(IdentityProviderExternalRestClient.class); - service.setIdentityProviderExternalRestClient(restClient); val utils = new Utils(null, 0, null, null); - service.setUtils(utils); + service = new ProvidersService(clients, restClient, builder, utils); provider = new IdentityProviderDto(); provider.setId(PROVIDER_ID); @@ -66,7 +62,7 @@ public final class ProvidersServiceTest { saml2Client = new SAML2Client(); saml2Client.setName("testSAML2Client"); - when(builder.buildSaml2Client(provider)).thenReturn(Optional.of(saml2Client)); + when(builder.buildClient(provider)).thenReturn(Optional.of(saml2Client)); identityProviderHelper = new IdentityProviderHelper(); } @@ -84,7 +80,7 @@ public final class ProvidersServiceTest { val userProvider = identityProviderHelper.findByUserIdentifier(service.getProviders(), "jerome@company.com"); assertTrue(userProvider.isPresent()); assertEquals(PROVIDER_ID, userProvider.get().getId()); - assertEquals(saml2Client, ((SamlIdentityProviderDto) userProvider.get()).getSaml2Client()); + assertEquals(saml2Client, ((Pac4jClientIdentityProviderDto) userProvider.get()).getClient()); } @Test diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenActionTest.java new file mode 100644 index 000000000..e80a88ccc --- /dev/null +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CheckMfaTokenActionTest.java @@ -0,0 +1,74 @@ +package fr.gouv.vitamui.cas.webflow.actions; + +import fr.gouv.vitamui.cas.BaseWebflowActionTest; +import fr.gouv.vitamui.commons.api.identity.ServerIdentityAutoConfiguration; +import lombok.val; +import org.apereo.cas.mfa.simple.CasSimpleMultifactorTokenCredential; +import org.apereo.cas.mfa.simple.ticket.CasSimpleMultifactorAuthenticationTicket; +import org.apereo.cas.ticket.registry.TicketRegistry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Tests {@link CheckMfaTokenAction}. + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = ServerIdentityAutoConfiguration.class) +@TestPropertySource(locations = "classpath:/application-test.properties") +public class CheckMfaTokenActionTest extends BaseWebflowActionTest { + + private static final String TOKEN = "000"; + + private TicketRegistry ticketRegistry; + + private CheckMfaTokenAction action; + + private CasSimpleMultifactorAuthenticationTicket ticket; + + @Override + @Before + public void setUp() { + super.setUp(); + + ticketRegistry = mock(TicketRegistry.class); + action = new CheckMfaTokenAction(ticketRegistry); + + val credential = mock(CasSimpleMultifactorTokenCredential.class); + when(credential.getToken()).thenReturn(TOKEN); + when(credential.getId()).thenReturn(TOKEN); + flowParameters.put("credential", credential); + + ticket = mock(CasSimpleMultifactorAuthenticationTicket.class); + when(ticketRegistry.getTicket("CASMFA-" + TOKEN, CasSimpleMultifactorAuthenticationTicket.class)).thenReturn(ticket); + } + + @Test + public void tokenNotExpired() { + val creationDate = ZonedDateTime.now().minus(30, ChronoUnit.SECONDS); + when(ticket.getCreationTime()).thenReturn(creationDate); + + val event = action.doExecute(context); + + assertEquals("success", event.getId()); + } + + @Test + public void tokenExpired() { + val creationDate = ZonedDateTime.now().minus(70, ChronoUnit.SECONDS); + when(ticket.getCreationTime()).thenReturn(creationDate); + + val event = action.doExecute(context); + + assertEquals("error", event.getId()); + } +} diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationActionTest.java index 2c515caf3..efae1c98d 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationActionTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/CustomDelegatedClientAuthenticationActionTest.java @@ -3,36 +3,26 @@ package fr.gouv.vitamui.cas.webflow.actions; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; -import java.util.ArrayList; - import fr.gouv.vitamui.cas.BaseWebflowActionTest; import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.cas.util.Utils; import fr.gouv.vitamui.iam.common.utils.IdentityProviderHelper; -import org.apereo.cas.CentralAuthenticationService; -import org.apereo.cas.audit.AuditableExecution; -import org.apereo.cas.authentication.AuthenticationServiceSelectionPlan; -import org.apereo.cas.authentication.AuthenticationSystemSupport; -import org.apereo.cas.authentication.adaptive.AdaptiveAuthenticationPolicy; +import lombok.val; import org.apereo.cas.authentication.credential.UsernamePasswordCredential; -import org.apereo.cas.configuration.CasConfigurationProperties; -import org.apereo.cas.services.ServicesManager; import org.apereo.cas.ticket.registry.TicketRegistry; -import org.apereo.cas.web.DelegatedClientWebflowManager; -import org.apereo.cas.web.flow.SingleSignOnParticipationStrategy; -import org.apereo.cas.web.flow.resolver.CasDelegatingWebflowEventResolver; -import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver; +import org.apereo.cas.web.flow.DelegatedClientAuthenticationConfigurationContext; +import org.apereo.cas.web.flow.DelegatedClientIdentityProviderConfigurationProducer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.pac4j.core.client.Clients; -import org.pac4j.core.context.session.SessionStore; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import fr.gouv.vitamui.commons.api.identity.ServerIdentityAutoConfiguration; +import static org.mockito.Mockito.*; + /** * Tests {@link CustomDelegatedClientAuthenticationAction}. * @@ -54,14 +44,10 @@ public final class CustomDelegatedClientAuthenticationActionTest extends BaseWeb public void setUp() { super.setUp(); - action = new CustomDelegatedClientAuthenticationAction(mock(CasDelegatingWebflowEventResolver.class), - mock(CasWebflowEventResolver.class), mock(AdaptiveAuthenticationPolicy.class), - mock(Clients.class), mock(ServicesManager.class), mock(AuditableExecution.class), - mock(DelegatedClientWebflowManager.class), mock(AuthenticationSystemSupport.class), - mock(CasConfigurationProperties.class), mock(AuthenticationServiceSelectionPlan.class), - mock(CentralAuthenticationService.class), mock(SingleSignOnParticipationStrategy.class), - mock(SessionStore.class), new ArrayList<>(), mock(IdentityProviderHelper.class), - mock(ProvidersService.class), mock(Utils.class), mock(TicketRegistry.class), "", ","); + val configContext = mock(DelegatedClientAuthenticationConfigurationContext.class); + when(configContext.getDelegatedClientIdentityProvidersProducer()).thenReturn(mock(DelegatedClientIdentityProviderConfigurationProducer.class)); + action = new CustomDelegatedClientAuthenticationAction(configContext, mock(IdentityProviderHelper.class), + mock(ProvidersService.class), mock(Utils.class), mock(TicketRegistry.class), "", ","); } @Test diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherActionTest.java index 3253377f3..baef17e0d 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherActionTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/DispatcherActionTest.java @@ -27,7 +27,7 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.webflow.execution.Event; -import fr.gouv.vitamui.cas.provider.SamlIdentityProviderDto; +import fr.gouv.vitamui.cas.provider.Pac4jClientIdentityProviderDto; import fr.gouv.vitamui.cas.provider.ProvidersService; import fr.gouv.vitamui.commons.api.identity.ServerIdentityAutoConfiguration; import fr.gouv.vitamui.iam.common.dto.IdentityProviderDto; @@ -57,7 +57,7 @@ public final class DispatcherActionTest extends BaseWebflowActionTest { private DispatcherAction action; - private SamlIdentityProviderDto provider; + private Pac4jClientIdentityProviderDto provider; @Override @Before @@ -72,7 +72,7 @@ public final class DispatcherActionTest extends BaseWebflowActionTest { action = new DispatcherAction(providersService, identityProviderHelper, casExternalRestClient, ",", utils, mock(SessionStore.class)); final SAML2Client client = new SAML2Client(); - provider = new SamlIdentityProviderDto(new IdentityProviderDto(), client); + provider = new Pac4jClientIdentityProviderDto(new IdentityProviderDto(), client); provider.setInternal(true); when(identityProviderHelper.findByUserIdentifier(any(LinkedList.class), eq(USERNAME))) .thenReturn(Optional.of(provider)); diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionActionTest.java index 298e658c5..765e8917a 100644 --- a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionActionTest.java +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/GeneralTerminateSessionActionTest.java @@ -1,5 +1,6 @@ package fr.gouv.vitamui.cas.webflow.actions; +import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.logout.LogoutManager; import org.apereo.cas.services.RegexRegisteredService; import org.apereo.cas.services.ServicesManager; @@ -27,9 +28,8 @@ public final class GeneralTerminateSessionActionTest { final LogoutManager logoutManager = mock(LogoutManager.class); - final GeneralTerminateSessionAction action = new GeneralTerminateSessionAction(null, null, null, null); - action.setServicesManager(servicesManager); - action.setLogoutManager(logoutManager); + final GeneralTerminateSessionAction action = new GeneralTerminateSessionAction(null, null, + null, logoutManager, null, null, null, servicesManager, new CasConfigurationProperties(), null); action.performGeneralLogout("tgtId"); } diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/TriggerChangePasswordActionTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/TriggerChangePasswordActionTest.java new file mode 100644 index 000000000..2d030a75b --- /dev/null +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/webflow/actions/TriggerChangePasswordActionTest.java @@ -0,0 +1,59 @@ +package fr.gouv.vitamui.cas.webflow.actions; + +import fr.gouv.vitamui.cas.BaseWebflowActionTest; +import fr.gouv.vitamui.cas.util.Utils; +import fr.gouv.vitamui.commons.api.identity.ServerIdentityAutoConfiguration; +import lombok.val; +import org.apereo.cas.authentication.principal.Principal; +import org.apereo.cas.ticket.registry.TicketRegistrySupport; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +/** + * Tests {@link TriggerChangePasswordAction}. + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = ServerIdentityAutoConfiguration.class) +@TestPropertySource(locations = "classpath:/application-test.properties") +public class TriggerChangePasswordActionTest extends BaseWebflowActionTest { + + private TriggerChangePasswordAction action; + + @Override + @Before + public void setUp() { + super.setUp(); + + val tgtId = "TGT-1"; + + flowParameters.put("ticketGrantingTicketId", tgtId); + + val ticketRegistrySupport = mock(TicketRegistrySupport.class); + action = new TriggerChangePasswordAction(ticketRegistrySupport, mock(Utils.class)); + + when(ticketRegistrySupport.getAuthenticatedPrincipalFrom(tgtId)).thenReturn(mock(Principal.class)); + } + + @Test + public void changePassword() { + requestParameters.put("doChangePassword", "yes"); + + val event = action.doExecute(context); + + assertEquals("changePassword", event.getId()); + } + + @Test + public void dontChangePassword() { + val event = action.doExecute(context); + + assertEquals("continue", event.getId()); + } +} diff --git a/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/x509/CertificateParserTest.java b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/x509/CertificateParserTest.java new file mode 100644 index 000000000..48b8c54c6 --- /dev/null +++ b/cas/cas-server/src/test/java/fr/gouv/vitamui/cas/x509/CertificateParserTest.java @@ -0,0 +1,59 @@ +package fr.gouv.vitamui.cas.x509; + +import org.apereo.cas.util.crypto.CertUtils; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Tests {@link CertificateParser}. + */ +public class CertificateParserTest { + + private static final String ISSUER_DN = "EMAILADDRESS=admin@vitam, CN=admin, O=Vitam, ST=Some-State, C=FR"; + private static final String SUBJECT_DN = "EMAILADDRESS=bob@email, CN=bob, OU=IT, O=MyCompany, ST=Some-State, C=FR"; + + private static X509Certificate cert; + + @BeforeClass + public static void beforeClass() throws IOException { + cert = CertUtils.readCertificate(new FileInputStream("src/test/resources/client.crt")); + } + + @Test + public void testIssuerDnNoParsingExpansion() throws CertificateParsingException { + assertEquals(ISSUER_DN, CertificateParser.extract(cert, new X509AttributeMapping(X509CertificateAttributes.ISSUER_DN.toString(), null, null))); + } + + @Test + public void testIssuerDnParsingNoExpansion() throws CertificateParsingException { + assertEquals("Vitam", CertificateParser.extract(cert, new X509AttributeMapping(X509CertificateAttributes.ISSUER_DN.toString(), ".*O=(.*), ST=.*", null))); + } + + @Test + public void testIssuerDnParsingExpansion() throws CertificateParsingException { + assertEquals("Vitam@email", CertificateParser.extract(cert, new X509AttributeMapping(X509CertificateAttributes.ISSUER_DN.toString(), ".*O=(.*), ST=.*", "{0}@email"))); + } + + @Test + public void testSubjectDnNoParsingExpansion() throws CertificateParsingException { + assertEquals(SUBJECT_DN, CertificateParser.extract(cert, new X509AttributeMapping(X509CertificateAttributes.SUBJECT_DN.toString(), null, null))); + } + + @Test + public void testSubjectAlternateNameNoParsingExpansion() { + try { + CertificateParser.extract(cert, new X509AttributeMapping(X509CertificateAttributes.SUBJECT_ALTERNATE_NAME.toString(), null, null)); + fail(); + } catch (final CertificateParsingException e) { + assertEquals("Cannot find X509 value for: SUBJECT_ALTERNATE_NAME", e.getMessage()); + } + } +} diff --git a/cas/cas-server/src/test/resources/application-test.properties b/cas/cas-server/src/test/resources/application-test.properties index 3977e9b8f..690d4e00a 100644 --- a/cas/cas-server/src/test/resources/application-test.properties +++ b/cas/cas-server/src/test/resources/application-test.properties @@ -1,5 +1,5 @@ -server-identity.identityName=CAS -server-identity.identityRole=SSO -server-identity.identityServerId=1 +server-identity.identity-name=CAS +server-identity.identity-role=SSO +server-identity.identity-server-id=1 -cas.authn.pm.policyPattern=^(?=(.*[$@!%*#\u00a3?&=\\-\\/:;\\(\\)\"\\.,\\?!'\\[\\]{}^\\+\\=_\\\\\\|~<>`]){2})(?=(?:.*[a-z]){2})(?=(?:.*[A-Z]){2})(?=(?:.*[\\d]){2})[A-Za-z\u00c0-\u00ff0-9$@!%*#\u00a3?&=\\-\\/:;\\(\\)\"\\.,\\?!'\\[\\]{}^\\+\\=_\\\\\\|~<>`]{12,}$ +cas.authn.pm.core.policy-pattern=^(?=(.*[$@!%*#\u00a3?&=\\-\\/:;\\(\\)\"\\.,\\?!'\\[\\]{}^\\+\\=_\\\\\\|~<>`]){2})(?=(?:.*[a-z]){2})(?=(?:.*[A-Z]){2})(?=(?:.*[\\d]){2})[A-Za-z\u00c0-\u00ff0-9$@!%*#\u00a3?&=\\-\\/:;\\(\\)\"\\.,\\?!'\\[\\]{}^\\+\\=_\\\\\\|~<>`]{12,}$ diff --git a/cas/cas-server/src/test/resources/client.crt b/cas/cas-server/src/test/resources/client.crt new file mode 100644 index 000000000..8c4810bf2 --- /dev/null +++ b/cas/cas-server/src/test/resources/client.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICvDCCAiUCAQEwDQYJKoZIhvcNAQELBQAwXjELMAkGA1UEBhMCRlIxEzARBgNV +BAgMClNvbWUtU3RhdGUxDjAMBgNVBAoMBVZpdGFtMQ4wDAYDVQQDDAVhZG1pbjEa +MBgGCSqGSIb3DQEJARYLYWRtaW5Adml0YW0wHhcNMjEwMzAxMTQxNTAzWhcNMzEw +MjI3MTQxNTAzWjBrMQswCQYDVQQGEwJGUjETMBEGA1UECAwKU29tZS1TdGF0ZTES +MBAGA1UECgwJTXlDb21wYW55MQswCQYDVQQLDAJJVDEMMAoGA1UEAwwDYm9iMRgw +FgYJKoZIhvcNAQkBFglib2JAZW1haWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDs1MhEdAxoupbY6lc2awKf+bKFVY2D94gNz41evmFGsvXGHJj9uvKL +76wJOMsSPhbm5cC1wJvvIsgo73bgWG6qfkxmIJiRgc2KOu8j1Et4+BFlUFmmtMhg +TQio5TsA6AiXgfPOTlLGeZbxv0RdUkv/fi35W6kfLqNByv5amZ8iG+ISAI10t0YY +Pj2Zl5VQdX3mUAzF4QETruStxIqVXySiOn7Sia2bUTgAI/DVXgrbPmXWxnk+lG7S +4qvpLjvcNnzsU/G1TKOvArJqjytg/Ze9vswkBYtRSTxbXWwjFBVqX2nFIav8JTnv +Lkitkl5qGUdS9uMHvrWEcVQ14nM7BQYTAgMBAAEwDQYJKoZIhvcNAQELBQADgYEA +RiQshNhSUi1vUBk5veFctdQRxXTcqmhK0bVc1kreZhula5iC+gHGfZsIJZ1/5ksr +d2rFv/ps4n7GZtGE8t5WTC0EcjbBy8L9VbpzFcAJUqD9wDC6ph8xA8paUB4d9F1Q +qfCIpvHbACc2Vtv3zEF4nE2BebWVJgbo92dpNDH2rs4= +-----END CERTIFICATE----- diff --git a/deployment/roles/vitamui/templates/cas-server/application.yml.j2 b/deployment/roles/vitamui/templates/cas-server/application.yml.j2 index fe21e8873..6bbe7978b 100644 --- a/deployment/roles/vitamui/templates/cas-server/application.yml.j2 +++ b/deployment/roles/vitamui/templates/cas-server/application.yml.j2 @@ -59,7 +59,7 @@ iam-client: cas.authn.accept.users: -cas.messageBundle.baseNames: classpath:overriden_messages,classpath:messages +cas.message-bundle.base-names: classpath:overriden_messages,classpath:messages {% if vitamui.cas_server.base_url is undefined %} @@ -76,7 +76,7 @@ cas.authn.pm.reset.crypto.enabled: true # # 4 (hours) * 60 (minutes) * 60 (seconds) #server.servlet.session.timeout: PT14400S -#cas.ticket.tgt.hardTimeout.timeToKillInSeconds: 14400 +#cas.ticket.tgt.hard-timeout.time-to-kill-in-seconds: 14400 {% if vitamui.cas_server.base_url is defined %} @@ -86,35 +86,36 @@ cas.server.prefix: {{ url_prefix }}/cas {% endif %} login.url: ${cas.server.prefix}/login -cas.serviceRegistry.mongo.clientUri: "mongodb://{{ mongodb.cas.user }}:{{ mongodb.cas.password }}@{{ mongodb.host }}:{{ mongodb.mongod_port | default(27017) }}/{{ mongodb.cas.db }}?replicaSet={{ mongod_replicaset_name }}&connectTimeoutMS={{ mongod_client_connect_timeout_ms }}" -cas.serviceRegistry.mongo.collection: services -cas.serviceRegistry.mongo.userId: {{ mongodb.cas.user }} -cas.serviceRegistry.mongo.password: {{ mongodb.cas.password }} +cas.service-registry.mongo.client-uri: "mongodb://{{ mongodb.cas.user }}:{{ mongodb.cas.password }}@{{ mongodb.host }}:{{ mongodb.mongod_port | default(27017) }}/{{ mongodb.cas.db }}?replicaSet={{ mongod_replicaset_name }}&connectTimeoutMS={{ mongod_client_connect_timeout_ms }}" +cas.service-registry.mongo.collection: services +cas.service-registry.mongo.user-id: {{ mongodb.cas.user }} +cas.service-registry.mongo.password: {{ mongodb.cas.password }} cas.authn.surrogate.separator: "," -cas.authn.surrogate.sms.attributeName: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS +cas.authn.surrogate.sms.attribute-name: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS # 24 hours cache for login delegation -cas.ticket.tst.timeToKillInSeconds: 86400 +# Must be at least 24 hours as this cache is also used for password management and its 24-hour-creation email +cas.ticket.tst.time-to-kill-in-seconds: 86400 -cas.authn.pm.enabled: true +cas.authn.pm.core.enabled: true {% if vitamui_password_configurations.anssiPolicyPattern is defined and vitamui_password_configurations.password.profile == "anssi" %} -cas.authn.pm.policyPattern: '{{ vitamui_password_configurations.anssiPolicyPattern | replace("'","''") }}' +cas.authn.pm.core.policy-pattern: '{{ vitamui_password_configurations.anssiPolicyPattern | replace("'","''") }}' {% else %} -cas.authn.pm.policyPattern: '{{ vitamui_password_configurations.customPolicyPattern | replace("'","''") }}' +cas.authn.pm.core.policy-pattern: '{{ vitamui_password_configurations.customPolicyPattern | replace("'","''") }}' {% endif %} cas.authn.pm.reset.mail.subject: Requete de reinitialisation de mot de passe cas.authn.pm.reset.mail.text: "Changez de mot de passe via le lien: %s" cas.authn.pm.reset.mail.from: {{ smtp.cas.sender }} -cas.authn.pm.reset.expirationMinutes: {{ smtp.cas.expiration }} -cas.authn.pm.reset.mail.attributeName: email -cas.authn.pm.reset.securityQuestionsEnabled: false -cas.authn.pm.reset.includeServerIpAddress: false -cas.authn.pm.reset.includeClientIpAddress: false -cas.authn.pm.autoLogin: true +cas.authn.pm.reset.expiration-minutes: {{ smtp.cas.expiration }} +cas.authn.pm.reset.mail.attribute-name: email +cas.authn.pm.reset.security-questions-enabled: false +cas.authn.pm.reset.include-server-ip-address: false +cas.authn.pm.reset.include-client-ip-address: false +cas.authn.pm.core.auto-login: true # Used to sign/encrypt the password-reset link # cas.authn.pm.reset.crypto.encryption.key: @@ -140,13 +141,13 @@ spring.mail.properties.mail.smtps.ssl.protocols: {{ smtp.smtps.ssl.protocols}} cas.authn.throttle.failure.threshold: 2 -cas.authn.throttle.failure.rangeSeconds: 3 +cas.authn.throttle.failure.range-seconds: 3 cas: logout: - followServiceRedirects: true - redirectParameter: next + follow-service-redirects: true + redirect-parameter: next management.endpoints.enabled-by-default: true @@ -157,8 +158,8 @@ management.metrics.export.prometheus.enabled: true {% if sms.enabled|lower == "true" %} # for SMS: -cas.smsProvider.twilio.accountId: {{ sms.account }} -cas.smsProvider.twilio.token: {{ sms.token }} +cas.sms-provider.twilio.account-id: {{ sms.account }} +cas.sms-provider.twilio.token: {{ sms.token }} mfa.sms.sender: "{{ sms.sender }}" {% endif %} @@ -173,7 +174,8 @@ ip.header: X-Real-IP # 8 hours in seconds -api.token.ttl: 28800 +# the old api.token.ttl property +cas.authn.oauth.access-token.max-time-to-live-in-seconds: 28800 server-identity: @@ -221,11 +223,11 @@ logging: {% if vitamui.cas_server.cors.enabled|lower == "true" %} # Cas CORS (necessary for mobile app) -cas.httpWebRequest.cors.enabled: true -cas.httpWebRequest.cors.allowCredentials: false -cas.httpWebRequest.cors.allowOrigins: ['*'] -cas.httpWebRequest.cors.allowMethods: ['*'] -cas.httpWebRequest.cors.allowHeaders: ['*'] +cas.http-web-request.cors.enabled: true +cas.http-web-request.cors.allow-credentials: false +cas.http-web-request.cors.allow-origins: ['*'] +cas.http-web-request.cors.allow-methods: ['*'] +cas.http-web-request.cors.allow-headers: ['*'] {% endif %} # Password configuration diff --git a/docs/fr/exploitation/sections/procedure.md b/docs/fr/exploitation/sections/procedure.md index 5d15991ca..b122fc912 100644 --- a/docs/fr/exploitation/sections/procedure.md +++ b/docs/fr/exploitation/sections/procedure.md @@ -180,8 +180,8 @@ L'exploitant peut changer quelques contraintes de la complexité du mot de passe #### Exemple de changement de la taille du mot de passe dans la section `password:`, l'attribut `length` est par défaut à 12, le changement de la taille du mot de passe revient à changer cet attribut `length`, cette valeur sera automatiquement portée par l'attribut -`cas.authn.pm.policyPattern:` qui contient l'expression `${password.length}$`. -l'attribut `cas.authn.pm.policyPattern:` contient l'expression régulière de validation du mot de passe et qui se trouve dans le meme fichier de configuration. +`cas.authn.pm.core.policy-pattern:` qui contient l'expression `${password.length}$`. +l'attribut `cas.authn.pm.core.policy-pattern:` contient l'expression régulière de validation du mot de passe et qui se trouve dans le meme fichier de configuration. > Cette valeur ne devra pas etre inférieur à la valeur par défaut, avec le profil anssi. @@ -261,8 +261,8 @@ constraints: #### Exemple de changement de la taille du mot de passe pour le profil custom -dans la section `password:`, l'attribut `length` est par défaut à 12, le changement de la taille du mot de passe revient à changer cet attribut `length`, cette valeur sera automatiquement portée par l'attribut `cas.authn.pm.policyPattern:` qui contient l'expression `${password.length}$` , à la fin de l'expression régulière. -L'attribut `cas.authn.pm.policyPattern:` contient l'expression régulière globale de validation du mot de passe et qui se trouve dans le meme fichier de configuration. +dans la section `password:`, l'attribut `length` est par défaut à 12, le changement de la taille du mot de passe revient à changer cet attribut `length`, cette valeur sera automatiquement portée par l'attribut `cas.authn.pm.core.policy-pattern:` qui contient l'expression `${password.length}$` , à la fin de l'expression régulière. +L'attribut `cas.authn.pm.core.policy-pattern:` contient l'expression régulière globale de validation du mot de passe et qui se trouve dans le meme fichier de configuration. > Cette valeur est par défaut égal à 8 pour le profil custom. diff --git a/pom.xml b/pom.xml index 3463ec0c4..59b5d97c9 100644 --- a/pom.xml +++ b/pom.xml @@ -67,13 +67,13 @@ <maven.compiler.target>11</maven.compiler.target> <!-- Dependencies version --> - <apache.commons.codec.version>1.14</apache.commons.codec.version> + <apache.commons.codec.version>1.15</apache.commons.codec.version> <apache.pdfbox.version>2.0.24</apache.pdfbox.version> <apache.pdfbox.xmpbox.version>2.0.16</apache.pdfbox.xmpbox.version> <asciidoctorj.pdf.version>1.5.3</asciidoctorj.pdf.version> <assertj.version>3.15.0</assertj.version> <bouncycastle.version>1.68</bouncycastle.version> - <cas.version>6.1.7.2</cas.version> + <cas.version>6.4.4.2</cas.version> <commons.beanutils.version>1.9.4</commons.beanutils.version> <commons.collections.version>3.2.2</commons.collections.version> <commons.collections4.version>4.4</commons.collections4.version> @@ -111,13 +111,13 @@ <junit.vintage.engine.version>5.7.0</junit.vintage.engine.version> <jruby.complete.version>9.2.13.0</jruby.complete.version> <jsonassert.version>1.5.0</jsonassert.version> - <logback.version>1.2.3</logback.version> + <logback.version>1.2.10</logback.version> <lombok.version>1.18.12</lombok.version> <micrometer.version>1.6.5</micrometer.version> <mapstruct.version>1.3.0.Final</mapstruct.version> <mockito.version>3.11.2</mockito.version> <nio.multipart.parser.version>1.1.0</nio.multipart.parser.version> - <pac4j.version>4.0.0</pac4j.version> + <pac4j.version>5.1.4</pac4j.version> <poi.version>4.1.2</poi.version> <org.odftoolkit.version>0.8.7</org.odftoolkit.version> <org.apache.odftoolkit.version>0.8.2-incubating</org.apache.odftoolkit.version> @@ -548,7 +548,12 @@ <!-- Security --> <dependency> <groupId>org.pac4j</groupId> - <artifactId>pac4j-saml-opensamlv3</artifactId> + <artifactId>pac4j-saml</artifactId> + <version>${pac4j.version}</version> + </dependency> + <dependency> + <groupId>org.pac4j</groupId> + <artifactId>pac4j-oidc</artifactId> <version>${pac4j.version}</version> </dependency> <dependency> diff --git a/ui/ui-frontend/package.json b/ui/ui-frontend/package.json index a70701219..016fa71a3 100644 --- a/ui/ui-frontend/package.json +++ b/ui/ui-frontend/package.json @@ -8,7 +8,7 @@ }, "scripts": { "ng": "ng", - "ng-high-memory": "node --max_old_space_size=4000 ./node_modules/@angular/cli/bin/ng", + "ng-high-memory": "node --max_old_space_size=8192 ./node_modules/@angular/cli/bin/ng", "start": "ng serve --proxy-config proxy.conf.json --disable-host-check --ssl --ssl-key $npm_package_pki_path/$npm_package_pki_asset.key --ssl-cert $npm_package_pki_path/$npm_package_pki_asset.crt", "start:en": "ng serve --proxy-config proxy.conf.json --configuration=en --disable-host-check --ssl --ssl-key $npm_package_pki_path/$npm_package_pki_asset.key --ssl-cert $npm_package_pki_path/$npm_package_pki_asset.crt", "start:demo": "ng serve demo --proxy-config proxy.conf.json --disable-host-check --ssl --ssl-key $npm_package_pki_path/$npm_package_pki_asset.key --ssl-cert $npm_package_pki_path/$npm_package_pki_asset.crt", @@ -23,17 +23,17 @@ "start:archive-search": "ng serve archive-search --proxy-config proxy.conf.json --disable-host-check --ssl --ssl-key $npm_package_pki_path/$npm_package_pki_asset.key --ssl-cert $npm_package_pki_path/$npm_package_pki_asset.crt", "build": "ng build --prod --i18n-locale fr --build-optimizer=false --optimization=false", "postinstall": "ngcc", - "build:prod": "export NODE_OPTIONS=--max_old_space_size=4096; ng build --prod --output-path ../../../target/www", + "build:prod": "export NODE_OPTIONS=--max_old_space_size=8192; ng build --prod --output-path ../../../target/www", "build:dev": "ng build --prod --i18n-locale fr --build-optimizer=false --optimization=false", "build:fr": "ng build --prod --i18n-locale fr --output-path ../../../target/www/fr", "build:en": "ng build --prod --i18n-file src/locale/messages.en.xlf --i18n-format xlf --i18n-locale en --output-path ../../../target/www/en", "build:all": "npm run build:fr && npm run build:en", "build:demo": "ng build demo --prod --i18n-locale fr", - "build:portal": "export NODE_OPTIONS=--max_old_space_size=4096; ng build portal --prod", - "build:identity": "export NODE_OPTIONS=--max_old_space_size=4096; ng build identity --prod --output-path ../../../target/www/fr", - "build:ingest": "export NODE_OPTIONS=--max_old_space_size=4096; ng build ingest --prod --i18n-locale fr", - "build:archive-search": "export NODE_OPTIONS=--max_old_space_size=4096; ng build archive-search --prod --i18n-locale fr", - "build:referential": "export NODE_OPTIONS=--max_old_space_size=4096; ng build referential --prod --i18n-locale fr", + "build:portal": "export NODE_OPTIONS=--max_old_space_size=8192; ng build portal --prod", + "build:identity": "export NODE_OPTIONS=--max_old_space_size=8192; ng build identity --prod --output-path ../../../target/www/fr", + "build:ingest": "export NODE_OPTIONS=--max_old_space_size=8192; ng build ingest --prod --i18n-locale fr", + "build:archive-search": "export NODE_OPTIONS=--max_old_space_size=8192; ng build archive-search --prod --i18n-locale fr", + "build:referential": "export NODE_OPTIONS=--max_old_space_size=8192; ng build referential --prod --i18n-locale fr", "analyze-portal": "ng build portal --stats-json --prod ; webpack-bundle-analyzer dist/portal/stats-es2015.json", "analyze-identity": "ng build identity --stats-json --prod --i18n-locale fr ; webpack-bundle-analyzer dist/identity/stats-es2015.json", "analyze-referential": "ng build referential --stats-json --prod --i18n-locale fr ; webpack-bundle-analyzer dist/referential/stats-es2015.json", -- GitLab