Merge branch 'bug/remember-me' of

git@github.com:Stirling-Tools/Stirling-PDF.git into bug/remember-me
This commit is contained in:
Anthony Stirling
2024-11-15 13:12:20 +00:00
parent 1405e4f5ee
commit 559581c59d
8 changed files with 278 additions and 58 deletions

View File

@@ -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;
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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, its 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);
}
}