Working on SAML 2
This commit is contained in:
committed by
Dario Ghunney Ware
parent
704da399d4
commit
6478674d9b
@@ -6,6 +6,7 @@ import java.security.interfaces.RSAPrivateKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -14,6 +15,7 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A
|
||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||
|
||||
import com.coveo.saml.SamlClient;
|
||||
import com.coveo.saml.SamlException;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -49,9 +51,8 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
} else if (authentication instanceof OAuth2AuthenticationToken) {
|
||||
// Handle OAuth2 logout redirection
|
||||
getRedirect_oauth2(request, response, authentication);
|
||||
}
|
||||
// Handle Username/Password logout
|
||||
else if (authentication instanceof UsernamePasswordAuthenticationToken) {
|
||||
} else if (authentication instanceof UsernamePasswordAuthenticationToken) {
|
||||
// Handle Username/Password logout
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
} else {
|
||||
// Handle unknown authentication types
|
||||
@@ -90,27 +91,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
certificates.add(certificate);
|
||||
|
||||
// Construct URLs required for SAML configuration
|
||||
String serverUrl =
|
||||
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
|
||||
|
||||
String relyingPartyIdentifier =
|
||||
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
||||
|
||||
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
|
||||
|
||||
String idpUrl = samlConf.getIdpSingleLogoutUrl();
|
||||
|
||||
String idpIssuer = samlConf.getIdpIssuer();
|
||||
|
||||
// Create SamlClient instance for SAML logout
|
||||
SamlClient samlClient =
|
||||
new SamlClient(
|
||||
relyingPartyIdentifier,
|
||||
assertionConsumerServiceUrl,
|
||||
idpUrl,
|
||||
idpIssuer,
|
||||
certificates,
|
||||
SamlClient.SamlIdpBinding.POST);
|
||||
SamlClient samlClient = getSamlClient(registrationId, samlConf, certificates);
|
||||
|
||||
// Read private key for service provider
|
||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||
@@ -127,7 +108,6 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect for OAuth2 authentication logout
|
||||
private void getRedirect_oauth2(
|
||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
throws IOException {
|
||||
@@ -164,12 +144,45 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
response.sendRedirect(redirectUrl);
|
||||
}
|
||||
default -> {
|
||||
log.info("Redirecting to default logout URL: {}", redirectUrl);
|
||||
response.sendRedirect(redirectUrl);
|
||||
String logoutUrl = oauth.getLogoutUrl();
|
||||
|
||||
if (StringUtils.isNotBlank(logoutUrl)) {
|
||||
log.info("Redirecting to logout URL: {}", logoutUrl);
|
||||
response.sendRedirect(logoutUrl);
|
||||
} else {
|
||||
log.info("Redirecting to default logout URL: {}", redirectUrl);
|
||||
response.sendRedirect(redirectUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect for OAuth2 authentication logout
|
||||
private static SamlClient getSamlClient(
|
||||
String registrationId, SAML2 samlConf, List<X509Certificate> certificates)
|
||||
throws SamlException {
|
||||
String serverUrl =
|
||||
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
|
||||
|
||||
String relyingPartyIdentifier =
|
||||
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
||||
|
||||
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
|
||||
|
||||
String idpUrl = samlConf.getIdpSingleLogoutUrl();
|
||||
|
||||
String idpIssuer = samlConf.getIdpIssuer();
|
||||
|
||||
// Create SamlClient instance for SAML logout
|
||||
return new SamlClient(
|
||||
relyingPartyIdentifier,
|
||||
assertionConsumerServiceUrl,
|
||||
idpUrl,
|
||||
idpIssuer,
|
||||
certificates,
|
||||
SamlClient.SamlIdpBinding.POST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles different error scenarios during logout. Will return a <code>String</code> containing
|
||||
* the error request parameter.
|
||||
|
||||
@@ -23,6 +23,10 @@ import org.springframework.security.saml2.provider.service.web.authentication.Op
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
|
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
|
||||
import org.springframework.security.web.context.SecurityContextRepository;
|
||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
@@ -51,11 +55,7 @@ public class SecurityConfiguration {
|
||||
|
||||
private final CustomUserDetailsService userDetailsService;
|
||||
private final UserService userService;
|
||||
|
||||
@Qualifier("loginEnabled")
|
||||
private final boolean loginEnabledValue;
|
||||
|
||||
@Qualifier("runningEE")
|
||||
private final boolean runningEE;
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
@@ -105,10 +105,11 @@ public class SecurityConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
public SecurityFilterChain filterChain(HttpSecurity http, SecurityContextRepository securityContextRepository) throws Exception {
|
||||
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
|
||||
http.csrf(csrf -> csrf.disable());
|
||||
}
|
||||
|
||||
if (loginEnabledValue) {
|
||||
http.addFilterBefore(
|
||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
@@ -164,8 +165,7 @@ public class SecurityConfiguration {
|
||||
.logoutSuccessHandler(
|
||||
new CustomLogoutSuccessHandler(applicationProperties))
|
||||
.clearAuthentication(true)
|
||||
.invalidateHttpSession( // Invalidate session
|
||||
true)
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID", "remember-me"));
|
||||
http.rememberMe(
|
||||
rememberMeConfigurer -> // Use the configurator directly
|
||||
@@ -234,7 +234,7 @@ public class SecurityConfiguration {
|
||||
.
|
||||
/*
|
||||
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
||||
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
|
||||
If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser'
|
||||
is set as true, else login fails with an error message advising the same.
|
||||
*/
|
||||
successHandler(
|
||||
@@ -258,14 +258,20 @@ public class SecurityConfiguration {
|
||||
.permitAll());
|
||||
}
|
||||
// Handle SAML
|
||||
if (applicationProperties.getSecurity().isSaml2Active()) {
|
||||
// && runningEE
|
||||
if (applicationProperties.getSecurity().isSaml2Active() && runningEE) {
|
||||
// Configure the authentication provider
|
||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||
new OpenSaml4AuthenticationProvider();
|
||||
authenticationProvider.setResponseAuthenticationConverter(
|
||||
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||
http.authenticationProvider(authenticationProvider)
|
||||
.securityContext(security ->
|
||||
security.securityContextRepository(
|
||||
new DelegatingSecurityContextRepository(
|
||||
new RequestAttributeSecurityContextRepository(),
|
||||
new HttpSessionSecurityContextRepository())
|
||||
)
|
||||
)
|
||||
.saml2Login(
|
||||
saml2 -> {
|
||||
try {
|
||||
@@ -284,12 +290,13 @@ public class SecurityConfiguration {
|
||||
.authenticationRequestResolver(
|
||||
saml2AuthenticationRequestResolver);
|
||||
} catch (Exception e) {
|
||||
log.error("Error configuring SAML2 login", e);
|
||||
log.error("Error configuring SAML 2 login", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log.info("SAML 2 login is not enabled. Using default.");
|
||||
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||
}
|
||||
return http.build();
|
||||
@@ -315,7 +322,7 @@ public class SecurityConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public boolean activSecurity() {
|
||||
public boolean activeSecurity() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
// Use API key to authenticate. This requires you to have an authentication
|
||||
// provider for API keys.
|
||||
Optional<User> user = userService.getUserByApiKey(apiKey);
|
||||
if (!user.isPresent()) {
|
||||
if (user.isEmpty()) {
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.getWriter().write("Invalid API Key.");
|
||||
return;
|
||||
|
||||
@@ -373,18 +373,15 @@ public class UserService implements UserServiceInterface {
|
||||
|
||||
public void invalidateUserSessions(String username) {
|
||||
String usernameP = "";
|
||||
|
||||
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
||||
for (SessionInformation sessionsInformation :
|
||||
sessionRegistry.getAllSessions(principal, false)) {
|
||||
if (principal instanceof UserDetails) {
|
||||
UserDetails userDetails = (UserDetails) principal;
|
||||
if (principal instanceof UserDetails userDetails) {
|
||||
usernameP = userDetails.getUsername();
|
||||
} else if (principal instanceof OAuth2User) {
|
||||
OAuth2User oAuth2User = (OAuth2User) principal;
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
usernameP = oAuth2User.getName();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||
CustomSaml2AuthenticatedPrincipal saml2User =
|
||||
(CustomSaml2AuthenticatedPrincipal) principal;
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
usernameP = saml2User.getName();
|
||||
} else if (principal instanceof String) {
|
||||
usernameP = (String) principal;
|
||||
@@ -398,6 +395,7 @@ public class UserService implements UserServiceInterface {
|
||||
|
||||
public String getCurrentUsername() {
|
||||
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
|
||||
if (principal instanceof UserDetails) {
|
||||
return ((UserDetails) principal).getUsername();
|
||||
} else if (principal instanceof OAuth2User) {
|
||||
@@ -406,8 +404,6 @@ public class UserService implements UserServiceInterface {
|
||||
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||
return ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||
} else if (principal instanceof String) {
|
||||
return (String) principal;
|
||||
} else {
|
||||
return principal.toString();
|
||||
}
|
||||
|
||||
@@ -50,8 +50,11 @@ public class CustomOAuth2AuthenticationFailureHandler
|
||||
if (error.getErrorCode().equals("Password must not be null")) {
|
||||
errorCode = "userAlreadyExistsWeb";
|
||||
}
|
||||
log.error("OAuth2 Authentication error: " + errorCode);
|
||||
log.error("OAuth2AuthenticationException", exception);
|
||||
|
||||
log.error(
|
||||
"OAuth2 Authentication error: {}",
|
||||
errorCode != null ? errorCode : exception.getMessage(),
|
||||
exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?errorOAuth=" + errorCode);
|
||||
}
|
||||
log.error("Unhandled authentication exception", exception);
|
||||
|
||||
@@ -47,23 +47,31 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
||||
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
|
||||
UsernameAttribute usernameAttribute =
|
||||
UsernameAttribute.valueOf(oauth2.getUseAsUsername().toUpperCase());
|
||||
String username = usernameAttribute.getName();
|
||||
String usernameAttributeKey = usernameAttribute.getName();
|
||||
|
||||
Optional<User> internalUser = userService.findByUsernameIgnoreCase(username);
|
||||
// todo: save user by OIDC ID instead of username
|
||||
Optional<User> internalUser =
|
||||
userService.findByUsernameIgnoreCase(user.getAttribute(usernameAttributeKey));
|
||||
|
||||
if (internalUser.isPresent()) {
|
||||
if (loginAttemptService.isBlocked(username)) {
|
||||
String internalUsername = internalUser.get().getUsername();
|
||||
if (loginAttemptService.isBlocked(internalUsername)) {
|
||||
throw new LockedException(
|
||||
"Your account has been locked due to too many failed login attempts.");
|
||||
"The account "
|
||||
+ internalUsername
|
||||
+ " has been locked due to too many failed login attempts.");
|
||||
}
|
||||
if (userService.hasPassword(username)) {
|
||||
if (userService.hasPassword(usernameAttributeKey)) {
|
||||
throw new IllegalArgumentException("Password must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
// Return a new OidcUser with adjusted attributes
|
||||
return new DefaultOidcUser(
|
||||
user.getAuthorities(), userRequest.getIdToken(), user.getUserInfo(), username);
|
||||
user.getAuthorities(),
|
||||
userRequest.getIdToken(),
|
||||
user.getUserInfo(),
|
||||
usernameAttributeKey);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Error loading OIDC user: {}", e.getMessage());
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
|
||||
|
||||
@@ -94,7 +94,7 @@ public class OAuth2Configuration {
|
||||
.clientId(keycloak.getClientId())
|
||||
.clientSecret(keycloak.getClientSecret())
|
||||
.scope(keycloak.getScopes())
|
||||
.userNameAttributeName(keycloak.getUseAsUsername().name())
|
||||
.userNameAttributeName(keycloak.getUseAsUsername().getName())
|
||||
.clientName(keycloak.getClientName())
|
||||
.build())
|
||||
: Optional.empty();
|
||||
@@ -125,7 +125,7 @@ public class OAuth2Configuration {
|
||||
.authorizationUri(google.getAuthorizationUri())
|
||||
.tokenUri(google.getTokenUri())
|
||||
.userInfoUri(google.getUserInfoUri())
|
||||
.userNameAttributeName(google.getUseAsUsername().name())
|
||||
.userNameAttributeName(google.getUseAsUsername().getName())
|
||||
.clientName(google.getClientName())
|
||||
.redirectUri(REDIRECT_URI_PATH + google.getName())
|
||||
.authorizationGrantType(AUTHORIZATION_CODE)
|
||||
@@ -158,7 +158,7 @@ public class OAuth2Configuration {
|
||||
.authorizationUri(github.getAuthorizationUri())
|
||||
.tokenUri(github.getTokenUri())
|
||||
.userInfoUri(github.getUserInfoUri())
|
||||
.userNameAttributeName(github.getUseAsUsername().name())
|
||||
.userNameAttributeName(github.getUseAsUsername().getName())
|
||||
.clientName(github.getClientName())
|
||||
.redirectUri(REDIRECT_URI_PATH + github.getName())
|
||||
.authorizationGrantType(AUTHORIZATION_CODE)
|
||||
@@ -186,6 +186,7 @@ public class OAuth2Configuration {
|
||||
oauth.getClientSecret(),
|
||||
oauth.getScopes(),
|
||||
UsernameAttribute.valueOf(oauth.getUseAsUsername().toUpperCase()),
|
||||
oauth.getLogoutUrl(),
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
@@ -220,9 +221,7 @@ public class OAuth2Configuration {
|
||||
*/
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
value = "security.oauth2.enabled",
|
||||
havingValue = "true")
|
||||
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
|
||||
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
||||
return (authorities) -> {
|
||||
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
||||
|
||||
@@ -8,7 +8,6 @@ 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;
|
||||
|
||||
@@ -22,7 +21,9 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException exception)
|
||||
throws IOException, ServletException {
|
||||
throws IOException {
|
||||
log.error("Authentication error", exception);
|
||||
|
||||
if (exception instanceof Saml2AuthenticationException) {
|
||||
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
|
||||
getRedirectStrategy()
|
||||
@@ -34,6 +35,5 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica
|
||||
response,
|
||||
"/login?errorOAuth=not_authentication_provider_found");
|
||||
}
|
||||
log.error("AuthenticationException: " + exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,13 +112,11 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
|
||||
log.debug("Successfully processed authentication for user: {}", username);
|
||||
response.sendRedirect(contextPath + "/");
|
||||
return;
|
||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||
log.debug(
|
||||
"Invalid username detected for user: {}, redirecting to logout",
|
||||
username);
|
||||
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -21,7 +21,7 @@ import stirling.software.SPDF.model.User;
|
||||
public class CustomSaml2ResponseAuthenticationConverter
|
||||
implements Converter<ResponseToken, Saml2Authentication> {
|
||||
|
||||
private UserService userService;
|
||||
private final UserService userService;
|
||||
|
||||
public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
|
||||
this.userService = userService;
|
||||
@@ -61,10 +61,10 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
||||
|
||||
// Debug log with actual values
|
||||
log.debug("Extracted SAML Attributes: " + attributes);
|
||||
log.debug("Extracted SAML Attributes: {}", attributes);
|
||||
|
||||
// Try to get username/identifier in order of preference
|
||||
String userIdentifier = null;
|
||||
String userIdentifier;
|
||||
if (hasAttribute(attributes, "username")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "username");
|
||||
} else if (hasAttribute(attributes, "emailaddress")) {
|
||||
@@ -84,10 +84,8 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
if (user != null) {
|
||||
simpleGrantedAuthority =
|
||||
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
|
||||
}
|
||||
simpleGrantedAuthority =
|
||||
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
|
||||
}
|
||||
|
||||
List<String> sessionIndexes = new ArrayList<>();
|
||||
@@ -102,7 +100,7 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
return new Saml2Authentication(
|
||||
principal,
|
||||
responseToken.getToken().getSaml2Response(),
|
||||
Collections.singletonList(simpleGrantedAuthority));
|
||||
List.of(simpleGrantedAuthority));
|
||||
}
|
||||
|
||||
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {
|
||||
|
||||
@@ -11,10 +11,12 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
|
||||
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
||||
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.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -39,7 +41,7 @@ public class SAML2Configuration {
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
|
||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getIdpCert());
|
||||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||
Resource certificateResource = samlConf.getSpCert();
|
||||
@@ -51,15 +53,26 @@ public class SAML2Configuration {
|
||||
RelyingPartyRegistration rp =
|
||||
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
||||
.signingX509Credentials(c -> c.add(signingCredential))
|
||||
.entityId(samlConf.getIdpIssuer())
|
||||
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
|
||||
.singleLogoutServiceLocation(samlConf.getIdpSingleLogoutUrl())
|
||||
.singleLogoutServiceResponseLocation("http://localhost:8080/login")
|
||||
.assertionConsumerServiceBinding(Saml2MessageBinding.POST)
|
||||
.assertionConsumerServiceLocation(
|
||||
"{baseUrl}/login/saml2/sso/{registrationId}")
|
||||
.assertingPartyMetadata(
|
||||
metadata ->
|
||||
metadata.entityId(samlConf.getIdpIssuer())
|
||||
.singleSignOnServiceLocation(
|
||||
samlConf.getIdpSingleLoginUrl())
|
||||
.verificationX509Credentials(
|
||||
c -> c.add(verificationCredential))
|
||||
.singleSignOnServiceBinding(
|
||||
Saml2MessageBinding.POST)
|
||||
.singleSignOnServiceLocation(
|
||||
samlConf.getIdpSingleLoginUrl())
|
||||
.singleLogoutServiceBinding(
|
||||
Saml2MessageBinding.POST)
|
||||
.singleLogoutServiceLocation(
|
||||
samlConf.getIdpSingleLogoutUrl())
|
||||
.wantAuthnRequestsSigned(true))
|
||||
.build();
|
||||
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
||||
@@ -71,58 +84,93 @@ public class SAML2Configuration {
|
||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||
OpenSaml4AuthenticationRequestResolver resolver =
|
||||
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||
|
||||
resolver.setAuthnRequestCustomizer(
|
||||
customizer -> {
|
||||
log.debug("Customizing SAML Authentication request");
|
||||
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||
log.debug("AuthnRequest ID: {}", authnRequest.getID());
|
||||
if (authnRequest.getID() == null) {
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
|
||||
}
|
||||
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
|
||||
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
|
||||
log.debug(
|
||||
"AuthnRequest Issuer: {}",
|
||||
authnRequest.getIssuer() != null
|
||||
? authnRequest.getIssuer().getValue()
|
||||
: "null");
|
||||
HttpServletRequest request = customizer.getRequest();
|
||||
// Log HTTP request details
|
||||
log.debug("HTTP Request Method: {}", request.getMethod());
|
||||
log.debug("Request URI: {}", request.getRequestURI());
|
||||
log.debug("Request URL: {}", request.getRequestURL().toString());
|
||||
log.debug("Query String: {}", request.getQueryString());
|
||||
log.debug("Remote Address: {}", request.getRemoteAddr());
|
||||
// Log headers
|
||||
Collections.list(request.getHeaderNames())
|
||||
.forEach(
|
||||
headerName -> {
|
||||
log.debug(
|
||||
"Header - {}: {}",
|
||||
headerName,
|
||||
request.getHeader(headerName));
|
||||
});
|
||||
// Log SAML specific parameters
|
||||
log.debug("SAML Request Parameters:");
|
||||
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
|
||||
log.debug("RelayState: {}", request.getParameter("RelayState"));
|
||||
// Log session debugrmation if exists
|
||||
if (request.getSession(false) != null) {
|
||||
log.debug("Session ID: {}", request.getSession().getId());
|
||||
}
|
||||
// Log any assertions consumer service details if present
|
||||
if (authnRequest.getAssertionConsumerServiceURL() != null) {
|
||||
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||
HttpSessionSaml2AuthenticationRequestRepository requestRepository =
|
||||
new HttpSessionSaml2AuthenticationRequestRepository();
|
||||
AbstractSaml2AuthenticationRequest saml2AuthenticationRequest =
|
||||
requestRepository.loadAuthenticationRequest(request);
|
||||
|
||||
if (saml2AuthenticationRequest != null) {
|
||||
String sessionId = request.getSession(false).getId();
|
||||
|
||||
log.debug(
|
||||
"AssertionConsumerServiceURL: {}",
|
||||
authnRequest.getAssertionConsumerServiceURL());
|
||||
}
|
||||
// Log NameID policy if present
|
||||
if (authnRequest.getNameIDPolicy() != null) {
|
||||
log.debug(
|
||||
"NameIDPolicy Format: {}",
|
||||
authnRequest.getNameIDPolicy().getFormat());
|
||||
"Retrieving SAML 2 authentication request ID from the current HTTP session {}",
|
||||
sessionId);
|
||||
|
||||
String authenticationRequestId = saml2AuthenticationRequest.getId();
|
||||
|
||||
if (!authenticationRequestId.isBlank()) {
|
||||
authnRequest.setID(authenticationRequestId);
|
||||
} else {
|
||||
log.warn(
|
||||
"No authentication request found for HTTP session {}. Generating new ID",
|
||||
sessionId);
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
||||
}
|
||||
} else {
|
||||
log.debug("Generating new authentication request ID");
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
||||
}
|
||||
|
||||
logAuthnRequestDetails(authnRequest);
|
||||
logHttpRequestDetails(request);
|
||||
});
|
||||
return resolver;
|
||||
}
|
||||
|
||||
private static void logAuthnRequestDetails(AuthnRequest authnRequest) {
|
||||
String message =
|
||||
"""
|
||||
AuthnRequest:
|
||||
|
||||
ID: {}
|
||||
Issuer: {}
|
||||
IssueInstant: {}
|
||||
AssertionConsumerService (ACS) URL: {}
|
||||
""";
|
||||
log.debug(
|
||||
message,
|
||||
authnRequest.getID(),
|
||||
authnRequest.getIssuer() != null ? authnRequest.getIssuer().getValue() : null,
|
||||
authnRequest.getIssueInstant(),
|
||||
authnRequest.getAssertionConsumerServiceURL());
|
||||
|
||||
if (authnRequest.getNameIDPolicy() != null) {
|
||||
log.debug("NameIDPolicy Format: {}", authnRequest.getNameIDPolicy().getFormat());
|
||||
}
|
||||
}
|
||||
|
||||
private static void logHttpRequestDetails(HttpServletRequest request) {
|
||||
log.debug("HTTP Headers: ");
|
||||
Collections.list(request.getHeaderNames())
|
||||
.forEach(
|
||||
headerName ->
|
||||
log.debug("{}: {}", headerName, request.getHeader(headerName)));
|
||||
String message =
|
||||
"""
|
||||
HTTP Request Method: {}
|
||||
Session ID: {}
|
||||
Request Path: {}
|
||||
Query String: {}
|
||||
Remote Address: {}
|
||||
|
||||
SAML Request Parameters:
|
||||
|
||||
SAMLRequest: {}
|
||||
RelayState: {}
|
||||
""";
|
||||
log.debug(
|
||||
message,
|
||||
request.getMethod(),
|
||||
request.getSession().getId(),
|
||||
request.getRequestURI(),
|
||||
request.getQueryString(),
|
||||
request.getRemoteAddr(),
|
||||
request.getParameter("SAMLRequest"),
|
||||
request.getParameter("RelayState"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user