-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Csrf fix and ssoAutoLogin for enterprise users (#2653)
This pull request includes several changes to the `SecurityConfiguration` and other related classes to enhance security and configuration management. The most important changes involve adding new beans, modifying logging levels, and updating dependency injections. Enhancements to security configuration: * [`src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java`](diffhunk://#diff-49df1b16b72e9fcaa7d0c58f46c94ffda0033f5f5e3ddab90a88e2f9022b66f4L3-L36): Added new dependencies and beans for `GrantedAuthoritiesMapper`, `RelyingPartyRegistrationRepository`, and `OpenSaml4AuthenticationRequestResolver`. Removed unused imports and simplified the class by removing the `@Lazy` annotation from `UserService`. [[1]](diffhunk://#diff-49df1b16b72e9fcaa7d0c58f46c94ffda0033f5f5e3ddab90a88e2f9022b66f4L3-L36) [[2]](diffhunk://#diff-49df1b16b72e9fcaa7d0c58f46c94ffda0033f5f5e3ddab90a88e2f9022b66f4L46-L63) [[3]](diffhunk://#diff-49df1b16b72e9fcaa7d0c58f46c94ffda0033f5f5e3ddab90a88e2f9022b66f4L75-R52) [[4]](diffhunk://#diff-49df1b16b72e9fcaa7d0c58f46c94ffda0033f5f5e3ddab90a88e2f9022b66f4R66-L98) [[5]](diffhunk://#diff-49df1b16b72e9fcaa7d0c58f46c94ffda0033f5f5e3ddab90a88e2f9022b66f4L109-R85) [[6]](diffhunk://#diff-49df1b16b72e9fcaa7d0c58f46c94ffda0033f5f5e3ddab90a88e2f9022b66f4R96-R98) Logging improvements: * [`src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java`](diffhunk://#diff-742f789731a32cb5aa20f7067ef18049002eec2a4909ef6f240d2a26bdcb53c4L97-R97): Changed the logging level from `info` to `debug` for the license validation response body to reduce log verbosity in production. Configuration updates: * [`src/main/java/stirling/software/SPDF/EE/EEAppConfig.java`](diffhunk://#diff-d842c2a4cf43f37ab5edcd644b19a51d614cb0e39963789e1c7e9fb28ddc1de8R30-R34): Added a new bean `ssoAutoLogin` to manage single sign-on auto-login configuration in the enterprise edition. These changes collectively enhance the security configuration and logging management of the application. Please provide a summary of the changes, including relevant motivation and context. Closes #(issue_number) ## Checklist - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have performed a self-review of my own code - [ ] I have attached images of the change if it is UI based - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] If my code has heavily changed functionality I have updated relevant docs on [Stirling-PDFs doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) - [ ] My changes generate no new warnings - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
- Loading branch information
Showing
15 changed files
with
532 additions
and
403 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
329 changes: 17 additions & 312 deletions
329
src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
213 changes: 213 additions & 0 deletions
213
src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
package stirling.software.SPDF.config.security.oauth2; | ||
|
||
import java.util.ArrayList; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
|
||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.context.annotation.Lazy; | ||
import org.springframework.security.core.GrantedAuthority; | ||
import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; | ||
import org.springframework.security.oauth2.client.registration.ClientRegistration; | ||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; | ||
import org.springframework.security.oauth2.client.registration.ClientRegistrations; | ||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; | ||
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
import stirling.software.SPDF.config.security.UserService; | ||
import stirling.software.SPDF.model.ApplicationProperties; | ||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; | ||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client; | ||
import stirling.software.SPDF.model.User; | ||
import stirling.software.SPDF.model.provider.GithubProvider; | ||
import stirling.software.SPDF.model.provider.GoogleProvider; | ||
import stirling.software.SPDF.model.provider.KeycloakProvider; | ||
|
||
@Configuration | ||
@Slf4j | ||
@ConditionalOnProperty( | ||
value = "security.oauth2.enabled", | ||
havingValue = "true", | ||
matchIfMissing = false) | ||
public class OAuth2Configuration { | ||
|
||
private final ApplicationProperties applicationProperties; | ||
@Lazy private final UserService userService; | ||
|
||
public OAuth2Configuration( | ||
ApplicationProperties applicationProperties, @Lazy UserService userService) { | ||
this.userService = userService; | ||
this.applicationProperties = applicationProperties; | ||
} | ||
|
||
@Bean | ||
@ConditionalOnProperty( | ||
value = "security.oauth2.enabled", | ||
havingValue = "true", | ||
matchIfMissing = false) | ||
public ClientRegistrationRepository clientRegistrationRepository() { | ||
List<ClientRegistration> registrations = new ArrayList<>(); | ||
githubClientRegistration().ifPresent(registrations::add); | ||
oidcClientRegistration().ifPresent(registrations::add); | ||
googleClientRegistration().ifPresent(registrations::add); | ||
keycloakClientRegistration().ifPresent(registrations::add); | ||
if (registrations.isEmpty()) { | ||
log.error("At least one OAuth2 provider must be configured"); | ||
System.exit(1); | ||
} | ||
return new InMemoryClientRegistrationRepository(registrations); | ||
} | ||
|
||
private Optional<ClientRegistration> googleClientRegistration() { | ||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); | ||
if (oauth == null || !oauth.getEnabled()) { | ||
return Optional.empty(); | ||
} | ||
Client client = oauth.getClient(); | ||
if (client == null) { | ||
return Optional.empty(); | ||
} | ||
GoogleProvider google = client.getGoogle(); | ||
return google != null && google.isSettingsValid() | ||
? Optional.of( | ||
ClientRegistration.withRegistrationId(google.getName()) | ||
.clientId(google.getClientId()) | ||
.clientSecret(google.getClientSecret()) | ||
.scope(google.getScopes()) | ||
.authorizationUri(google.getAuthorizationuri()) | ||
.tokenUri(google.getTokenuri()) | ||
.userInfoUri(google.getUserinfouri()) | ||
.userNameAttributeName(google.getUseAsUsername()) | ||
.clientName(google.getClientName()) | ||
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName()) | ||
.authorizationGrantType( | ||
org.springframework.security.oauth2.core | ||
.AuthorizationGrantType.AUTHORIZATION_CODE) | ||
.build()) | ||
: Optional.empty(); | ||
} | ||
|
||
private Optional<ClientRegistration> keycloakClientRegistration() { | ||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); | ||
if (oauth == null || !oauth.getEnabled()) { | ||
return Optional.empty(); | ||
} | ||
Client client = oauth.getClient(); | ||
if (client == null) { | ||
return Optional.empty(); | ||
} | ||
KeycloakProvider keycloak = client.getKeycloak(); | ||
return keycloak != null && keycloak.isSettingsValid() | ||
? Optional.of( | ||
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer()) | ||
.registrationId(keycloak.getName()) | ||
.clientId(keycloak.getClientId()) | ||
.clientSecret(keycloak.getClientSecret()) | ||
.scope(keycloak.getScopes()) | ||
.userNameAttributeName(keycloak.getUseAsUsername()) | ||
.clientName(keycloak.getClientName()) | ||
.build()) | ||
: Optional.empty(); | ||
} | ||
|
||
private Optional<ClientRegistration> githubClientRegistration() { | ||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); | ||
if (oauth == null || !oauth.getEnabled()) { | ||
return Optional.empty(); | ||
} | ||
Client client = oauth.getClient(); | ||
if (client == null) { | ||
return Optional.empty(); | ||
} | ||
GithubProvider github = client.getGithub(); | ||
return github != null && github.isSettingsValid() | ||
? Optional.of( | ||
ClientRegistration.withRegistrationId(github.getName()) | ||
.clientId(github.getClientId()) | ||
.clientSecret(github.getClientSecret()) | ||
.scope(github.getScopes()) | ||
.authorizationUri(github.getAuthorizationuri()) | ||
.tokenUri(github.getTokenuri()) | ||
.userInfoUri(github.getUserinfouri()) | ||
.userNameAttributeName(github.getUseAsUsername()) | ||
.clientName(github.getClientName()) | ||
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName()) | ||
.authorizationGrantType( | ||
org.springframework.security.oauth2.core | ||
.AuthorizationGrantType.AUTHORIZATION_CODE) | ||
.build()) | ||
: Optional.empty(); | ||
} | ||
|
||
private Optional<ClientRegistration> oidcClientRegistration() { | ||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); | ||
if (oauth == null | ||
|| oauth.getIssuer() == null | ||
|| oauth.getIssuer().isEmpty() | ||
|| oauth.getClientId() == null | ||
|| oauth.getClientId().isEmpty() | ||
|| oauth.getClientSecret() == null | ||
|| oauth.getClientSecret().isEmpty() | ||
|| oauth.getScopes() == null | ||
|| oauth.getScopes().isEmpty() | ||
|| oauth.getUseAsUsername() == null | ||
|| oauth.getUseAsUsername().isEmpty()) { | ||
return Optional.empty(); | ||
} | ||
return Optional.of( | ||
ClientRegistrations.fromIssuerLocation(oauth.getIssuer()) | ||
.registrationId("oidc") | ||
.clientId(oauth.getClientId()) | ||
.clientSecret(oauth.getClientSecret()) | ||
.scope(oauth.getScopes()) | ||
.userNameAttributeName(oauth.getUseAsUsername()) | ||
.clientName("OIDC") | ||
.build()); | ||
} | ||
|
||
/* | ||
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database. | ||
This is required for the internal; 'hasRole()' function to give out the correct role. | ||
*/ | ||
@Bean | ||
@ConditionalOnProperty( | ||
value = "security.oauth2.enabled", | ||
havingValue = "true", | ||
matchIfMissing = false) | ||
GrantedAuthoritiesMapper userAuthoritiesMapper() { | ||
return (authorities) -> { | ||
Set<GrantedAuthority> mappedAuthorities = new HashSet<>(); | ||
authorities.forEach( | ||
authority -> { | ||
// Add existing OAUTH2 Authorities | ||
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority())); | ||
// Add Authorities from database for existing user, if user is present. | ||
if (authority instanceof OAuth2UserAuthority oauth2Auth) { | ||
String useAsUsername = | ||
applicationProperties | ||
.getSecurity() | ||
.getOauth2() | ||
.getUseAsUsername(); | ||
Optional<User> userOpt = | ||
userService.findByUsernameIgnoreCase( | ||
(String) oauth2Auth.getAttributes().get(useAsUsername)); | ||
if (userOpt.isPresent()) { | ||
User user = userOpt.get(); | ||
if (user != null) { | ||
mappedAuthorities.add( | ||
new SimpleGrantedAuthority( | ||
userService.findRole(user).getAuthority())); | ||
} | ||
} | ||
} | ||
}); | ||
return mappedAuthorities; | ||
}; | ||
} | ||
} |
136 changes: 136 additions & 0 deletions
136
src/main/java/stirling/software/SPDF/config/security/saml2/SAML2Configuration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package stirling.software.SPDF.config.security.saml2; | ||
|
||
import java.security.cert.X509Certificate; | ||
import java.util.Collections; | ||
import java.util.UUID; | ||
|
||
import org.opensaml.saml.saml2.core.AuthnRequest; | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
import org.springframework.context.annotation.Bean; | ||
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.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.authentication.OpenSaml4AuthenticationRequestResolver; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import lombok.extern.slf4j.Slf4j; | ||
import stirling.software.SPDF.model.ApplicationProperties; | ||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2; | ||
|
||
@Configuration | ||
@Slf4j | ||
@ConditionalOnProperty( | ||
value = "security.saml2.enabled", | ||
havingValue = "true", | ||
matchIfMissing = false) | ||
public class SAML2Configuration { | ||
|
||
private final ApplicationProperties applicationProperties; | ||
|
||
public SAML2Configuration(ApplicationProperties applicationProperties) { | ||
|
||
this.applicationProperties = applicationProperties; | ||
} | ||
|
||
@Bean | ||
@ConditionalOnProperty( | ||
name = "security.saml2.enabled", | ||
havingValue = "true", | ||
matchIfMissing = false) | ||
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception { | ||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2(); | ||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert()); | ||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert); | ||
Resource privateKeyResource = samlConf.getPrivateKey(); | ||
Resource certificateResource = samlConf.getSpCert(); | ||
Saml2X509Credential signingCredential = | ||
new Saml2X509Credential( | ||
CertificateUtils.readPrivateKey(privateKeyResource), | ||
CertificateUtils.readCertificate(certificateResource), | ||
Saml2X509CredentialType.SIGNING); | ||
RelyingPartyRegistration rp = | ||
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId()) | ||
.signingX509Credentials(c -> c.add(signingCredential)) | ||
.assertingPartyMetadata( | ||
metadata -> | ||
metadata.entityId(samlConf.getIdpIssuer()) | ||
.singleSignOnServiceLocation( | ||
samlConf.getIdpSingleLoginUrl()) | ||
.verificationX509Credentials( | ||
c -> c.add(verificationCredential)) | ||
.singleSignOnServiceBinding( | ||
Saml2MessageBinding.POST) | ||
.wantAuthnRequestsSigned(true)) | ||
.build(); | ||
return new InMemoryRelyingPartyRegistrationRepository(rp); | ||
} | ||
|
||
@Bean | ||
@ConditionalOnProperty( | ||
name = "security.saml2.enabled", | ||
havingValue = "true", | ||
matchIfMissing = false) | ||
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver( | ||
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) { | ||
log.debug( | ||
"AssertionConsumerServiceURL: {}", | ||
authnRequest.getAssertionConsumerServiceURL()); | ||
} | ||
// Log NameID policy if present | ||
if (authnRequest.getNameIDPolicy() != null) { | ||
log.debug( | ||
"NameIDPolicy Format: {}", | ||
authnRequest.getNameIDPolicy().getFormat()); | ||
} | ||
}); | ||
return resolver; | ||
} | ||
} |
Oops, something went wrong.