oauth to saml and compare fixes etc

This commit is contained in:
Anthony Stirling 2024-11-28 19:27:37 +00:00
parent 2885fac30d
commit d20e8f7d54
13 changed files with 56 additions and 55 deletions

View File

@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@ -30,6 +31,7 @@ public class InitialSecuritySetup {
initializeAdminUser(); initializeAdminUser();
} else { } else {
databaseBackupHelper.exportDatabase(); databaseBackupHelper.exportDatabase();
userService.migrateOauth2ToSSO();
} }
initializeInternalApiUser(); initializeInternalApiUser();
} }
@ -75,4 +77,7 @@ public class InitialSecuritySetup {
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
} }
} }
} }

View File

@ -107,13 +107,14 @@ public class SecurityConfiguration {
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
}
if (loginEnabledValue) { if (loginEnabledValue) {
http.addFilterBefore( http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if (applicationProperties.getSecurity().getCsrfDisabled()) { if (!applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
} else {
CookieCsrfTokenRepository cookieRepo = CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse(); CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler = CsrfTokenRequestAttributeHandler requestHandler =
@ -268,11 +269,10 @@ public class SecurityConfiguration {
try { try {
saml2 saml2
.loginPage("/saml2") .loginPage("/saml2")
// Add this
.relyingPartyRegistrationRepository(relyingPartyRegistrations()) .relyingPartyRegistrationRepository(relyingPartyRegistrations())
.authenticationRequestResolver(new OpenSaml4AuthenticationRequestResolver( //.authenticationRequestResolver(new OpenSaml4AuthenticationRequestResolver(
relyingPartyRegistrations() // relyingPartyRegistrations()
)) // ))
.successHandler( .successHandler(
new CustomSaml2AuthenticationSuccessHandler( new CustomSaml2AuthenticationSuccessHandler(
loginAttemptService, loginAttemptService,
@ -284,16 +284,10 @@ public class SecurityConfiguration {
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
}) });
.saml2Logout(logout -> logout
.logoutUrl("/logout"))
;
} }
} else { } else {
if (applicationProperties.getSecurity().getCsrfDisabled()) { if (!applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
} else {
CookieCsrfTokenRepository cookieRepo = CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse(); CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler = CsrfTokenRequestAttributeHandler requestHandler =
@ -316,19 +310,6 @@ public class SecurityConfiguration {
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseAuthenticationConverter( provider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService)); new CustomSaml2ResponseAuthenticationConverter(userService));
provider.setAssertionValidator(token -> {
try {
HashMap<String, Object> params = new HashMap<>();
// Add 5 minutes clock skew
params.put(Saml2ErrorCodes.INVALID_ASSERTION, Duration.ofMinutes(5));
ValidationContext context = new ValidationContext(params);
return Saml2ResponseValidatorResult.success();
} catch (Exception e) {
return Saml2ResponseValidatorResult.failure();
}
});
return provider; return provider;
} }
// Client Registration Repository for OAUTH2 OIDC Login // Client Registration Repository for OAUTH2 OIDC Login
@ -489,7 +470,7 @@ public class SecurityConfiguration {
.entityId(samlConf.getIdpIssuer()) .entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(samlConf.getIdpSingleLoginUrl()) .singleSignOnServiceLocation(samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials(c -> c.add(verificationCredential)) .verificationX509Credentials(c -> c.add(verificationCredential))
.singleSignOnServiceBinding(Saml2MessageBinding.POST) // Add this .singleSignOnServiceBinding(Saml2MessageBinding.POST)
.wantAuthnRequestsSigned(true) .wantAuthnRequestsSigned(true)
) )
.build(); .build();

View File

@ -18,6 +18,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
@ -50,8 +51,17 @@ public class UserService implements UserServiceInterface {
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Transactional
public void migrateOauth2ToSSO() {
userRepository.findByAuthenticationTypeIgnoreCase("OAUTH2")
.forEach(user -> {
user.setAuthenticationType(AuthenticationType.SSO);
userRepository.save(user);
});
}
// Handle OAUTH2 login and user auto creation. // Handle OAUTH2 login and user auto creation.
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser) public boolean processSSOPostLogin(String username, boolean autoCreateUser)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
return false; return false;
@ -61,7 +71,7 @@ public class UserService implements UserServiceInterface {
return true; return true;
} }
if (autoCreateUser) { if (autoCreateUser) {
saveUser(username, AuthenticationType.OAUTH2); saveUser(username, AuthenticationType.SSO);
return true; return true;
} }
return false; return false;

View File

@ -83,7 +83,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
if (userService.usernameExistsIgnoreCase(username) if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username) && userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername( && !userService.isAuthenticationTypeByUsername(
username, AuthenticationType.OAUTH2) username, AuthenticationType.SSO)
&& oAuth.getAutoCreateUser()) { && oAuth.getAutoCreateUser()) {
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true"); response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return; return;
@ -95,7 +95,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
return; return;
} }
if (principal instanceof OAuth2User) { if (principal instanceof OAuth2User) {
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser()); userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
} }
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
return; return;

View File

@ -63,7 +63,7 @@ public class CustomSaml2AuthenticationSuccessHandler
if (userService.usernameExistsIgnoreCase(username) if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username) && userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername( && !userService.isAuthenticationTypeByUsername(
username, AuthenticationType.OAUTH2) username, AuthenticationType.SSO)
&& saml2.getAutoCreateUser()) { && saml2.getAutoCreateUser()) {
response.sendRedirect( response.sendRedirect(
contextPath + "/logout?oauth2AuthenticationErrorWeb=true"); contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
@ -76,7 +76,7 @@ public class CustomSaml2AuthenticationSuccessHandler
contextPath + "/login?erroroauth=oauth2_admin_blocked_user"); contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
return; return;
} }
userService.processOAuth2PostLogin(username, saml2.getAutoCreateUser()); userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
return; return;
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {

View File

@ -244,8 +244,8 @@ public class UserController {
return new RedirectView("/addUsers?messageType=invalidRole", true); return new RedirectView("/addUsers?messageType=invalidRole", true);
} }
if (authType.equalsIgnoreCase(AuthenticationType.OAUTH2.toString())) { if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
userService.saveUser(username, AuthenticationType.OAUTH2, role); userService.saveUser(username, AuthenticationType.SSO, role);
} else { } else {
if (password.isBlank()) { if (password.isBlank()) {
return new RedirectView("/addUsers?messageType=invalidPassword", true); return new RedirectView("/addUsers?messageType=invalidPassword", true);

View File

@ -2,5 +2,5 @@ package stirling.software.SPDF.model;
public enum AuthenticationType { public enum AuthenticationType {
WEB, WEB,
OAUTH2 SSO
} }

View File

@ -1,5 +1,6 @@
package stirling.software.SPDF.repository; package stirling.software.SPDF.repository;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
@ -19,4 +20,7 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username); Optional<User> findByUsername(String username);
Optional<User> findByApiKey(String apiKey); Optional<User> findByApiKey(String apiKey);
List<User> findByAuthenticationTypeIgnoreCase(String authenticationType);
} }

View File

@ -16,7 +16,7 @@ security:
csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production) csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production)
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1 loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2) loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2)
initialLogin: initialLogin:
username: '' # initial username for the first login username: '' # initial username for the first login
password: '' # initial password for the first login password: '' # initial password for the first login
@ -42,14 +42,14 @@ security:
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint
clientId: '' # client ID from your provider clientId: '' # client ID from your provider
clientSecret: '' # client secret from your provider clientSecret: '' # client secret from your provider
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
useAsUsername: email # default is 'email'; custom fields can be used as the username useAsUsername: email # default is 'email'; custom fields can be used as the username
scopes: openid, profile, email # specify the scopes for which the application will request permissions scopes: openid, profile, email # specify the scopes for which the application will request permissions
provider: google # set this to your OAuth provider's name, e.g., 'google' or 'keycloak' provider: google # set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
saml2: saml2:
enabled: false # currently in alpha, not recommended for use yet, enableAlphaFunctionality must be set to true enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true)
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
registrationId: stirling registrationId: stirling
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata

View File

@ -83,21 +83,22 @@ function setupFileInput(chooser) {
$("#" + elementId).on("change", function (e) { $("#" + elementId).on("change", function (e) {
let element = e.target; let element = e.target;
const isDragAndDrop = e.detail?.source == 'drag-drop'; const isDragAndDrop = e.detail?.source == 'drag-drop';
if (element instanceof HTMLInputElement && element.hasAttribute("multiple")) { if (element instanceof HTMLInputElement && element.hasAttribute("multiple")) {
allFiles = isDragAndDrop ? allFiles : [... allFiles, ... element.files]; allFiles = isDragAndDrop ? allFiles : [...allFiles, ...element.files];
} else { } else {
allFiles = Array.from(isDragAndDrop ? allFiles : element.files[0]); allFiles = isDragAndDrop ? allFiles : [element.files[0]];
} }
if (!isDragAndDrop) { if (!isDragAndDrop) {
let dataTransfer = new DataTransfer(); let dataTransfer = new DataTransfer();
allFiles.forEach(file => dataTransfer.items.add(file)); allFiles.forEach(file => dataTransfer.items.add(file));
element.files = dataTransfer.files; element.files = dataTransfer.files;
} }
handleFileInputChange(this); handleFileInputChange(this);
this.dispatchEvent(new CustomEvent("file-input-change", { bubbles: true })); this.dispatchEvent(new CustomEvent("file-input-change", { bubbles: true }));
}); });
function handleFileInputChange(inputElement) { function handleFileInputChange(inputElement) {
const files = allFiles; const files = allFiles;

View File

@ -189,7 +189,7 @@
<label for="authType">Authentication Type</label> <label for="authType">Authentication Type</label>
<select id="authType" name="authType" class="form-control" required> <select id="authType" name="authType" class="form-control" required>
<option value="web" selected>WEB</option> <option value="web" selected>WEB</option>
<option value="oauth2">OAUTH2</option> <option value="sso">SSO</option>
</select> </select>
</div> </div>
<div class="form-check mb-3" id="checkboxContainer"> <div class="form-check mb-3" id="checkboxContainer">
@ -267,7 +267,7 @@
var passwordFieldContainer = $('#passwordContainer'); var passwordFieldContainer = $('#passwordContainer');
var checkboxContainer = $('#checkboxContainer'); var checkboxContainer = $('#checkboxContainer');
if (authType === 'oauth2') { if (authType === 'sso') {
passwordField.removeAttr('required'); passwordField.removeAttr('required');
passwordField.prop('disabled', true).val(''); passwordField.prop('disabled', true).val('');
passwordFieldContainer.slideUp('fast'); passwordFieldContainer.slideUp('fast');

View File

@ -59,7 +59,7 @@
<link rel="stylesheet" th:href="@{'/css/fileSelect.css'}" th:if="${currentPage != 'home'}"> <link rel="stylesheet" th:href="@{'/css/fileSelect.css'}" th:if="${currentPage != 'home'}">
<link rel="stylesheet" th:href="@{'/css/footer.css'}"> <link rel="stylesheet" th:href="@{'/css/footer.css'}">
<link rel="preload" href="/fonts/google-symbol.woff2" as="font" type="font/woff2" crossorigin="anonymous"> <link rel="preload" th:href="@{'/fonts/google-symbol.woff2'}" as="font" type="font/woff2" crossorigin="anonymous">
<script th:src="@{'/js/thirdParty/fontfaceobserver.standalone.js'}"></script> <script th:src="@{'/js/thirdParty/fontfaceobserver.standalone.js'}"></script>

View File

@ -156,7 +156,7 @@
resultDiv2.innerHTML = loading; resultDiv2.innerHTML = loading;
// Create a new Worker // Create a new Worker
const worker = new Worker('/js/compare/pdfWorker.js'); const worker = new Worker('./js/compare/pdfWorker.js');
// Post messages to the worker // Post messages to the worker