Fix: Access to Swagger UI when login enabled (#5194)

Fixes for /swagger-ui/index.html & /v1/api-docs endpoints not being
accessible when login was enabled.

- `UserAuthenticationFilter.isPublicAuthEndpoint()` had gaps in its
check, missing `/v1/api-docs`
- Refactored `UserAuthenticationFilter` to use
`RequestUriUtils.isPublicAuthEndpoint()` instead of its own incorrect
method

Closes #5125 & #5028

---

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
Dario Ghunney Ware
2025-12-10 11:39:29 +00:00
committed by GitHub
parent f17ad56def
commit 7b26b184d1
5 changed files with 16 additions and 41 deletions
@@ -162,10 +162,9 @@ public class RequestUriUtils {
// enableLogin) // enableLogin)
|| trimmedUri.startsWith( || trimmedUri.startsWith(
"/api/v1/ui-data/footer-info") // Public footer configuration "/api/v1/ui-data/footer-info") // Public footer configuration
|| trimmedUri.startsWith("/v1/api-docs")
|| trimmedUri.startsWith("/api/v1/invite/validate") || trimmedUri.startsWith("/api/v1/invite/validate")
|| trimmedUri.startsWith("/api/v1/invite/accept") || trimmedUri.startsWith("/api/v1/invite/accept")
|| trimmedUri.contains("/v1/api-docs"); || trimmedUri.startsWith("/v1/api-docs");
} }
private static String stripContextPath(String contextPath, String requestURI) { private static String stripContextPath(String contextPath, String requestURI) {
@@ -62,10 +62,14 @@ public class OpenApiConfig {
// Add server configuration from environment variable // Add server configuration from environment variable
String swaggerServerUrl = System.getenv("SWAGGER_SERVER_URL"); String swaggerServerUrl = System.getenv("SWAGGER_SERVER_URL");
Server server;
if (swaggerServerUrl != null && !swaggerServerUrl.trim().isEmpty()) { if (swaggerServerUrl != null && !swaggerServerUrl.trim().isEmpty()) {
Server server = new Server().url(swaggerServerUrl).description("API Server"); server = new Server().url(swaggerServerUrl).description("API Server");
openAPI.addServersItem(server); } else {
// Use relative path so Swagger uses the current browser origin to avoid CORS issues when accessing via different ports
server = new Server().url("/").description("Current Server");
} }
openAPI.addServersItem(server);
// Add ErrorResponse schema to components // Add ErrorResponse schema to components
Schema<?> errorResponseSchema = Schema<?> errorResponseSchema =
@@ -26,6 +26,7 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.ApplicationProperties;
@@ -39,6 +40,7 @@ import stirling.software.proprietary.security.service.JwtServiceInterface;
import stirling.software.proprietary.security.service.UserService; import stirling.software.proprietary.security.service.UserService;
@Slf4j @Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter { public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtServiceInterface jwtService; private final JwtServiceInterface jwtService;
@@ -47,19 +49,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final AuthenticationEntryPoint authenticationEntryPoint; private final AuthenticationEntryPoint authenticationEntryPoint;
private final ApplicationProperties.Security securityProperties; private final ApplicationProperties.Security securityProperties;
public JwtAuthenticationFilter(
JwtServiceInterface jwtService,
UserService userService,
CustomUserDetailsService userDetailsService,
AuthenticationEntryPoint authenticationEntryPoint,
ApplicationProperties.Security securityProperties) {
this.jwtService = jwtService;
this.userService = userService;
this.userDetailsService = userDetailsService;
this.authenticationEntryPoint = authenticationEntryPoint;
this.securityProperties = securityProperties;
}
@Override @Override
protected void doFilterInternal( protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -68,7 +57,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
if (isStaticResource(request.getContextPath(), request.getRequestURI())) {
String requestURI = request.getRequestURI();
String contextPath = request.getContextPath();
if (isStaticResource(contextPath, requestURI)) {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
@@ -77,10 +70,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
String jwtToken = jwtService.extractToken(request); String jwtToken = jwtService.extractToken(request);
if (jwtToken == null) { if (jwtToken == null) {
// Allow specific auth endpoints to pass through without JWT // Allow auth endpoints to pass through without JWT
String requestURI = request.getRequestURI();
String contextPath = request.getContextPath();
if (!isPublicAuthEndpoint(requestURI, contextPath)) { if (!isPublicAuthEndpoint(requestURI, contextPath)) {
// For API requests, return 401 JSON // For API requests, return 401 JSON
String acceptHeader = request.getHeader("Accept"); String acceptHeader = request.getHeader("Accept");
@@ -241,24 +241,6 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); 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")
|| trimmedUri.startsWith("/api/v1/proprietary/ui-data/login");
}
private enum UserLoginType { private enum UserLoginType {
USERDETAILS("UserDetails"), USERDETAILS("UserDetails"),
OAUTH2USER("OAuth2User"), OAUTH2USER("OAuth2User"),
+1 -1
View File
@@ -1 +1 @@
export const devApiLink = "https://registry.scalar.com/@stirlingpdf/apis/stirling-pdf-processing-api/"; export const devApiLink = "/swagger-ui/index.html";