fixes and other changes and debug of WIP SAML (#2360)
* backup * remove debugs * oauth to saml and compare fixes etc * ee flag for saml * more fixes * info to debug * remove unused repo * spring dev fix for saml * debugs * saml stuff * debugs * fix
This commit is contained in:
@@ -3,12 +3,14 @@ package stirling.software.SPDF.config.security.saml2;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
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 org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.bouncycastle.util.io.pem.PemObject;
|
||||
import org.bouncycastle.util.io.pem.PemReader;
|
||||
import org.springframework.core.io.Resource;
|
||||
@@ -28,15 +30,26 @@ public class CertificateUtils {
|
||||
}
|
||||
|
||||
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
|
||||
try (PemReader pemReader =
|
||||
new PemReader(
|
||||
try (PEMParser pemParser =
|
||||
new PEMParser(
|
||||
new InputStreamReader(
|
||||
privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
PemObject pemObject = pemReader.readPemObject();
|
||||
byte[] decodedKey = pemObject.getContent();
|
||||
return (RSAPrivateKey)
|
||||
KeyFactory.getInstance("RSA")
|
||||
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
|
||||
|
||||
Object object = pemParser.readObject();
|
||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
|
||||
|
||||
if (object instanceof PEMKeyPair) {
|
||||
// Handle traditional RSA private key format
|
||||
PEMKeyPair keypair = (PEMKeyPair) object;
|
||||
return (RSAPrivateKey) converter.getPrivateKey(keypair.getPrivateKeyInfo());
|
||||
} else if (object instanceof PrivateKeyInfo) {
|
||||
// Handle PKCS#8 format
|
||||
return (RSAPrivateKey) converter.getPrivateKey((PrivateKeyInfo) object);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported key format: "
|
||||
+ (object != null ? object.getClass().getName() : "null"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
@@ -20,11 +21,11 @@ import stirling.software.SPDF.model.AuthenticationType;
|
||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class CustomSaml2AuthenticationSuccessHandler
|
||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||
|
||||
private LoginAttemptService loginAttemptService;
|
||||
|
||||
private ApplicationProperties applicationProperties;
|
||||
private UserService userService;
|
||||
|
||||
@@ -34,10 +35,12 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
throws ServletException, IOException {
|
||||
|
||||
Object principal = authentication.getPrincipal();
|
||||
log.debug("Starting SAML2 authentication success handling");
|
||||
|
||||
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||
// Get the saved request
|
||||
log.debug("Authenticated principal found for user: {}", username);
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
String contextPath = request.getContextPath();
|
||||
SavedRequest savedRequest =
|
||||
@@ -45,46 +48,77 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||
: null;
|
||||
|
||||
log.debug(
|
||||
"Session exists: {}, Saved request exists: {}",
|
||||
session != null,
|
||||
savedRequest != null);
|
||||
|
||||
if (savedRequest != null
|
||||
&& !RequestUriUtils.isStaticResource(
|
||||
contextPath, savedRequest.getRedirectUrl())) {
|
||||
// Redirect to the original destination
|
||||
log.debug(
|
||||
"Valid saved request found, redirecting to original destination: {}",
|
||||
savedRequest.getRedirectUrl());
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
} else {
|
||||
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
|
||||
log.debug(
|
||||
"Processing SAML2 authentication with autoCreateUser: {}",
|
||||
saml2.getAutoCreateUser());
|
||||
|
||||
if (loginAttemptService.isBlocked(username)) {
|
||||
log.debug("User {} is blocked due to too many login attempts", username);
|
||||
if (session != null) {
|
||||
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
||||
}
|
||||
throw new LockedException(
|
||||
"Your account has been locked due to too many failed login attempts.");
|
||||
}
|
||||
if (userService.usernameExistsIgnoreCase(username)
|
||||
&& userService.hasPassword(username)
|
||||
&& !userService.isAuthenticationTypeByUsername(
|
||||
username, AuthenticationType.OAUTH2)
|
||||
&& saml2.getAutoCreateUser()) {
|
||||
|
||||
boolean userExists = userService.usernameExistsIgnoreCase(username);
|
||||
boolean hasPassword = userExists && userService.hasPassword(username);
|
||||
boolean isSSOUser =
|
||||
userExists
|
||||
&& userService.isAuthenticationTypeByUsername(
|
||||
username, AuthenticationType.SSO);
|
||||
|
||||
log.debug(
|
||||
"User status - Exists: {}, Has password: {}, Is SSO user: {}",
|
||||
userExists,
|
||||
hasPassword,
|
||||
isSSOUser);
|
||||
|
||||
if (userExists && hasPassword && !isSSOUser && saml2.getAutoCreateUser()) {
|
||||
log.debug(
|
||||
"User {} exists with password but is not SSO user, redirecting to logout",
|
||||
username);
|
||||
response.sendRedirect(
|
||||
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (saml2.getBlockRegistration()
|
||||
&& !userService.usernameExistsIgnoreCase(username)) {
|
||||
if (saml2.getBlockRegistration() && !userExists) {
|
||||
log.debug("Registration blocked for new user: {}", username);
|
||||
response.sendRedirect(
|
||||
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
|
||||
return;
|
||||
}
|
||||
userService.processOAuth2PostLogin(username, saml2.getAutoCreateUser());
|
||||
log.debug("Processing SSO post-login for user: {}", username);
|
||||
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
|
||||
log.debug("Successfully processed authentication for user: {}", username);
|
||||
response.sendRedirect(contextPath + "/");
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug(
|
||||
"Invalid username detected for user: {}, redirecting to logout",
|
||||
username);
|
||||
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug("Non-SAML2 principal detected, delegating to parent handler");
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package stirling.software.SPDF.config.security.saml2;
|
||||
import java.util.*;
|
||||
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
import org.opensaml.core.xml.schema.XSBoolean;
|
||||
import org.opensaml.core.xml.schema.XSString;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.opensaml.saml.saml2.core.Attribute;
|
||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||
@@ -30,15 +28,60 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
|
||||
Map<String, List<Object>> attributes = new HashMap<>();
|
||||
|
||||
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
|
||||
for (Attribute attribute : attributeStatement.getAttributes()) {
|
||||
String attributeName = attribute.getName();
|
||||
List<Object> values = new ArrayList<>();
|
||||
|
||||
for (XMLObject xmlObject : attribute.getAttributeValues()) {
|
||||
// Get the text content directly
|
||||
String value = xmlObject.getDOM().getTextContent();
|
||||
if (value != null && !value.trim().isEmpty()) {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!values.isEmpty()) {
|
||||
// Store with both full URI and last part of the URI
|
||||
attributes.put(attributeName, values);
|
||||
String shortName = attributeName.substring(attributeName.lastIndexOf('/') + 1);
|
||||
attributes.put(shortName, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Saml2Authentication convert(ResponseToken responseToken) {
|
||||
// Extract the assertion from the response
|
||||
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
|
||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
||||
|
||||
// Extract the NameID
|
||||
String nameId = assertion.getSubject().getNameID().getValue();
|
||||
// Debug log with actual values
|
||||
log.debug("Extracted SAML Attributes: " + attributes);
|
||||
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(nameId);
|
||||
// Try to get username/identifier in order of preference
|
||||
String userIdentifier = null;
|
||||
if (hasAttribute(attributes, "username")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "username");
|
||||
} else if (hasAttribute(attributes, "emailaddress")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "emailaddress");
|
||||
} else if (hasAttribute(attributes, "name")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "name");
|
||||
} else if (hasAttribute(attributes, "upn")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "upn");
|
||||
} else if (hasAttribute(attributes, "uid")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "uid");
|
||||
} else {
|
||||
userIdentifier = assertion.getSubject().getNameID().getValue();
|
||||
}
|
||||
|
||||
// Rest of your existing code...
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(userIdentifier);
|
||||
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
@@ -48,39 +91,27 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the SessionIndexes
|
||||
List<String> sessionIndexes = new ArrayList<>();
|
||||
for (AuthnStatement authnStatement : assertion.getAuthnStatements()) {
|
||||
sessionIndexes.add(authnStatement.getSessionIndex());
|
||||
}
|
||||
|
||||
// Extract the Attributes
|
||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
||||
|
||||
// Create the custom principal
|
||||
CustomSaml2AuthenticatedPrincipal principal =
|
||||
new CustomSaml2AuthenticatedPrincipal(nameId, attributes, nameId, sessionIndexes);
|
||||
new CustomSaml2AuthenticatedPrincipal(
|
||||
userIdentifier, attributes, userIdentifier, sessionIndexes);
|
||||
|
||||
// Create the Saml2Authentication
|
||||
return new Saml2Authentication(
|
||||
principal,
|
||||
responseToken.getToken().getSaml2Response(),
|
||||
Collections.singletonList(simpleGrantedAuthority));
|
||||
}
|
||||
|
||||
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
|
||||
Map<String, List<Object>> attributes = new HashMap<>();
|
||||
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
|
||||
for (Attribute attribute : attributeStatement.getAttributes()) {
|
||||
String attributeName = attribute.getName();
|
||||
List<Object> values = new ArrayList<>();
|
||||
for (XMLObject xmlObject : attribute.getAttributeValues()) {
|
||||
log.info("BOOL: " + ((XSBoolean) xmlObject).getValue());
|
||||
values.add(((XSString) xmlObject).getValue());
|
||||
}
|
||||
attributes.put(attributeName, values);
|
||||
}
|
||||
}
|
||||
return attributes;
|
||||
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {
|
||||
return attributes.containsKey(name) && !attributes.get(name).isEmpty();
|
||||
}
|
||||
|
||||
private String getFirstAttributeValue(Map<String, List<Object>> attributes, String name) {
|
||||
List<Object> values = attributes.get(name);
|
||||
return values != null && !values.isEmpty() ? values.get(0).toString() : null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user