Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright Siemens AG, 2025-2026. Part of the SW360 Portal Project.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.sw360.rest.resourceserver.security;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.sw360.rest.resourceserver.user.UserController;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class JwtBlacklistService {
private static final Logger log = LogManager.getLogger(JwtBlacklistService.class);
private final Set<String> blacklistedTokens = Collections.synchronizedSet(new HashSet<>());

public void blacklistToken(String token) {
if (token != null && !token.isEmpty()) {
synchronized (blacklistedTokens) {
blacklistedTokens.add(token);
log.info("Token added to blacklist. Blacklist size: {}", blacklistedTokens.size());
}
}
}

public boolean isTokenBlacklisted(String token) {
if (token == null || token.isEmpty()) {
return false;
}
boolean isBlacklisted = blacklistedTokens.contains(token);
log.debug("Token blacklist check - Is blacklisted: {}", isBlacklisted);
return isBlacklisted;
}

public void removeFromBlacklist(String token) {
if (token != null && !token.isEmpty()) {
synchronized (blacklistedTokens) {
blacklistedTokens.remove(token);
log.info("Token removed from blacklist. Blacklist size: {}", blacklistedTokens.size());
}
}
}

public int getBlacklistSize() {
return blacklistedTokens.size();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Siemens AG, 2025-2026. Part of the SW360 Portal Project.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.sw360.rest.resourceserver.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JwtConfig {
@Bean
public JwtBlacklistService jwtBlacklistService() {
return new JwtBlacklistService();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@

package org.eclipse.sw360.rest.resourceserver.security;

import jakarta.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.sw360.rest.resourceserver.core.SimpleAuthenticationEntryPoint;
import org.eclipse.sw360.rest.resourceserver.security.apiToken.ApiTokenAuthenticationFilter;
import org.eclipse.sw360.rest.resourceserver.security.apiToken.ApiTokenAuthenticationProvider;
import org.eclipse.sw360.rest.resourceserver.security.basic.Sw360UserAuthenticationProvider;
import org.eclipse.sw360.rest.resourceserver.security.jwt.JwtBlacklistFilter;
import org.eclipse.sw360.rest.resourceserver.security.jwt.Sw360JWTAccessTokenConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -31,6 +33,7 @@
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
Expand Down Expand Up @@ -60,15 +63,27 @@ public class ResourceServerConfiguration {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
String issuerUri;

private final JwtBlacklistService jwtBlacklistService;

// Constructor injection instead of field injection
public ResourceServerConfiguration(JwtBlacklistService jwtBlacklistService) {
this.jwtBlacklistService = jwtBlacklistService;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
ApiTokenAuthenticationFilter apiTokenAuthenticationFilter = new ApiTokenAuthenticationFilter(authenticationManager, saep);
ApiTokenAuthenticationFilter apiTokenAuthenticationFilter =
new ApiTokenAuthenticationFilter(authenticationManager, saep, jwtBlacklistService);

JwtBlacklistFilter jwtBlacklistFilter = new JwtBlacklistFilter(jwtBlacklistService);
return http
.addFilterBefore(apiTokenAuthenticationFilter, BasicAuthenticationFilter.class)
.addFilterBefore(jwtBlacklistFilter, BearerTokenAuthenticationFilter.class)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt ->
jwt.jwtAuthenticationConverter(sw360JWTAccessTokenConverter)
.jwkSetUri(issuerUri)).authenticationEntryPoint(saep))
.authorizeHttpRequests(auth -> {
auth.requestMatchers(HttpMethod.POST, "/resource/api/users/logout").authenticated(); // Add this line
auth.requestMatchers(HttpMethod.GET, "/api/health").permitAll();
auth.requestMatchers(HttpMethod.GET, "/api/info").hasAuthority("WRITE");
auth.requestMatchers(HttpMethod.GET, "/api").permitAll();
Expand All @@ -83,14 +98,29 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, Authentication
auth.requestMatchers(HttpMethod.GET, "/docs/**").permitAll();
auth.requestMatchers(HttpMethod.GET, "/mkdocs/**").permitAll();
})
.logout(logout -> logout
.logoutUrl("/api/auth/logout")
.addLogoutHandler((request, response, authentication) -> {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
jwtBlacklistService.blacklistToken(token);
}
})
.logoutSuccessHandler((request, response, authentication) -> {
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().write("Logged out successfully");
})
)
.httpBasic(Customizer.withDefaults())
.exceptionHandling(x -> x.authenticationEntryPoint(saep))
.headers(headers -> headers.xssProtection(xXssConfig -> xXssConfig.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
.headers(headers -> headers.xssProtection(xXssConfig ->
xXssConfig.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
.contentSecurityPolicy(cps -> cps.policyDirectives("script-src 'self'")))
.csrf(csrf -> csrf.disable()).build();
.csrf(csrf -> csrf.disable())
.build();
}


@Bean
AuthenticationManager authenticationManager() {
return new ProviderManager(List.of(authProvider, sw360UserAuthenticationProvider));
Expand All @@ -100,4 +130,4 @@ AuthenticationManager authenticationManager() {
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@

package org.eclipse.sw360.rest.resourceserver.security.apiToken;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.sw360.rest.resourceserver.Sw360ResourceServer;
import org.eclipse.sw360.rest.resourceserver.security.JwtBlacklistService;
import org.springframework.context.annotation.Profile;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
Expand All @@ -26,10 +29,7 @@
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.web.AuthenticationEntryPoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;

@Profile("!SECURITY_MOCK")
Expand All @@ -38,13 +38,22 @@ public class ApiTokenAuthenticationFilter implements Filter {
private static final Logger log = LogManager.getLogger(ApiTokenAuthenticationFilter.class);
private static final String AUTHENTICATION_TOKEN_PARAMETER = "authorization";
private static final String OIDC_AUTHENTICATION_TOKEN_PARAMETER = "oidcauthorization";
private static final String ERROR_TOKEN_REVOKED = "Token revoked";
private static final String ERROR_INVALID_TOKEN_FORMAT = "Invalid token format";

private JwtBlacklistService jwtBlacklistService;

private final AuthenticationManager authenticationManager;
private final AuthenticationEntryPoint authenticationEntryPoint;

public ApiTokenAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
this(authenticationManager, authenticationEntryPoint, null);
}

public ApiTokenAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint, JwtBlacklistService jwtBlacklistService) {
this.authenticationManager = authenticationManager;
this.authenticationEntryPoint = authenticationEntryPoint;
this.jwtBlacklistService = jwtBlacklistService;
}

@Override
Expand All @@ -60,36 +69,84 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
} else {
try {
Map<String, String> headers = Collections.list(((HttpServletRequest) request).getHeaderNames()).stream()
.collect(Collectors.toMap(h -> h, ((HttpServletRequest) request)::getHeader));
.collect(Collectors.toMap(h -> h.toLowerCase(), ((HttpServletRequest) request)::getHeader));

if (!headers.isEmpty() && headers.containsKey(AUTHENTICATION_TOKEN_PARAMETER)) {
String authorization = headers.get(AUTHENTICATION_TOKEN_PARAMETER);
String[] token = authorization.trim().split("\\s+");
if (token.length == 2 && token[0].equalsIgnoreCase("token")) {
if (token.length != 2) {
log.warn("Invalid token format in Authorization header: {}", authorization);
((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED, ERROR_INVALID_TOKEN_FORMAT);
return;
}
if (token[0].equalsIgnoreCase("token")) {
Authentication auth = authenticationManager.authenticate(new ApiTokenAuthentication(token[1]));
SecurityContextHolder.getContext().setAuthentication(auth);
} else if (token.length == 2 && token[0].equalsIgnoreCase("Bearer")) {
Authentication auth = authenticationManager.authenticate(new ApiTokenAuthentication(token[1]).setType(AuthType.JWKS));
SecurityContextHolder.getContext().setAuthentication(auth);
} else if (token[0].equalsIgnoreCase("Bearer")) {
if (!authenticateJwtToken(token[1], (HttpServletResponse) response, "Authorization")) {
return;
}
}
} else if (Sw360ResourceServer.IS_JWKS_VALIDATION_ENABLED && !headers.isEmpty()
&& headers.containsKey(OIDC_AUTHENTICATION_TOKEN_PARAMETER)) {
String authorization = headers.get(OIDC_AUTHENTICATION_TOKEN_PARAMETER);
String[] token = authorization.trim().split("\\s+");
if (token.length == 2 && token[0].equalsIgnoreCase("Bearer")) {
Authentication auth = authenticationManager.authenticate(new ApiTokenAuthentication(token[1]).setType(AuthType.JWKS));
SecurityContextHolder.getContext().setAuthentication(auth);
if (token.length != 2) {
log.warn("Invalid token format in OIDC Authorization header: {}", authorization);
((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED, ERROR_INVALID_TOKEN_FORMAT);
return;
}
if (token[0].equalsIgnoreCase("Bearer")) {
if (!authenticateJwtToken(token[1], (HttpServletResponse) response, "OIDC")) {
return;
}
}
}
} catch (AuthenticationException e) {
log.error("Authentication failed: {}", e.getMessage());
SecurityContextHolder.clearContext();
authenticationEntryPoint.commence((HttpServletRequest) request, (HttpServletResponse) response, e);
} catch (Exception e) {
log.error("Unexpected error in authentication filter", e);
SecurityContextHolder.clearContext();
((HttpServletResponse) response).sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal error");
return;
}
}

filterChain.doFilter(request, response);
}

private boolean authenticateJwtToken(String token, HttpServletResponse response, String logPrefix) throws IOException {
if (jwtBlacklistService != null) {
log.debug("{} Checking JWT blacklist for token", logPrefix);
if (jwtBlacklistService.isTokenBlacklisted(token)) {
log.warn("{} JWT token is blacklisted - Access denied", logPrefix);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);

Map<String, String> error = new HashMap<>();
error.put("error", ERROR_TOKEN_REVOKED);
error.put("message", "Token has been revoked. Please login again.");

new ObjectMapper().writeValue(response.getWriter(), error);
return false;
}
}

try {
Authentication auth = authenticationManager.authenticate(
new ApiTokenAuthentication(token).setType(AuthType.JWKS)
);
SecurityContextHolder.getContext().setAuthentication(auth);
return true;
} catch (AuthenticationException e) {
log.error("{} Authentication failed for JWT token", logPrefix, e);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed");
return false;
}
}

@Override
public void destroy() {
}
Expand All @@ -102,7 +159,6 @@ class ApiTokenAuthentication implements Authentication {
private static final long serialVersionUID = 1L;

private String token;

private AuthType type;

private ApiTokenAuthentication(String token) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright Siemens AG, 2025-2026. Part of the SW360 Portal Project.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

package org.eclipse.sw360.rest.resourceserver.security.jwt;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.sw360.rest.resourceserver.security.JwtBlacklistService;
import org.springframework.http.MediaType;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class JwtBlacklistFilter extends OncePerRequestFilter {
private static final Logger log = LogManager.getLogger(JwtBlacklistFilter.class);
private final JwtBlacklistService jwtBlacklistService;

public JwtBlacklistFilter(JwtBlacklistService jwtBlacklistService) {
this.jwtBlacklistService = jwtBlacklistService;
}

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {

String token = extractToken(request);

if (token != null) {
log.debug("Checking token against blacklist");
if (jwtBlacklistService.isTokenBlacklisted(token)) {
log.warn("Blocked blacklisted token access attempt");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);

Map<String, String> error = new HashMap<>();
error.put("error", "invalid_token");
error.put("error_description", "The token has been revoked");

new ObjectMapper().writeValue(response.getWriter(), error);
return;
}
}

filterChain.doFilter(request, response);
}

private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getServletPath().equals("/resource/api/users/logout");
}
}
Loading