From d90e51233d2d0bc47b75597c64cdc7d0d4821aec Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 23:07:59 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A4=96=20format=20everything=20with?= =?UTF-8?q?=20pre-commit=20by=20stirlingbot=20(#5538)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-generated by [create-pull-request][1] with **stirlingbot** [1]: https://github.com/peter-evans/create-pull-request --------- Signed-off-by: stirlingbot[bot] Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> --- build.gradle | 2 +- frontend/src-tauri/tauri.conf.json | 2 +- .../core/testing/serverExperienceSimulations.ts | 2 +- .../testing/serverExperienceSimulations.ts | 2 +- scripts/convert_cff_to_ttf.py | 16 +++++++++------- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 950705d4f8..d4f26e4e91 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,7 @@ springBoot { allprojects { group = 'stirling.software' - version = '2.4.1' + version = '2.4.2' configurations.configureEach { exclude group: 'commons-logging', module: 'commons-logging' diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 41c446709c..76a6bc51ba 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Stirling-PDF", - "version": "2.4.1", + "version": "2.4.2", "identifier": "stirling.pdf.dev", "build": { "frontendDist": "../dist", diff --git a/frontend/src/core/testing/serverExperienceSimulations.ts b/frontend/src/core/testing/serverExperienceSimulations.ts index a0948ba28d..86a6b41095 100644 --- a/frontend/src/core/testing/serverExperienceSimulations.ts +++ b/frontend/src/core/testing/serverExperienceSimulations.ts @@ -38,7 +38,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: '2.4.1', + appVersion: '2.4.2', serverCertificateEnabled: false, enableAlphaFunctionality: false, serverPort: 8080, diff --git a/frontend/src/proprietary/testing/serverExperienceSimulations.ts b/frontend/src/proprietary/testing/serverExperienceSimulations.ts index 056a4aa09e..5d2d71fc6e 100644 --- a/frontend/src/proprietary/testing/serverExperienceSimulations.ts +++ b/frontend/src/proprietary/testing/serverExperienceSimulations.ts @@ -48,7 +48,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: '2.4.1', + appVersion: '2.4.2', serverCertificateEnabled: false, enableAlphaFunctionality: false, enableDesktopInstallSlide: true, diff --git a/scripts/convert_cff_to_ttf.py b/scripts/convert_cff_to_ttf.py index fb4016b3e7..3b73e3e24d 100644 --- a/scripts/convert_cff_to_ttf.py +++ b/scripts/convert_cff_to_ttf.py @@ -509,18 +509,20 @@ Examples: # Positional arguments (backward compatibility): convert_cff_to_ttf.py font.cff font.otf mapping.tounicode - """ + """, ) # Add named arguments - parser.add_argument('--input', dest='input_file', help='Input CFF file path') - parser.add_argument('--output', dest='output_file', help='Output OTF file path') - parser.add_argument('--to-unicode', dest='tounicode_file', help='ToUnicode mapping file path') + parser.add_argument("--input", dest="input_file", help="Input CFF file path") + parser.add_argument("--output", dest="output_file", help="Output OTF file path") + parser.add_argument( + "--to-unicode", dest="tounicode_file", help="ToUnicode mapping file path" + ) # Add positional arguments for backward compatibility - parser.add_argument('input_pos', nargs='?', help='Input CFF file (positional)') - parser.add_argument('output_pos', nargs='?', help='Output OTF file (positional)') - parser.add_argument('tounicode_pos', nargs='?', help='ToUnicode file (positional)') + parser.add_argument("input_pos", nargs="?", help="Input CFF file (positional)") + parser.add_argument("output_pos", nargs="?", help="Output OTF file (positional)") + parser.add_argument("tounicode_pos", nargs="?", help="ToUnicode file (positional)") args = parser.parse_args() From 1cc562a6b1f0226d54e24a81813f09a2e00f3d23 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Fri, 30 Jan 2026 15:27:35 +0000 Subject: [PATCH 2/3] Stop type checking TypeScript files that won't be run (#5607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes This PR fixes false-positive TypeScript errors in our layered build setup (core → proprietary → desktop) by ensuring each build’s typecheck only evaluates files that are actually part of that build’s reachable module graph. This prevents overridden core implementations from being typechecked in higher-layer builds where they are effectively unreachable due to alias-based overrides. ## Background We maintain multiple build targets from a layered source tree: - core: open source baseline - proprietary: core + proprietary additions/overrides - desktop: proprietary + desktop-specific additions/overrides We implement overrides via paths/aliases such that placing a file in a higher layer at the same relative path supersedes the lower-layer file at runtime. For safety, we run TypeScript typechecking independently per build target to ensure all builds remain valid. ## Problem Our existing tsconfig setup often typechecked files that are not actually reachable in a given build. Specifically: - When a file in core is overridden by a file in proprietary or desktop, the overridden core file can still be included in the TypeScript Program for the higher-layer build (typically due to broad include globs). - This produces false-positive type errors in higher-layer typecheck runs, even though those core files are effectively unreachable in the build. This created friction and noise, and meant we had to make unnecessary changes to `core` to make the other builds happy, reducing type safety in the process. ## Solution This PR adjusts the tsconfig strategy so each build target's typecheck is driven by reachable entrypoints rather than blanket inclusion of all layer source trees. Concretely: - Each build’s tsconfig now includes only: - that build’s entrypoints and layer sources that are intended to be compiled for the target - any shared/top-level sources required by the target - Lower layers (e.g., core) are not globally included in higher-layer builds; they are instead pulled in through module resolution only when actually referenced (with paths ordering ensuring the correct override wins). - This means that we still check all the files that will actually be run with whatever the overridden logic is, but avoid wasting time and introducing false-positives by not checking files which have been overridden. ## Notes Unfortunately, the config we use for the type checking can't be the same as the one we use for Vite in this strategy. Vite needs to know about the entire source tree, so it can't only include the subfolders because it causes build errors. Because of this, I've duplicated the existing (valid) tsconfig files and use them for Vite. This is a little clunky but it does the job. Some day hopefully I'll come back to it and be able to figure out a nicer way to do it, but for now at least, this solves the type checking issues without impacting the runtime builds. Also, I noticed that `@desktop` is defined as an alias, which was presumably missed when I was removing the self-aliases from the files. I don't see why you'd ever need to have a desktop file reference `@desktop` to say "import this but make it impossible for something else to override the import". I've removed the `@desktop` alias in this PR while I was in there. --- .../src/desktop/components/AppProviders.tsx | 2 +- .../SetupWizard/DesktopOAuthButtons.tsx | 2 +- .../desktop/components/SetupWizard/index.tsx | 2 +- .../orchestrator/useOnboardingOrchestrator.ts | 4 ++-- .../src/desktop/hooks/useEndpointConfig.ts | 2 +- frontend/tsconfig.core.json | 9 ++++---- frontend/tsconfig.core.vite.json | 15 +++++++++++++ frontend/tsconfig.desktop.json | 13 ++++++------ frontend/tsconfig.desktop.vite.json | 21 +++++++++++++++++++ frontend/tsconfig.json | 1 - frontend/tsconfig.proprietary.json | 11 +++++----- frontend/tsconfig.proprietary.vite.json | 18 ++++++++++++++++ frontend/vite.config.ts | 4 ++-- frontend/vitest.config.ts | 6 +++--- 14 files changed, 82 insertions(+), 28 deletions(-) create mode 100644 frontend/tsconfig.core.vite.json create mode 100644 frontend/tsconfig.desktop.vite.json create mode 100644 frontend/tsconfig.proprietary.vite.json diff --git a/frontend/src/desktop/components/AppProviders.tsx b/frontend/src/desktop/components/AppProviders.tsx index 8fcd9c64d2..02617cd0dc 100644 --- a/frontend/src/desktop/components/AppProviders.tsx +++ b/frontend/src/desktop/components/AppProviders.tsx @@ -6,7 +6,7 @@ import { SetupWizard } from '@app/components/SetupWizard'; import { useFirstLaunchCheck } from '@app/hooks/useFirstLaunchCheck'; import { useBackendInitializer } from '@app/hooks/useBackendInitializer'; import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig'; -import { connectionModeService } from '@desktop/services/connectionModeService'; +import { connectionModeService } from '@app/services/connectionModeService'; import { tauriBackendService } from '@app/services/tauriBackendService'; import { authService } from '@app/services/authService'; diff --git a/frontend/src/desktop/components/SetupWizard/DesktopOAuthButtons.tsx b/frontend/src/desktop/components/SetupWizard/DesktopOAuthButtons.tsx index d0465f3c57..a7c5d5c343 100644 --- a/frontend/src/desktop/components/SetupWizard/DesktopOAuthButtons.tsx +++ b/frontend/src/desktop/components/SetupWizard/DesktopOAuthButtons.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { authService, UserInfo } from '@app/services/authService'; import { buildOAuthCallbackHtml } from '@app/utils/oauthCallbackHtml'; import { BASE_PATH } from '@app/constants/app'; -import { STIRLING_SAAS_URL } from '@desktop/constants/connection'; +import { STIRLING_SAAS_URL } from '@app/constants/connection'; import '@app/routes/authShared/auth.css'; type KnownProviderId = 'google' | 'github' | 'keycloak' | 'azure' | 'apple' | 'oidc'; diff --git a/frontend/src/desktop/components/SetupWizard/index.tsx b/frontend/src/desktop/components/SetupWizard/index.tsx index 171684237b..a8e1f17ea9 100644 --- a/frontend/src/desktop/components/SetupWizard/index.tsx +++ b/frontend/src/desktop/components/SetupWizard/index.tsx @@ -8,7 +8,7 @@ import { SelfHostedLoginScreen } from '@app/components/SetupWizard/SelfHostedLog import { ServerConfig, connectionModeService } from '@app/services/connectionModeService'; import { AuthServiceError, authService, UserInfo } from '@app/services/authService'; import { tauriBackendService } from '@app/services/tauriBackendService'; -import { STIRLING_SAAS_URL } from '@desktop/constants/connection'; +import { STIRLING_SAAS_URL } from '@app/constants/connection'; import { listen } from '@tauri-apps/api/event'; import { useEffect } from 'react'; import '@app/routes/authShared/auth.css'; diff --git a/frontend/src/desktop/components/onboarding/orchestrator/useOnboardingOrchestrator.ts b/frontend/src/desktop/components/onboarding/orchestrator/useOnboardingOrchestrator.ts index a0e602ec95..fed28ed9df 100644 --- a/frontend/src/desktop/components/onboarding/orchestrator/useOnboardingOrchestrator.ts +++ b/frontend/src/desktop/components/onboarding/orchestrator/useOnboardingOrchestrator.ts @@ -1,6 +1,6 @@ /** * Desktop Override: Onboarding Orchestrator Hook - * + * * Simply wraps the core hook with the desktop-specific default runtime state * which has isDesktopApp set to true. */ @@ -9,7 +9,7 @@ import { useOnboardingOrchestrator as useCoreOnboardingOrchestrator, type UseOnboardingOrchestratorResult, } from '@core/components/onboarding/orchestrator/useOnboardingOrchestrator'; -import { DEFAULT_RUNTIME_STATE } from '@desktop/components/onboarding/orchestrator/onboardingConfig'; +import { DEFAULT_RUNTIME_STATE } from '@app/components/onboarding/orchestrator/onboardingConfig'; export type { OnboardingOrchestratorState, diff --git a/frontend/src/desktop/hooks/useEndpointConfig.ts b/frontend/src/desktop/hooks/useEndpointConfig.ts index 0b12868a1c..b59f2492e1 100644 --- a/frontend/src/desktop/hooks/useEndpointConfig.ts +++ b/frontend/src/desktop/hooks/useEndpointConfig.ts @@ -5,7 +5,7 @@ import apiClient from '@app/services/apiClient'; import { tauriBackendService } from '@app/services/tauriBackendService'; import { isBackendNotReadyError } from '@app/constants/backendErrors'; import type { EndpointAvailabilityDetails } from '@app/types/endpointAvailability'; -import { connectionModeService } from '@desktop/services/connectionModeService'; +import { connectionModeService } from '@app/services/connectionModeService'; import type { AppConfig } from '@app/contexts/AppConfigContext'; diff --git a/frontend/tsconfig.core.json b/frontend/tsconfig.core.json index a06e67c0f9..bccf7f84b5 100644 --- a/frontend/tsconfig.core.json +++ b/frontend/tsconfig.core.json @@ -7,9 +7,10 @@ ] } }, - "exclude": [ - "src/proprietary", - "src/desktop", - "node_modules" + "include": [ + "src/*.js", + "src/*.ts", + "src/*.tsx", + "src/core" ] } diff --git a/frontend/tsconfig.core.vite.json b/frontend/tsconfig.core.vite.json new file mode 100644 index 0000000000..a06e67c0f9 --- /dev/null +++ b/frontend/tsconfig.core.vite.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "@app/*": [ + "src/core/*" + ] + } + }, + "exclude": [ + "src/proprietary", + "src/desktop", + "node_modules" + ] +} diff --git a/frontend/tsconfig.desktop.json b/frontend/tsconfig.desktop.json index cb842c3991..679f0664f6 100644 --- a/frontend/tsconfig.desktop.json +++ b/frontend/tsconfig.desktop.json @@ -7,16 +7,15 @@ "src/proprietary/*", "src/core/*" ], - "@desktop/*": ["src/desktop/*"], "@proprietary/*": ["src/proprietary/*"], "@core/*": ["src/core/*"] } }, - "exclude": [ - "src/core/**/*.test.ts*", - "src/core/**/*.spec.ts*", - "src/proprietary/**/*.test.ts*", - "src/proprietary/**/*.spec.ts*", - "node_modules" + "include": [ + "src/*.js", + "src/*.ts", + "src/*.tsx", + "src/core/setupTests.ts", + "src/desktop" ] } diff --git a/frontend/tsconfig.desktop.vite.json b/frontend/tsconfig.desktop.vite.json new file mode 100644 index 0000000000..8d586f5bee --- /dev/null +++ b/frontend/tsconfig.desktop.vite.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig.proprietary.vite.json", + "compilerOptions": { + "paths": { + "@app/*": [ + "src/desktop/*", + "src/proprietary/*", + "src/core/*" + ], + "@proprietary/*": ["src/proprietary/*"], + "@core/*": ["src/core/*"] + } + }, + "exclude": [ + "src/core/**/*.test.ts*", + "src/core/**/*.spec.ts*", + "src/proprietary/**/*.test.ts*", + "src/proprietary/**/*.spec.ts*", + "node_modules" + ] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 4d151995b1..242f518f6b 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -36,7 +36,6 @@ "src/proprietary/*", "src/core/*" ], - "@desktop/*": ["src/desktop/*"], "@proprietary/*": ["src/proprietary/*"], "@core/*": ["src/core/*"] }, diff --git a/frontend/tsconfig.proprietary.json b/frontend/tsconfig.proprietary.json index 4c726f09a7..6b60d8bc0c 100644 --- a/frontend/tsconfig.proprietary.json +++ b/frontend/tsconfig.proprietary.json @@ -9,10 +9,11 @@ "@core/*": ["src/core/*"] } }, - "exclude": [ - "src/core/**/*.test.ts*", - "src/core/**/*.spec.ts*", - "src/desktop", - "node_modules" + "include": [ + "src/*.js", + "src/*.ts", + "src/*.tsx", + "src/core/setupTests.ts", + "src/proprietary" ] } diff --git a/frontend/tsconfig.proprietary.vite.json b/frontend/tsconfig.proprietary.vite.json new file mode 100644 index 0000000000..4c726f09a7 --- /dev/null +++ b/frontend/tsconfig.proprietary.vite.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "@app/*": [ + "src/proprietary/*", + "src/core/*" + ], + "@core/*": ["src/core/*"] + } + }, + "exclude": [ + "src/core/**/*.test.ts*", + "src/core/**/*.spec.ts*", + "src/desktop", + "node_modules" + ] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index ad269a19d4..de3dc20fc6 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -11,8 +11,8 @@ export default defineConfig(({ mode }) => { process.env.STIRLING_DESKTOP === 'true' || process.env.VITE_DESKTOP === 'true'; - const baseProject = isProprietary ? './tsconfig.proprietary.json' : './tsconfig.core.json'; - const desktopProject = isProprietary ? './tsconfig.desktop.json' : baseProject; + const baseProject = isProprietary ? './tsconfig.proprietary.vite.json' : './tsconfig.core.vite.json'; + const desktopProject = isProprietary ? './tsconfig.desktop.vite.json' : baseProject; const tsconfigProject = isDesktopMode ? desktopProject : baseProject; return { diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index 1110998964..6bfdf92dcb 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -37,7 +37,7 @@ export default defineConfig({ plugins: [ react(), tsconfigPaths({ - projects: ['./tsconfig.core.json'], + projects: ['./tsconfig.core.vite.json'], }), ], esbuild: { @@ -55,7 +55,7 @@ export default defineConfig({ plugins: [ react(), tsconfigPaths({ - projects: ['./tsconfig.proprietary.json'], + projects: ['./tsconfig.proprietary.vite.json'], }), ], esbuild: { @@ -73,7 +73,7 @@ export default defineConfig({ plugins: [ react(), tsconfigPaths({ - projects: ['./tsconfig.desktop.json'], + projects: ['./tsconfig.desktop.vite.json'], }), ], esbuild: { From 6fee27739c5736af0fada18049f7ec64b74b00bc Mon Sep 17 00:00:00 2001 From: Dario Ghunney Ware Date: Fri, 30 Jan 2026 16:27:31 +0000 Subject: [PATCH 3/3] Fixed missing AuthnRequest bug (#5606) --- .../configuration/SecurityConfiguration.java | 33 ++- ...tSaml2AuthenticationRequestRepository.java | 135 ---------- .../security/saml2/Saml2Configuration.java | 42 +-- .../security/service/UserService.java | 1 + ...l2AuthenticationRequestRepositoryTest.java | 243 ------------------ 5 files changed, 34 insertions(+), 420 deletions(-) delete mode 100644 app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java delete mode 100644 app/proprietary/src/test/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepositoryTest.java diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index 3f03dcbafd..06efcf3a1d 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -8,6 +8,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Lazy; +import org.springframework.core.annotation.Order; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @@ -185,11 +186,39 @@ public class SecurityConfiguration { } @Bean + @Order(1) + public SecurityFilterChain samlFilterChain( + HttpSecurity http, + @Lazy IPRateLimitingFilter rateLimitingFilter, + @Lazy JwtAuthenticationFilter jwtAuthenticationFilter) + throws Exception { + http.securityMatcher("/saml2/**", "/login/saml2/**"); + + SessionCreationPolicy sessionPolicy = + (securityProperties.isSaml2Active() && runningProOrHigher) + ? SessionCreationPolicy.IF_REQUIRED + : SessionCreationPolicy.STATELESS; + + return configureSecurity(http, rateLimitingFilter, jwtAuthenticationFilter, sessionPolicy); + } + + @Bean + @Order(2) public SecurityFilterChain filterChain( HttpSecurity http, @Lazy IPRateLimitingFilter rateLimitingFilter, @Lazy JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { + SessionCreationPolicy sessionPolicy = SessionCreationPolicy.STATELESS; + return configureSecurity(http, rateLimitingFilter, jwtAuthenticationFilter, sessionPolicy); + } + + private SecurityFilterChain configureSecurity( + HttpSecurity http, + @Lazy IPRateLimitingFilter rateLimitingFilter, + @Lazy JwtAuthenticationFilter jwtAuthenticationFilter, + SessionCreationPolicy sessionPolicy) + throws Exception { // Enable CORS only if we have configured origins CorsConfigurationSource corsSource = corsConfigurationSource(); if (corsSource != null) { @@ -233,9 +262,7 @@ public class SecurityConfiguration { .addFilterBefore(jwtAuthenticationFilter, UserAuthenticationFilter.class); http.sessionManagement( - sessionManagement -> - sessionManagement.sessionCreationPolicy( - SessionCreationPolicy.STATELESS)); + sessionManagement -> sessionManagement.sessionCreationPolicy(sessionPolicy)); http.authenticationProvider(daoAuthenticationProvider()); http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache())); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java deleted file mode 100644 index d0508151c5..0000000000 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepository.java +++ /dev/null @@ -1,135 +0,0 @@ -package stirling.software.proprietary.security.saml2; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; -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.web.Saml2AuthenticationRequestRepository; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import lombok.extern.slf4j.Slf4j; - -import stirling.software.proprietary.security.service.JwtServiceInterface; - -@Slf4j -public class JwtSaml2AuthenticationRequestRepository - implements Saml2AuthenticationRequestRepository { - private final Map tokenStore; - private final JwtServiceInterface jwtService; - private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; - - private static final String SAML_REQUEST_TOKEN = "stirling_saml_request_token"; - - public JwtSaml2AuthenticationRequestRepository( - Map tokenStore, - JwtServiceInterface jwtService, - RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { - this.tokenStore = tokenStore; - this.jwtService = jwtService; - this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository; - } - - @Override - public void saveAuthenticationRequest( - Saml2PostAuthenticationRequest authRequest, - HttpServletRequest request, - HttpServletResponse response) { - if (!jwtService.isJwtEnabled()) { - log.debug("V2 is not enabled, skipping SAMLRequest token storage"); - return; - } - - if (authRequest == null) { - removeAuthenticationRequest(request, response); - return; - } - - Map claims = serializeSamlRequest(authRequest); - String token = jwtService.generateToken("", claims); - String relayState = authRequest.getRelayState(); - - tokenStore.put(relayState, token); - request.setAttribute(SAML_REQUEST_TOKEN, relayState); - response.addHeader(SAML_REQUEST_TOKEN, relayState); - - log.debug("Saved SAMLRequest token with RelayState: {}", relayState); - } - - @Override - public Saml2PostAuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) { - String token = extractTokenFromStore(request); - - if (token == null) { - log.debug("No SAMLResponse token found in RelayState"); - return null; - } - - Map claims = jwtService.extractClaims(token); - return deserializeSamlRequest(claims); - } - - @Override - public Saml2PostAuthenticationRequest removeAuthenticationRequest( - HttpServletRequest request, HttpServletResponse response) { - Saml2PostAuthenticationRequest authRequest = loadAuthenticationRequest(request); - - String relayStateId = request.getParameter("RelayState"); - if (relayStateId != null) { - tokenStore.remove(relayStateId); - log.debug("Removed SAMLRequest token for RelayState ID: {}", relayStateId); - } - - return authRequest; - } - - private String extractTokenFromStore(HttpServletRequest request) { - String authnRequestId = request.getParameter("RelayState"); - - if (authnRequestId != null && !authnRequestId.isEmpty()) { - String token = tokenStore.get(authnRequestId); - - if (token != null) { - tokenStore.remove(authnRequestId); - log.debug("Retrieved SAMLRequest token for RelayState ID: {}", authnRequestId); - return token; - } else { - log.warn("No SAMLRequest token found for RelayState ID: {}", authnRequestId); - } - } - - return null; - } - - private Map serializeSamlRequest(Saml2PostAuthenticationRequest authRequest) { - Map claims = new HashMap<>(); - - claims.put("id", authRequest.getId()); - claims.put("relyingPartyRegistrationId", authRequest.getRelyingPartyRegistrationId()); - claims.put("authenticationRequestUri", authRequest.getAuthenticationRequestUri()); - claims.put("samlRequest", authRequest.getSamlRequest()); - claims.put("relayState", authRequest.getRelayState()); - - return claims; - } - - private Saml2PostAuthenticationRequest deserializeSamlRequest(Map claims) { - String relyingPartyRegistrationId = (String) claims.get("relyingPartyRegistrationId"); - RelyingPartyRegistration relyingPartyRegistration = - relyingPartyRegistrationRepository.findByRegistrationId(relyingPartyRegistrationId); - - if (relyingPartyRegistration == null) { - return null; - } - - return Saml2PostAuthenticationRequest.withRelyingPartyRegistration(relyingPartyRegistration) - .id((String) claims.get("id")) - .authenticationRequestUri((String) claims.get("authenticationRequestUri")) - .samlRequest((String) claims.get("samlRequest")) - .relayState((String) claims.get("relayState")) - .build(); - } -} diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java index 6ccffa1dac..9cbde954fe 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/saml2/Saml2Configuration.java @@ -3,7 +3,6 @@ package stirling.software.proprietary.security.saml2; import java.security.cert.X509Certificate; import java.util.Collections; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import org.opensaml.saml.saml2.core.AuthnRequest; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -12,12 +11,10 @@ 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.Saml2PostAuthenticationRequest; 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.Saml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; import jakarta.servlet.http.HttpServletRequest; @@ -27,7 +24,6 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.ApplicationProperties.Security.SAML2; -import stirling.software.proprietary.security.service.JwtServiceInterface; @Configuration @Slf4j @@ -153,22 +149,10 @@ public class Saml2Configuration { return new InMemoryRelyingPartyRegistrationRepository(rp); } - @Bean - @ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true") - public Saml2AuthenticationRequestRepository - saml2AuthenticationRequestRepository( - JwtServiceInterface jwtService, - RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { - return new JwtSaml2AuthenticationRequestRepository( - new ConcurrentHashMap<>(), jwtService, relyingPartyRegistrationRepository); - } - @Bean @ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true") public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver( - RelyingPartyRegistrationRepository relyingPartyRegistrationRepository, - Saml2AuthenticationRequestRepository - saml2AuthenticationRequestRepository) { + RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { OpenSaml4AuthenticationRequestResolver resolver = new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository); @@ -176,30 +160,10 @@ public class Saml2Configuration { customizer -> { HttpServletRequest request = customizer.getRequest(); AuthnRequest authnRequest = customizer.getAuthnRequest(); - Saml2PostAuthenticationRequest saml2AuthenticationRequest = - saml2AuthenticationRequestRepository.loadAuthenticationRequest(request); - if (saml2AuthenticationRequest != null) { - String sessionId = request.getSession(false).getId(); + // Generate a unique AuthnRequest ID for each SAML request + authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1)); - log.debug( - "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); }); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java index f2bc3cc2cc..dbff599974 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/UserService.java @@ -67,6 +67,7 @@ public class UserService implements UserServiceInterface { private final ApplicationProperties.Security.OAUTH2 oAuth2; + @Transactional public void processSSOPostLogin( String username, String ssoProviderId, diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepositoryTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepositoryTest.java deleted file mode 100644 index 915c97444c..0000000000 --- a/app/proprietary/src/test/java/stirling/software/proprietary/security/saml2/JwtSaml2AuthenticationRequestRepositoryTest.java +++ /dev/null @@ -1,243 +0,0 @@ -package stirling.software.proprietary.security.saml2; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; -import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import stirling.software.proprietary.security.service.JwtServiceInterface; - -@ExtendWith(MockitoExtension.class) -class JwtSaml2AuthenticationRequestRepositoryTest { - - private static final String SAML_REQUEST_TOKEN = "stirling_saml_request_token"; - - private Map tokenStore; - - @Mock private JwtServiceInterface jwtService; - - @Mock private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; - - private JwtSaml2AuthenticationRequestRepository jwtSaml2AuthenticationRequestRepository; - - @BeforeEach - void setUp() { - tokenStore = new ConcurrentHashMap<>(); - jwtSaml2AuthenticationRequestRepository = - new JwtSaml2AuthenticationRequestRepository( - tokenStore, jwtService, relyingPartyRegistrationRepository); - } - - @Test - void saveAuthenticationRequest() { - var authRequest = mock(Saml2PostAuthenticationRequest.class); - var request = mock(MockHttpServletRequest.class); - var response = mock(MockHttpServletResponse.class); - String token = "testToken"; - String id = "testId"; - String relayState = "testRelayState"; - String authnRequestUri = "example.com/authnRequest"; - String samlRequest = "testSamlRequest"; - String relyingPartyRegistrationId = "stirling-pdf"; - - when(jwtService.isJwtEnabled()).thenReturn(true); - when(authRequest.getRelayState()).thenReturn(relayState); - when(authRequest.getId()).thenReturn(id); - when(authRequest.getAuthenticationRequestUri()).thenReturn(authnRequestUri); - when(authRequest.getSamlRequest()).thenReturn(samlRequest); - when(authRequest.getRelyingPartyRegistrationId()).thenReturn(relyingPartyRegistrationId); - when(jwtService.generateToken(eq(""), anyMap())).thenReturn(token); - - jwtSaml2AuthenticationRequestRepository.saveAuthenticationRequest( - authRequest, request, response); - - verify(request).setAttribute(SAML_REQUEST_TOKEN, relayState); - verify(response).addHeader(SAML_REQUEST_TOKEN, relayState); - } - - @Test - void saveAuthenticationRequestWithNullRequest() { - var request = mock(MockHttpServletRequest.class); - var response = mock(MockHttpServletResponse.class); - - jwtSaml2AuthenticationRequestRepository.saveAuthenticationRequest(null, request, response); - - assertTrue(tokenStore.isEmpty()); - } - - @Test - void loadAuthenticationRequest() { - var request = mock(MockHttpServletRequest.class); - var relyingPartyRegistration = mock(RelyingPartyRegistration.class); - var assertingPartyMetadata = mock(AssertingPartyMetadata.class); - String relayState = "testRelayState"; - String token = "testToken"; - Map claims = - Map.of( - "id", "testId", - "relyingPartyRegistrationId", "stirling-pdf", - "authenticationRequestUri", "example.com/authnRequest", - "samlRequest", "testSamlRequest", - "relayState", relayState); - - when(request.getParameter("RelayState")).thenReturn(relayState); - when(jwtService.extractClaims(token)).thenReturn(claims); - when(relyingPartyRegistrationRepository.findByRegistrationId("stirling-pdf")) - .thenReturn(relyingPartyRegistration); - when(relyingPartyRegistration.getRegistrationId()).thenReturn("stirling-pdf"); - when(relyingPartyRegistration.getAssertingPartyMetadata()) - .thenReturn(assertingPartyMetadata); - when(assertingPartyMetadata.getSingleSignOnServiceLocation()) - .thenReturn("https://example.com/sso"); - tokenStore.put(relayState, token); - - var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request); - - assertNotNull(result); - assertFalse(tokenStore.containsKey(relayState)); - } - - @ParameterizedTest - @NullAndEmptySource - void loadAuthenticationRequestWithInvalidRelayState(String relayState) { - var request = mock(MockHttpServletRequest.class); - when(request.getParameter("RelayState")).thenReturn(relayState); - - var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request); - - assertNull(result); - } - - @Test - void loadAuthenticationRequestWithNonExistentToken() { - var request = mock(MockHttpServletRequest.class); - when(request.getParameter("RelayState")).thenReturn("nonExistentRelayState"); - - var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request); - - assertNull(result); - } - - @Test - void loadAuthenticationRequestWithNullRelyingPartyRegistration() { - var request = mock(MockHttpServletRequest.class); - String relayState = "testRelayState"; - String token = "testToken"; - Map claims = - Map.of( - "id", "testId", - "relyingPartyRegistrationId", "stirling-pdf", - "authenticationRequestUri", "example.com/authnRequest", - "samlRequest", "testSamlRequest", - "relayState", relayState); - - when(request.getParameter("RelayState")).thenReturn(relayState); - when(jwtService.extractClaims(token)).thenReturn(claims); - when(relyingPartyRegistrationRepository.findByRegistrationId("stirling-pdf")) - .thenReturn(null); - tokenStore.put(relayState, token); - - var result = jwtSaml2AuthenticationRequestRepository.loadAuthenticationRequest(request); - - assertNull(result); - } - - @Test - void removeAuthenticationRequest() { - var request = mock(HttpServletRequest.class); - var response = mock(HttpServletResponse.class); - var relyingPartyRegistration = mock(RelyingPartyRegistration.class); - var assertingPartyMetadata = mock(AssertingPartyMetadata.class); - String relayState = "testRelayState"; - String token = "testToken"; - Map claims = - Map.of( - "id", "testId", - "relyingPartyRegistrationId", "stirling-pdf", - "authenticationRequestUri", "example.com/authnRequest", - "samlRequest", "testSamlRequest", - "relayState", relayState); - - when(request.getParameter("RelayState")).thenReturn(relayState); - when(jwtService.extractClaims(token)).thenReturn(claims); - when(relyingPartyRegistrationRepository.findByRegistrationId("stirling-pdf")) - .thenReturn(relyingPartyRegistration); - when(relyingPartyRegistration.getRegistrationId()).thenReturn("stirling-pdf"); - when(relyingPartyRegistration.getAssertingPartyMetadata()) - .thenReturn(assertingPartyMetadata); - when(assertingPartyMetadata.getSingleSignOnServiceLocation()) - .thenReturn("https://example.com/sso"); - tokenStore.put(relayState, token); - - var result = - jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest( - request, response); - - assertNotNull(result); - assertFalse(tokenStore.containsKey(relayState)); - } - - @Test - void removeAuthenticationRequestWithNullRelayState() { - var request = mock(HttpServletRequest.class); - var response = mock(HttpServletResponse.class); - when(request.getParameter("RelayState")).thenReturn(null); - - var result = - jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest( - request, response); - - assertNull(result); - } - - @Test - void removeAuthenticationRequestWithNonExistentToken() { - var request = mock(HttpServletRequest.class); - var response = mock(HttpServletResponse.class); - when(request.getParameter("RelayState")).thenReturn("nonExistentRelayState"); - - var result = - jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest( - request, response); - - assertNull(result); - } - - @Test - void removeAuthenticationRequestWithOnlyRelayState() { - var request = mock(HttpServletRequest.class); - var response = mock(HttpServletResponse.class); - String relayState = "testRelayState"; - - when(request.getParameter("RelayState")).thenReturn(relayState); - - var result = - jwtSaml2AuthenticationRequestRepository.removeAuthenticationRequest( - request, response); - - assertNull(result); - assertFalse(tokenStore.containsKey(relayState)); - } -}