Merge branch 'bug/remember-me' of
git@github.com:Stirling-Tools/Stirling-PDF.git into bug/remember-me
This commit is contained in:
@@ -1,18 +1,25 @@
|
||||
package stirling.software.SPDF.config.security.saml2;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Scanner;
|
||||
|
||||
import org.bouncycastle.util.io.pem.PemObject;
|
||||
import org.bouncycastle.util.io.pem.PemReader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class CertificateUtils {
|
||||
|
||||
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
|
||||
@@ -39,4 +46,84 @@ public class CertificateUtils {
|
||||
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static X509Certificate getIdPCertificate(Resource certificateResource) throws Exception {
|
||||
|
||||
if (certificateResource instanceof UrlResource) {
|
||||
return extractCertificateFromMetadata(certificateResource);
|
||||
} else {
|
||||
// Treat as file resource
|
||||
return readCertificate(certificateResource);
|
||||
}
|
||||
}
|
||||
|
||||
private static X509Certificate extractCertificateFromMetadata(Resource metadataResource) throws Exception {
|
||||
log.info("Attempting to extract certificate from metadata resource: {}", metadataResource.getDescription());
|
||||
|
||||
try (InputStream is = metadataResource.getInputStream()) {
|
||||
String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
log.info("Retrieved metadata content, length: {}", content.length());
|
||||
|
||||
// Find the certificate data
|
||||
int startIndex = content.indexOf("<ds:X509Certificate>");
|
||||
int endIndex = content.indexOf("</ds:X509Certificate>");
|
||||
|
||||
if (startIndex == -1 || endIndex == -1) {
|
||||
log.error("Certificate tags not found in metadata");
|
||||
throw new Exception("Certificate tags not found in metadata");
|
||||
}
|
||||
|
||||
// Extract certificate data
|
||||
String certData = content.substring(
|
||||
startIndex + "<ds:X509Certificate>".length(),
|
||||
endIndex
|
||||
).trim();
|
||||
|
||||
log.info("Found certificate data, length: {}", certData.length());
|
||||
|
||||
// Remove any whitespace and newlines from cert data
|
||||
certData = certData.replaceAll("\\s+", "");
|
||||
|
||||
// Reconstruct PEM format with proper line breaks
|
||||
StringBuilder pemBuilder = new StringBuilder();
|
||||
pemBuilder.append("-----BEGIN CERTIFICATE-----\n");
|
||||
|
||||
// Insert line breaks every 64 characters
|
||||
int lineLength = 64;
|
||||
for (int i = 0; i < certData.length(); i += lineLength) {
|
||||
int end = Math.min(i + lineLength, certData.length());
|
||||
pemBuilder.append(certData, i, end).append('\n');
|
||||
}
|
||||
|
||||
pemBuilder.append("-----END CERTIFICATE-----");
|
||||
String pemCert = pemBuilder.toString();
|
||||
|
||||
log.debug("Reconstructed PEM certificate:\n{}", pemCert);
|
||||
|
||||
try {
|
||||
ByteArrayInputStream pemStream = new ByteArrayInputStream(pemCert.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
X509Certificate cert = (X509Certificate) cf.generateCertificate(pemStream);
|
||||
|
||||
log.info("Successfully parsed certificate. Subject: {}", cert.getSubjectX500Principal());
|
||||
|
||||
// Optional: check validity dates
|
||||
cert.checkValidity(); // Throws CertificateExpiredException if expired
|
||||
log.info("Certificate is valid (not expired)");
|
||||
|
||||
return cert;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to parse certificate", e);
|
||||
throw new Exception("Failed to parse X509 certificate from metadata", e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing metadata resource", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -16,23 +16,35 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException exception)
|
||||
throws IOException, ServletException {
|
||||
if (exception instanceof Saml2AuthenticationException) {
|
||||
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
|
||||
} else if (exception instanceof ProviderNotFoundException) {
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(
|
||||
request,
|
||||
response,
|
||||
"/login?erroroauth=not_authentication_provider_found");
|
||||
}
|
||||
log.error("AuthenticationException: " + exception);
|
||||
}
|
||||
@Override
|
||||
public void onAuthenticationFailure(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException exception)
|
||||
throws IOException, ServletException {
|
||||
|
||||
if (exception instanceof Saml2AuthenticationException saml2Exception) {
|
||||
Saml2Error error = saml2Exception.getSaml2Error();
|
||||
|
||||
// Log detailed information about the SAML error
|
||||
log.error("SAML Authentication failed with error code: {}", error.getErrorCode());
|
||||
log.error("Error description: {}", error.getDescription());
|
||||
|
||||
// Redirect to login with specific error code
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
|
||||
} else if (exception instanceof ProviderNotFoundException) {
|
||||
log.error("Authentication failed: No authentication provider found");
|
||||
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(
|
||||
request,
|
||||
response,
|
||||
"/login?erroroauth=not_authentication_provider_found");
|
||||
} else {
|
||||
log.error("Unknown AuthenticationException: {}", exception.getMessage());
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=unknown_error");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package stirling.software.SPDF.config.security.saml2;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
|
||||
public class LoggingSamlAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LoggingSamlAuthenticationProvider.class);
|
||||
private final OpenSaml4AuthenticationProvider delegate;
|
||||
|
||||
public LoggingSamlAuthenticationProvider(OpenSaml4AuthenticationProvider delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
if (authentication instanceof Saml2AuthenticationToken token) {
|
||||
String samlResponse = token.getSaml2Response();
|
||||
|
||||
// Log the raw SAML response
|
||||
log.info("Raw SAML Response (Base64): {}", samlResponse);
|
||||
|
||||
// Decode and log the SAML response XML
|
||||
try {
|
||||
String decodedResponse = new String(Base64.getDecoder().decode(samlResponse), StandardCharsets.UTF_8);
|
||||
log.info("Decoded SAML Response XML:\n{}", decodedResponse);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// If decoding fails, it’s likely already plain XML
|
||||
log.warn("SAML Response appears to be different format, not Base64-encoded.");
|
||||
log.debug("SAML Response XML:\n{}", samlResponse);
|
||||
}
|
||||
// Delegate the actual authentication to the wrapped OpenSaml4AuthenticationProvider
|
||||
try {
|
||||
return delegate.authenticate(authentication);
|
||||
} catch (Saml2AuthenticationException e) {
|
||||
log.error("SAML authentication failed: {}");
|
||||
log.error("Detailed error message: {}", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
// Only support Saml2AuthenticationToken
|
||||
return Saml2AuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user