Major Enhancements to SAML2 and OAuth2 Integration with Simplified Security Configurations (#2040)

* implement Saml2 login/logout

* changed: deprecation code

* relyingPartyRegistrations only enabled samle
This commit is contained in:
Ludy
2024-10-20 13:30:58 +02:00
committed by GitHub
parent 227d18a469
commit eff1843061
32 changed files with 1080 additions and 839 deletions

View File

@@ -0,0 +1,48 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.ByteArrayInputStream;
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.Base64;
import org.springframework.core.io.Resource;
import org.springframework.util.FileCopyUtils;
public class CertificateUtils {
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
String certificateString =
new String(
FileCopyUtils.copyToByteArray(certificateResource.getInputStream()),
StandardCharsets.UTF_8);
String certContent =
certificateString
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replaceAll("\\R", "")
.replaceAll("\\s+", "");
CertificateFactory cf = CertificateFactory.getInstance("X.509");
byte[] decodedCert = Base64.getDecoder().decode(certContent);
return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(decodedCert));
}
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
String privateKeyString =
new String(
FileCopyUtils.copyToByteArray(privateKeyResource.getInputStream()),
StandardCharsets.UTF_8);
String privateKeyContent =
privateKeyString
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\R", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
byte[] decodedKey = Base64.getDecoder().decode(privateKeyContent);
return (RSAPrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
}
}

View File

@@ -0,0 +1,45 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
public class CustomSaml2AuthenticatedPrincipal
implements Saml2AuthenticatedPrincipal, Serializable {
private final String name;
private final Map<String, List<Object>> attributes;
private final String nameId;
private final List<String> sessionIndexes;
public CustomSaml2AuthenticatedPrincipal(
String name,
Map<String, List<Object>> attributes,
String nameId,
List<String> sessionIndexes) {
this.name = name;
this.attributes = attributes;
this.nameId = nameId;
this.sessionIndexes = sessionIndexes;
}
@Override
public String getName() {
return this.name;
}
@Override
public Map<String, List<Object>> getAttributes() {
return this.attributes;
}
public String getNameId() {
return this.nameId;
}
public List<String> getSessionIndexes() {
return this.sessionIndexes;
}
}

View File

@@ -0,0 +1,38 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.IOException;
import org.springframework.security.authentication.ProviderNotFoundException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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);
}
}

View File

@@ -0,0 +1,91 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.IOException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.AllArgsConstructor;
import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor
public class CustomSaml2AuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService;
private ApplicationProperties applicationProperties;
private UserService userService;
@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
Object principal = authentication.getPrincipal();
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
// Get the saved request
HttpSession session = request.getSession(false);
String contextPath = request.getContextPath();
SavedRequest savedRequest =
(session != null)
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(
contextPath, savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
if (loginAttemptService.isBlocked(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()) {
response.sendRedirect(
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return;
}
try {
if (saml2.getBlockRegistration()
&& !userService.usernameExistsIgnoreCase(username)) {
response.sendRedirect(
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
return;
}
userService.processOAuth2PostLogin(username, saml2.getAutoCreateUser());
response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
}
}
} else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}

View File

@@ -0,0 +1,86 @@
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;
import org.opensaml.saml.saml2.core.AuthnStatement;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.User;
@Component
@Slf4j
public class CustomSaml2ResponseAuthenticationConverter
implements Converter<ResponseToken, Saml2Authentication> {
private UserService userService;
public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
this.userService = userService;
}
@Override
public Saml2Authentication convert(ResponseToken responseToken) {
// Extract the assertion from the response
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
// Extract the NameID
String nameId = assertion.getSubject().getNameID().getValue();
Optional<User> userOpt = userService.findByUsernameIgnoreCase(nameId);
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
simpleGrantedAuthority =
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
}
}
// 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);
// 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;
}
}