mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01:00
fixing oauth redirect issues
# Conflicts: # app/proprietary/src/main/java/stirling/software/proprietary/security/filter/UserAuthenticationFilter.java # Conflicts: # app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java
This commit is contained in:
parent
b4f9ed031e
commit
e6c49d1737
@ -71,6 +71,7 @@ public class SecurityConfiguration {
|
||||
private final boolean loginEnabledValue;
|
||||
private final boolean runningProOrHigher;
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final ApplicationProperties.Security securityProperties;
|
||||
private final AppConfig appConfig;
|
||||
private final UserAuthenticationFilter userAuthenticationFilter;
|
||||
@ -90,6 +91,7 @@ public class SecurityConfiguration {
|
||||
@Qualifier("loginEnabled") boolean loginEnabledValue,
|
||||
@Qualifier("runningProOrHigher") boolean runningProOrHigher,
|
||||
AppConfig appConfig,
|
||||
ApplicationProperties applicationProperties,
|
||||
ApplicationProperties.Security securityProperties,
|
||||
UserAuthenticationFilter userAuthenticationFilter,
|
||||
JwtServiceInterface jwtService,
|
||||
@ -106,6 +108,7 @@ public class SecurityConfiguration {
|
||||
this.loginEnabledValue = loginEnabledValue;
|
||||
this.runningProOrHigher = runningProOrHigher;
|
||||
this.appConfig = appConfig;
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.securityProperties = securityProperties;
|
||||
this.userAuthenticationFilter = userAuthenticationFilter;
|
||||
this.jwtService = jwtService;
|
||||
@ -127,53 +130,46 @@ public class SecurityConfiguration {
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration cfg = new CorsConfiguration();
|
||||
|
||||
// Set allowed origin patterns
|
||||
if (appConfig.v2Enabled()) {
|
||||
// Development mode - allow common development ports
|
||||
cfg.setAllowedOriginPatterns(
|
||||
// Read CORS allowed origins from settings
|
||||
if (applicationProperties.getSystem() != null
|
||||
&& applicationProperties.getSystem().getCorsAllowedOrigins() != null
|
||||
&& !applicationProperties.getSystem().getCorsAllowedOrigins().isEmpty()) {
|
||||
|
||||
List<String> allowedOrigins = applicationProperties.getSystem().getCorsAllowedOrigins();
|
||||
|
||||
cfg.setAllowedOrigins(allowedOrigins);
|
||||
log.info("CORS configured with allowed origins from settings.yml: {}", allowedOrigins);
|
||||
|
||||
// Set allowed methods explicitly
|
||||
cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
|
||||
// Set allowed headers explicitly
|
||||
cfg.setAllowedHeaders(
|
||||
List.of(
|
||||
"http://localhost:3000", // Common React dev server
|
||||
"http://localhost:5173", // Vite default port
|
||||
"http://localhost:5174", // Vite alternate port
|
||||
"http://localhost:8080", // Backend port
|
||||
"http://localhost:*", // Any localhost port
|
||||
"https://localhost:*" // HTTPS localhost
|
||||
));
|
||||
log.info("CORS configured for development mode (v2 enabled)");
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-Requested-With",
|
||||
"Accept",
|
||||
"Origin",
|
||||
"X-API-KEY",
|
||||
"X-CSRF-TOKEN"));
|
||||
|
||||
// Set exposed headers (headers that the browser can access)
|
||||
cfg.setExposedHeaders(
|
||||
List.of("WWW-Authenticate", "X-Total-Count", "X-Page-Number", "X-Page-Size"));
|
||||
|
||||
// Allow credentials (cookies, authorization headers)
|
||||
cfg.setAllowCredentials(true);
|
||||
|
||||
// Set max age for preflight cache
|
||||
cfg.setMaxAge(3600L);
|
||||
} else {
|
||||
// Production mode - be more restrictive
|
||||
// You should configure production domains here
|
||||
cfg.setAllowedOriginPatterns(
|
||||
List.of(
|
||||
"http://localhost:*", // Still allow localhost for local deployments
|
||||
"https://localhost:*"));
|
||||
log.info("CORS configured for production mode");
|
||||
// No CORS origins configured - CORS is disabled (secure by default)
|
||||
// In production, frontend and backend are served from same origin (no CORS needed)
|
||||
log.info(
|
||||
"CORS is disabled - no allowed origins configured in settings.yml (system.corsAllowedOrigins)");
|
||||
}
|
||||
|
||||
// Set allowed methods explicitly
|
||||
cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
|
||||
// Set allowed headers explicitly
|
||||
cfg.setAllowedHeaders(
|
||||
List.of(
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-Requested-With",
|
||||
"Accept",
|
||||
"Origin",
|
||||
"X-API-KEY",
|
||||
"X-CSRF-TOKEN"));
|
||||
|
||||
// Set exposed headers (headers that the browser can access)
|
||||
cfg.setExposedHeaders(
|
||||
List.of("WWW-Authenticate", "X-Total-Count", "X-Page-Number", "X-Page-Size"));
|
||||
|
||||
// Allow credentials (cookies, authorization headers)
|
||||
cfg.setAllowCredentials(true);
|
||||
|
||||
// Set max age for preflight cache
|
||||
cfg.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", cfg);
|
||||
return source;
|
||||
@ -181,7 +177,7 @@ public class SecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
// Enable CORS with our custom configuration
|
||||
// Enable CORS with custom configuration
|
||||
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
|
||||
|
||||
if (securityProperties.getCsrfDisabled() || !loginEnabledValue) {
|
||||
@ -194,11 +190,8 @@ public class SecurityConfiguration {
|
||||
http.addFilterBefore(
|
||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterBefore(
|
||||
rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
if (v2Enabled) {
|
||||
http.addFilterBefore(jwtAuthenticationFilter(), UserAuthenticationFilter.class);
|
||||
}
|
||||
rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterBefore(jwtAuthenticationFilter(), UserAuthenticationFilter.class);
|
||||
|
||||
if (!securityProperties.getCsrfDisabled()) {
|
||||
CookieCsrfTokenRepository cookieRepo =
|
||||
@ -337,6 +330,8 @@ public class SecurityConfiguration {
|
||||
"/api/v1/auth/login")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/auth/refresh")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/auth/logout")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/proprietary/ui-data/account")
|
||||
|| trimmedUri.startsWith("/v1/api-docs")
|
||||
|
||||
@ -135,18 +135,24 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
|
||||
private static boolean isPublicAuthEndpoint(String requestURI, String contextPath) {
|
||||
// Remove context path from URI to normalize path matching
|
||||
String trimmedUri =
|
||||
requestURI.startsWith(contextPath)
|
||||
? requestURI.substring(contextPath.length())
|
||||
: requestURI;
|
||||
|
||||
// Public auth endpoints that don't require JWT
|
||||
boolean isPublicAuthEndpoint =
|
||||
requestURI.startsWith(contextPath + "/login")
|
||||
|| requestURI.startsWith(contextPath + "/signup")
|
||||
|| requestURI.startsWith(contextPath + "/auth/")
|
||||
|| requestURI.startsWith(contextPath + "/oauth2")
|
||||
|| requestURI.startsWith(contextPath + "/api/v1/auth/login")
|
||||
|| requestURI.startsWith(contextPath + "/api/v1/auth/register")
|
||||
|| requestURI.startsWith(contextPath + "/api/v1/auth/refresh")
|
||||
|| requestURI.startsWith(
|
||||
contextPath + "/api/v1/proprietary/ui-data/account")
|
||||
|| requestURI.startsWith(contextPath + "/api/v1/config");
|
||||
trimmedUri.startsWith("/login")
|
||||
|| trimmedUri.startsWith("/signup")
|
||||
|| trimmedUri.startsWith("/auth/")
|
||||
|| trimmedUri.startsWith("/oauth2")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/login")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/register")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/refresh")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/logout")
|
||||
|| trimmedUri.startsWith("/api/v1/proprietary/ui-data/account")
|
||||
|| trimmedUri.startsWith("/api/v1/config");
|
||||
return isPublicAuthEndpoint;
|
||||
}
|
||||
|
||||
|
||||
@ -105,11 +105,17 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have any authentication, deny the request
|
||||
// If we still don't have any authentication, check if it's a public endpoint. If not, deny the request
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
String method = request.getMethod();
|
||||
String contextPath = request.getContextPath();
|
||||
|
||||
// Allow public auth endpoints to pass through without authentication
|
||||
if (isPublicAuthEndpoint(requestURI, contextPath)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("GET".equalsIgnoreCase(method) && !requestURI.startsWith(contextPath + "/login")) {
|
||||
response.sendRedirect(contextPath + "/login"); // redirect to the login page
|
||||
} else {
|
||||
@ -200,6 +206,23 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private static boolean isPublicAuthEndpoint(String requestURI, String contextPath) {
|
||||
// Remove context path from URI to normalize path matching
|
||||
String trimmedUri =
|
||||
requestURI.startsWith(contextPath)
|
||||
? requestURI.substring(contextPath.length())
|
||||
: requestURI;
|
||||
|
||||
// Public auth endpoints that don't require authentication
|
||||
return trimmedUri.startsWith("/login")
|
||||
|| trimmedUri.startsWith("/auth/")
|
||||
|| trimmedUri.startsWith("/oauth2")
|
||||
|| trimmedUri.startsWith("/saml2")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/login")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/refresh")
|
||||
|| trimmedUri.startsWith("/api/v1/auth/logout");
|
||||
}
|
||||
|
||||
private enum UserLoginType {
|
||||
USERDETAILS("UserDetails"),
|
||||
OAUTH2USER("OAuth2User"),
|
||||
|
||||
@ -186,6 +186,6 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
origin.append(":").append(serverPort);
|
||||
}
|
||||
|
||||
return origin.toString() + "/auth/callback#access_token=" + jwt;
|
||||
return origin + "/auth/callback#access_token=" + jwt;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
|
||||
// Helper to get JWT from localStorage for Authorization header
|
||||
function getAuthHeaders(): HeadersInit {
|
||||
const token = localStorage.getItem('stirling_jwt');
|
||||
return token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
}
|
||||
import apiClient from '@app/services/apiClient';
|
||||
|
||||
export interface AppConfig {
|
||||
baseUrl?: string;
|
||||
@ -65,41 +60,27 @@ export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't fetch config if we're on the login page and don't have JWT
|
||||
const isLoginPage = window.location.pathname.includes('/login');
|
||||
const hasJwt = !!localStorage.getItem('stirling_jwt');
|
||||
|
||||
if (isLoginPage && !hasJwt) {
|
||||
console.debug('[AppConfig] On login page without JWT - using default config');
|
||||
setConfig({ enableLogin: true });
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch('/api/v1/config/app-config', {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
// apiClient automatically adds JWT header if available via interceptors
|
||||
const response = await apiClient.get<AppConfig>('/api/v1/config/app-config');
|
||||
const data = response.data;
|
||||
|
||||
if (!response.ok) {
|
||||
// On 401 (not authenticated), use default config with login enabled
|
||||
if (response.status === 401) {
|
||||
console.debug('[AppConfig] 401 error - using default config (login enabled)');
|
||||
setConfig({ enableLogin: true });
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
throw new Error(`Failed to fetch config: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: AppConfig = await response.json();
|
||||
console.debug('[AppConfig] Config fetched successfully:', data);
|
||||
setConfig(data);
|
||||
setFetchCount(prev => prev + 1);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
// On 401 (not authenticated), use default config with login enabled
|
||||
// This allows the app to work even without authentication
|
||||
if (err.response?.status === 401) {
|
||||
console.debug('[AppConfig] 401 error - using default config (login enabled)');
|
||||
setConfig({ enableLogin: true });
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||
setError(errorMessage);
|
||||
console.error('[AppConfig] Failed to fetch app config:', err);
|
||||
|
||||
@ -1,16 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRequestHeaders } from '@app/hooks/useRequestHeaders';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
|
||||
// Track globally fetched endpoint sets to prevent duplicate fetches across components
|
||||
const globalFetchedSets = new Set<string>();
|
||||
const globalEndpointCache: Record<string, boolean> = {};
|
||||
|
||||
// Helper to get JWT from localStorage for Authorization header
|
||||
function getAuthHeaders(): HeadersInit {
|
||||
const token = localStorage.getItem('stirling_jwt');
|
||||
return token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to check if a specific endpoint is enabled
|
||||
* This wraps the context for single endpoint checks
|
||||
@ -24,7 +18,6 @@ export function useEndpointEnabled(endpoint: string): {
|
||||
const [enabled, setEnabled] = useState<boolean | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const _headers = useRequestHeaders();
|
||||
|
||||
const fetchEndpointStatus = async () => {
|
||||
if (!endpoint) {
|
||||
@ -37,15 +30,8 @@ export function useEndpointEnabled(endpoint: string): {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await fetch(`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to check endpoint: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const isEnabled: boolean = await response.json();
|
||||
const response = await apiClient.get<boolean>(`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`);
|
||||
const isEnabled = response.data;
|
||||
setEnabled(isEnabled);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||
@ -80,8 +66,6 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
||||
const [endpointStatus, setEndpointStatus] = useState<Record<string, boolean>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastFetchedEndpoints, setLastFetchedEndpoints] = useState<string>('');
|
||||
const _headers = useRequestHeaders();
|
||||
|
||||
const fetchAllEndpointStatuses = async (force = false) => {
|
||||
const endpointsKey = [...endpoints].sort().join(',');
|
||||
@ -139,27 +123,8 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
||||
// Use batch API for efficiency - only fetch new endpoints
|
||||
const endpointsParam = newEndpoints.join(',');
|
||||
|
||||
const response = await fetch(`/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`, {
|
||||
headers: getAuthHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// On 401 (auth error), use optimistic fallback instead of disabling
|
||||
if (response.status === 401) {
|
||||
console.warn('[useEndpointConfig] 401 error - using optimistic fallback');
|
||||
const optimisticStatus = endpoints.reduce((acc, endpoint) => {
|
||||
acc[endpoint] = true;
|
||||
globalEndpointCache[endpoint] = true; // Cache the optimistic value
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
setEndpointStatus(optimisticStatus);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
throw new Error(`Failed to check endpoints: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const statusMap: Record<string, boolean> = await response.json();
|
||||
const response = await apiClient.get<Record<string, boolean>>(`/api/v1/config/endpoints-enabled?endpoints=${encodeURIComponent(endpointsParam)}`);
|
||||
const statusMap = response.data;
|
||||
|
||||
// Update global cache with new results
|
||||
Object.assign(globalEndpointCache, statusMap);
|
||||
@ -172,8 +137,20 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
||||
|
||||
setEndpointStatus(fullStatus);
|
||||
globalFetchedSets.add(endpointsKey);
|
||||
setLastFetchedEndpoints(endpointsKey);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
// On 401 (auth error), use optimistic fallback instead of disabling
|
||||
if (err.response?.status === 401) {
|
||||
console.warn('[useEndpointConfig] 401 error - using optimistic fallback');
|
||||
const optimisticStatus = endpoints.reduce((acc, endpoint) => {
|
||||
acc[endpoint] = true;
|
||||
globalEndpointCache[endpoint] = true; // Cache the optimistic value
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
setEndpointStatus(optimisticStatus);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
||||
setError(errorMessage);
|
||||
console.error('[EndpointConfig] Failed to check multiple endpoints:', err);
|
||||
@ -200,7 +177,6 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): {
|
||||
// Clear the global cache to allow refetch with JWT
|
||||
globalFetchedSets.clear();
|
||||
Object.keys(globalEndpointCache).forEach(key => delete globalEndpointCache[key]);
|
||||
setLastFetchedEndpoints('');
|
||||
fetchAllEndpointStatuses(true);
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user