diff --git a/pom.xml b/pom.xml
index 55765564..e8fb4bca 100644
--- a/pom.xml
+++ b/pom.xml
@@ -116,10 +116,6 @@
jjwt-jackson
0.11.5
-
- org.springframework.boot
- spring-boot-starter-web
-
jakarta.validation
jakarta.validation-api
@@ -139,6 +135,37 @@
problem-spring-web
0.23.0
+
+
+
+ org.springframework.security
+ spring-security-saml2-service-provider
+
+
+
+
+ org.opensaml
+ opensaml-core
+ 4.0.1
+
+
+
+
+
+ org.opensaml
+ opensaml-saml-api
+ 4.0.1
+
+
+
+
+
+ org.opensaml
+ opensaml-saml-impl
+ 4.0.1
+
+
+
org.springframework.cloud
@@ -158,7 +185,6 @@
org.liquibase
liquibase-maven-plugin
- 4.20.0
src/main/resources/application.properties
diff --git a/src/main/java/net/gepafin/tendermanagement/config/SecurityConfig.java b/src/main/java/net/gepafin/tendermanagement/config/SecurityConfig.java
index e06c3e4b..22ac7dff 100644
--- a/src/main/java/net/gepafin/tendermanagement/config/SecurityConfig.java
+++ b/src/main/java/net/gepafin/tendermanagement/config/SecurityConfig.java
@@ -1,6 +1,18 @@
package net.gepafin.tendermanagement.config;
-
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileReader;
+import java.io.InputStream;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.PKCS8EncodedKeySpec;
+import org.bouncycastle.util.io.pem.PemReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
@@ -14,6 +26,11 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.saml2.core.Saml2X509Credential;
+import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
@@ -23,6 +40,8 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityRequirement;
@@ -30,24 +49,29 @@ import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import net.gepafin.tendermanagement.config.jwt.JWTFilter;
import net.gepafin.tendermanagement.config.jwt.TokenProvider;
-
+import net.gepafin.tendermanagement.entities.SamlResponseLogEntity;
+import net.gepafin.tendermanagement.repositories.SamlResponseLogRepository;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
-
+ private final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
private final TokenProvider tokenProvider;
-
+
+ @Value("${base-url}")
+ String baseUrl;
+
+ @Autowired
+ private SamlResponseLogRepository samlResponseLogRepository;
+
@Autowired
public SecurityConfig(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
-
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
-
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@@ -75,8 +99,8 @@ public class SecurityConfig {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
- config.addAllowedMethod("*");
- config.addAllowedHeader("*");
+ config.addAllowedMethod("*");
+ config.addAllowedHeader("*");
config.setMaxAge(3600l);
if (config.getAllowedOrigins() != null && !config.getAllowedOrigins().isEmpty()) {
@@ -86,24 +110,91 @@ public class SecurityConfig {
}
return new CorsFilter(source);
}
-
-
@Bean
- public SecurityFilterChain securityFilterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {
- http
- .csrf(AbstractHttpConfigurer::disable)
- .authorizeHttpRequests(auth -> auth
- .requestMatchers(mvc.pattern(HttpMethod.POST, "/v1/user/login")).permitAll()
- .requestMatchers("/swagger-ui/**").permitAll()
- .requestMatchers("/v1/api-docs/**").permitAll()
- .anyRequest().authenticated()
- )
- .sessionManagement(session -> session
- .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- )
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(auth -> auth
+ // Allow public access to the login endpoints
+ .requestMatchers("/v1/user/login").permitAll() // JWT-based login
+ .requestMatchers("/v1/saml/**").permitAll() // JWT-based login
+ .requestMatchers("/saml2/**").permitAll() // SAML login initiation
+ .requestMatchers("/swagger-ui/**").permitAll() // Swagger docs
+ .requestMatchers("/v1/api-docs/**").permitAll() // API docs
+ .anyRequest().authenticated())
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(corsFilter(), UsernamePasswordAuthenticationFilter.class)
- .addFilterBefore(new JWTFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);
+ .addFilterBefore(new JWTFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class)
+ // Add SAML2 login configuration (for BENEFICIARI)
+ /*
+ * .saml2Login(saml -> saml.loginPage("/saml/login") // Entry point for SAML
+ * login .defaultSuccessUrl("/") // Redirect after successful SAML login );
+ */
+ .saml2Login(saml ->
+ saml.defaultSuccessUrl("/")
+ .successHandler((request, response, authentication) -> {
+ logger.error("SAML success login");
+ SamlResponseLogEntity samlResponseLogEntity = new SamlResponseLogEntity();
+ samlResponseLogEntity.setRequest(request.toString());
+ samlResponseLogEntity.setResponse(response.toString());
+ samlResponseLogEntity.setAuthenticationObject(authentication.toString());
+ samlResponseLogRepository.save(samlResponseLogEntity);
+ try {
+ ObjectMapper objectMapper = new ObjectMapper();
+ // Create a new SAML log entity
+ SamlResponseLogEntity samlResponseLogEntity1 = new SamlResponseLogEntity();
+ // Convert request, response, and authentication to JSON format
+ String requestJson = objectMapper.writeValueAsString(request.getParameterMap()); // Assuming request params to JSON
+ String responseJson = objectMapper.writeValueAsString(response); // This may need to be adapted based on your response object
+ String authenticationJson = objectMapper.writeValueAsString(authentication); // Authentication object to JSON
+
+ // Set the JSON strings in the entity
+ samlResponseLogEntity1.setRequest(requestJson);
+ samlResponseLogEntity1.setResponse(responseJson);
+ samlResponseLogEntity1.setAuthenticationObject(authenticationJson);
+ samlResponseLogRepository.save(samlResponseLogEntity1);
+
+ logger.info("SAML Request: " + requestJson);
+ logger.info("SAML Response: " + responseJson);
+ logger.info("Authentication Details: " + authenticationJson);
+ }catch(Exception e) {
+ logger.info("Exception object" + e);
+ }
+// samlResponseLogRepository
+ logger.info("SAML login successful for user: " + authentication.getName());
+ response.sendRedirect("http://gepafin-staging-fe.s3-website.eu-central-1.amazonaws.com/");
+ }).failureHandler((request, response, exception) -> {
+ logger.error("SAML login failed: " + exception.getMessage());
+
+ SamlResponseLogEntity samlResponseLogEntity = new SamlResponseLogEntity();
+ samlResponseLogEntity.setRequest(request.toString());
+ samlResponseLogEntity.setResponse(response.toString());
+ samlResponseLogEntity.setExceptionObject(exception.toString());
+ samlResponseLogRepository.save(samlResponseLogEntity);
+ try {
+ ObjectMapper objectMapper = new ObjectMapper();
+
+ // Create a new SAML log entity
+ SamlResponseLogEntity samlResponseLogEntity1 = new SamlResponseLogEntity();
+
+ // Convert request, response, and authentication to JSON format
+ String requestJson = objectMapper.writeValueAsString(request.getParameterMap()); // Assuming request params to JSON
+ String responseJson = objectMapper.writeValueAsString(response); // This may need to be adapted based on your response object
+ String exceptionJson = objectMapper.writeValueAsString(exception); // Authentication object to JSON
+
+ // Set the JSON strings in the entity
+ samlResponseLogEntity1.setRequest(requestJson);
+ samlResponseLogEntity1.setResponse(responseJson);
+ samlResponseLogEntity1.setAuthenticationObject(exceptionJson);
+ samlResponseLogRepository.save(samlResponseLogEntity1);
+
+ logger.info("SAML Request: " + requestJson);
+ logger.info("SAML Response: " + responseJson);
+ logger.info("exception Details: " + exceptionJson);
+ }catch(Exception e) {
+ logger.info("Exception object" + e);
+ }
+ response.sendRedirect("http://gepafin-staging-fe.s3-website.eu-central-1.amazonaws.com/login");
+ }));
return http.build();
}
@@ -116,4 +207,51 @@ public class SecurityConfig {
new SecurityScheme().type(SecurityScheme.Type.HTTP)
.scheme("bearer").bearerFormat("JWT")));
}
-}
+
+ @Bean
+ public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
+
+ String entityId = baseUrl + "/v1/saml/gw/metadata";
+ String acsUrl = baseUrl + "/login/saml2/sso/loginumbria";
+ RelyingPartyRegistration registration = RelyingPartyRegistration.withRegistrationId("loginumbria")
+ .entityId(entityId)
+ .signingX509Credentials(credentials -> {
+ try {
+ credentials.add(Saml2X509Credential.signing(readPrivateKey(), readCertificate()));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ })
+ .assertionConsumerServiceLocation(acsUrl)
+ .assertingPartyDetails(details -> details.entityId("https://federatest.umbriadigitale.it/gw/metadata")
+ .singleSignOnServiceLocation("https://federatest.umbriadigitale.it/gw/SSOProxy/SAML2")
+ .singleSignOnServiceBinding(Saml2MessageBinding.POST).wantAuthnRequestsSigned(false)
+ )
+ .build();
+ return new InMemoryRelyingPartyRegistrationRepository(registration);
+ }
+
+
+
+ public PrivateKey readPrivateKey() throws Exception {
+ // Path to your private key PEM file
+ File privateKeyFile = new File("src/main/resources/dev/saml/private-key.pem");
+ try (PemReader pemReader = new PemReader(new FileReader(privateKeyFile))) {
+ // Read the PEM content
+ byte[] pemContent = pemReader.readPemObject().getContent();
+ // Decode the PEM content
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pemContent);
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA"); // Use RSA algorithm
+ // Generate and return the PrivateKey
+ return keyFactory.generatePrivate(keySpec);
+ }
+ }
+ public X509Certificate readCertificate() throws Exception {
+ // Path to your certificate PEM fileFile
+ File certFile = new File("src/main/resources/dev/saml/public-cert.pem");
+ try (InputStream inStream = new FileInputStream(certFile)) {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ return (X509Certificate) certFactory.generateCertificate(inStream);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/net/gepafin/tendermanagement/entities/SamlResponseLogEntity.java b/src/main/java/net/gepafin/tendermanagement/entities/SamlResponseLogEntity.java
new file mode 100644
index 00000000..53e262e9
--- /dev/null
+++ b/src/main/java/net/gepafin/tendermanagement/entities/SamlResponseLogEntity.java
@@ -0,0 +1,26 @@
+package net.gepafin.tendermanagement.entities;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.Data;
+
+@Entity
+@Table(name = "SAML_RESPONSE_LOG")
+@Data
+public class SamlResponseLogEntity extends BaseEntity{
+
+ @Column(name = "REQUEST")
+ private String request;
+
+ @Column(name = "RESPONSE")
+ private String response;
+
+ @Column(name = "AUTHENTICATION_OBJECT")
+ private String authenticationObject;
+
+ @Column(name = "EXCEPTION_OBJECT")
+ private String exceptionObject;
+
+
+}
diff --git a/src/main/java/net/gepafin/tendermanagement/repositories/SamlResponseLogRepository.java b/src/main/java/net/gepafin/tendermanagement/repositories/SamlResponseLogRepository.java
new file mode 100644
index 00000000..3f98e083
--- /dev/null
+++ b/src/main/java/net/gepafin/tendermanagement/repositories/SamlResponseLogRepository.java
@@ -0,0 +1,11 @@
+package net.gepafin.tendermanagement.repositories;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import net.gepafin.tendermanagement.entities.SamlResponseLogEntity;
+
+@Repository
+public interface SamlResponseLogRepository extends JpaRepository {
+
+}
diff --git a/src/main/java/net/gepafin/tendermanagement/web/rest/api/SamlApi.java b/src/main/java/net/gepafin/tendermanagement/web/rest/api/SamlApi.java
new file mode 100644
index 00000000..54ac52dd
--- /dev/null
+++ b/src/main/java/net/gepafin/tendermanagement/web/rest/api/SamlApi.java
@@ -0,0 +1,30 @@
+package net.gepafin.tendermanagement.web.rest.api;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import net.gepafin.tendermanagement.web.rest.api.errors.ErrorConstants;
+
+public interface SamlApi {
+
+
+ @Operation(summary = "Api to get SP metadata",
+ responses = {
+ @ApiResponse(responseCode = "200", description = "OK"),
+ @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = {
+ @ExampleObject(value = ErrorConstants.NOTFOUND_ERROR_EXAMPLE) })),
+ @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = {
+ @ExampleObject(value = ErrorConstants.UNAUTHORIZED_ERROR_EXAMPLE) })),
+ @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, examples = {
+ @ExampleObject(value = ErrorConstants.BADREQUEST_ERROR_EXAMPLE) })) })
+ @GetMapping(value = "/gw/metadata",
+ produces = { "application/json" })
+ ResponseEntity getMetadata(HttpServletRequest request);
+
+}
diff --git a/src/main/java/net/gepafin/tendermanagement/web/rest/api/impl/SamlApiController.java b/src/main/java/net/gepafin/tendermanagement/web/rest/api/impl/SamlApiController.java
new file mode 100644
index 00000000..f49b9fa5
--- /dev/null
+++ b/src/main/java/net/gepafin/tendermanagement/web/rest/api/impl/SamlApiController.java
@@ -0,0 +1,39 @@
+package net.gepafin.tendermanagement.web.rest.api.impl;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
+import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResolver;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.gepafin.tendermanagement.config.SecurityConfig;
+import net.gepafin.tendermanagement.web.rest.api.SamlApi;
+
+@RestController
+@RequestMapping("${openapi.gepafin.base-path:/v1/saml}")
+public class SamlApiController implements SamlApi{
+
+ private final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
+
+ @Autowired
+ private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
+
+ @Override
+ public ResponseEntity getMetadata(HttpServletRequest request) {
+ logger.info("get SP metadata");
+ Saml2MetadataResolver metadataResolver = new OpenSamlMetadataResolver();
+ RelyingPartyRegistration registration = relyingPartyRegistrationRepository.findByRegistrationId("loginumbria");
+ return ResponseEntity.status(HttpStatus.OK).header("Content-Type", MediaType.APPLICATION_XML_VALUE)
+ .body(metadataResolver.resolve(registration));
+
+ }
+
+}
diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties
index 19588213..930cbd9f 100644
--- a/src/main/resources/application-dev.properties
+++ b/src/main/resources/application-dev.properties
@@ -6,5 +6,6 @@ spring.datasource.driver-class-name=org.postgresql.Driver
# JPA Configuration
spring.h2.console.enabled=true
+base-url=https://api-dev-gepafin.memento.credit
isVatCheckGloballyDisabled = false
diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties
index 7ed5944b..6b925e03 100644
--- a/src/main/resources/application-local.properties
+++ b/src/main/resources/application-local.properties
@@ -5,4 +5,5 @@ spring.datasource.password=root
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA Configuration
-spring.jpa.show-sql=true
\ No newline at end of file
+spring.jpa.show-sql=true
+base-url=http://localhost:8080
\ No newline at end of file
diff --git a/src/main/resources/application-testing.properties b/src/main/resources/application-testing.properties
index ea3a5732..12b95acb 100644
--- a/src/main/resources/application-testing.properties
+++ b/src/main/resources/application-testing.properties
@@ -4,4 +4,5 @@ spring.datasource.username=sa
spring.datasource.password=sa
# JPA Configuration
-spring.h2.console.enabled=true
\ No newline at end of file
+spring.h2.console.enabled=true
+base-url=http://localhost:8080
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 8c3ecdf1..1e41a2e9 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -35,6 +35,7 @@ aws.s3.url.folder=gepafin
# Ensure these values match your expectations
security.authentication.jwt.secret=my-secret-token-to-change-in-prod-environment-your-super-secure-randomly-generated-key
security.authentication.jwt.token-validity-in-seconds=86400
+base-url=https://api-dev-gepafin.memento.credit
spring.main.allow-circular-references=true
diff --git a/src/main/resources/db/changelog/db.changelog-1.0.0.xml b/src/main/resources/db/changelog/db.changelog-1.0.0.xml
index f4a78969..d7377f5c 100644
--- a/src/main/resources/db/changelog/db.changelog-1.0.0.xml
+++ b/src/main/resources/db/changelog/db.changelog-1.0.0.xml
@@ -710,4 +710,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+