Skip to content

Commit fb25222

Browse files
Feature/whitelisted urls ignore tokens no matter valid invalid or expired extra filter validation (#25)
AadAppRoleStatelessAuthenticationFilter tries to get a valid UserPrincipal on every request (no matter if it is whitelisted). That's ok if token is not existing, if token is invalid, but if token is valid but expired, AadAppRoleStatelessAuthenticationFilter rejects request. That's an issue, and sadly, AadAppRoleStatelessAuthenticationFilter does not offer an option to ignore that. This MR will introduce a custom preFilter, ConditionalAadFilter, that will skip AadAppRoleStatelessAuthenticationFilter if request is whitelisted.
1 parent 99596c1 commit fb25222

File tree

4 files changed

+169
-20
lines changed

4 files changed

+169
-20
lines changed

src/main/java/org/opendevstack/component_catalog/config/SecurityConfiguration.java

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@
22

33
import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.AadAppRoleStatelessAuthenticationFilter;
44
import lombok.RequiredArgsConstructor;
5+
import org.opendevstack.component_catalog.config.azure.ConditionalAadFilter;
56
import org.springframework.context.annotation.Bean;
67
import org.springframework.context.annotation.Configuration;
78
import org.springframework.context.annotation.Primary;
89
import org.springframework.core.annotation.Order;
910
import org.springframework.http.HttpMethod;
1011
import org.springframework.security.config.Customizer;
1112
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
12-
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
1313
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
1414
import org.springframework.security.config.http.SessionCreationPolicy;
1515
import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
1616
import org.springframework.security.web.SecurityFilterChain;
1717
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
1818
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
1919
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
20+
import org.springframework.security.web.util.matcher.OrRequestMatcher;
21+
import org.springframework.security.web.util.matcher.RequestMatcher;
2022
import org.springframework.web.cors.CorsConfiguration;
2123

2224
@Configuration
@@ -35,19 +37,6 @@ PathPatternRequestMatcherBuilderFactoryBean pathPatternRequestMatcherBuilderFact
3537
return new PathPatternRequestMatcherBuilderFactoryBean();
3638
}
3739

38-
@Bean
39-
public WebSecurityCustomizer webSecurityCustomizer() {
40-
return web -> web.ignoring().requestMatchers(
41-
"/swagger-ui/**",
42-
"/v3/api-docs/**",
43-
"/v1/catalog-items/*/user-actions/**",
44-
"/v1/user-actions/**",
45-
"/v1/provision/*/*",
46-
"/v1/schema-validation/**",
47-
"/actuator/health"
48-
);
49-
}
50-
5140
/**
5241
* Chain #1: ONLY for DELETE /v1/provision/* -> HTTP Basic with role PROVISIONER
5342
*/
@@ -79,20 +68,39 @@ SecurityFilterChain basicForProvisionDelete(HttpSecurity http) throws Exception
7968
@Order(2)
8069
public SecurityFilterChain aadForEverythingElse(HttpSecurity http) throws Exception {
8170

71+
RequestMatcher protectedEndpoints = new OrRequestMatcher(
72+
PathPatternRequestMatcher.withDefaults().matcher("/v1/**"),
73+
PathPatternRequestMatcher.withDefaults().matcher("/actuator/**")
74+
);
75+
76+
RequestMatcher whitelistedEndpoints = new OrRequestMatcher(
77+
PathPatternRequestMatcher.withDefaults().matcher("/v1/catalog-items/*/user-actions/**"),
78+
PathPatternRequestMatcher.withDefaults().matcher("/v1/user-actions/**"),
79+
PathPatternRequestMatcher.withDefaults().matcher("/v1/schema-validation/**"),
80+
PathPatternRequestMatcher.withDefaults().matcher("/swagger-ui/**"),
81+
PathPatternRequestMatcher.withDefaults().matcher("/v3/api-docs/**"),
82+
PathPatternRequestMatcher.withDefaults().matcher("/v1/user-actions/**"),
83+
PathPatternRequestMatcher.withDefaults().matcher("/v1/provision/*/*"),
84+
PathPatternRequestMatcher.withDefaults().matcher("/actuator/health")
85+
);
86+
8287
http
83-
.authorizeHttpRequests(request -> request
84-
.requestMatchers("/v1/**", "/actuator/**")
85-
.hasAuthority("ROLE_USER") // If required, change or add proper roles set by AAD
88+
.authorizeHttpRequests(req -> req
89+
.requestMatchers(
90+
whitelistedEndpoints
91+
).permitAll()
92+
.anyRequest().hasAuthority("ROLE_USER")
8693
)
8794
.csrf(CsrfConfigurer::disable) //NOSONAR required for /actuator endpoints, STATELESS prevents CSRF
8895
.cors(c -> c.configurationSource(request ->
8996
new CorsConfiguration().applyPermitDefaultValues()))
9097
.sessionManagement(configurer ->
9198
// Avoid session caching and validation e.g. via JSESSIONID cookie, as we are stateless
9299
configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
93-
94-
// 2) Azure AD (bearer/JWT) for the rest
95-
.addFilterBefore(aadAuthFilter, UsernamePasswordAuthenticationFilter.class);
100+
.addFilterBefore(
101+
new ConditionalAadFilter(aadAuthFilter, protectedEndpoints, whitelistedEndpoints),
102+
UsernamePasswordAuthenticationFilter.class
103+
);
96104

97105
return http.build();
98106
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package org.opendevstack.component_catalog.config.azure;
2+
3+
import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.AadAppRoleStatelessAuthenticationFilter;
4+
import jakarta.servlet.FilterChain;
5+
import jakarta.servlet.ServletException;
6+
import jakarta.servlet.http.HttpServletRequest;
7+
import jakarta.servlet.http.HttpServletResponse;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.jspecify.annotations.NonNull;
10+
import org.springframework.security.web.util.matcher.RequestMatcher;
11+
import org.springframework.web.filter.OncePerRequestFilter;
12+
13+
import java.io.IOException;
14+
15+
@Slf4j
16+
public class ConditionalAadFilter extends OncePerRequestFilter {
17+
18+
// Ideally, we should create different filters and order them in the security configuration.
19+
// The thing is, AadAppRoleStatelessAuthenticationFilter is not ordered, so if we try to do so, spring will complain.
20+
// The solution would be, wrap it and add an @order annotation, or wrap it in the conditional filter itself, as we do here.
21+
private final AadAppRoleStatelessAuthenticationFilter delegate;
22+
private final RequestMatcher protectedEndpoints;
23+
private final RequestMatcher whitelistedEndpoints;
24+
25+
public ConditionalAadFilter(
26+
AadAppRoleStatelessAuthenticationFilter delegate,
27+
RequestMatcher protectedEndpoints,
28+
RequestMatcher whitelistedEndpoints) {
29+
this.delegate = delegate;
30+
this.protectedEndpoints = protectedEndpoints;
31+
this.whitelistedEndpoints = whitelistedEndpoints;
32+
}
33+
34+
@Override
35+
protected boolean shouldNotFilter(@NonNull HttpServletRequest request) {
36+
var shouldNotFilter = whitelistedEndpoints.matches(request) || !protectedEndpoints.matches(request);
37+
38+
log.debug("Validating url {}: protectedEndpoints matches? {}, whitelistedEndpoints matches? {}, shouldNotFilter? {}",
39+
request.getRequestURI(),
40+
protectedEndpoints.matches(request),
41+
whitelistedEndpoints.matches(request),
42+
shouldNotFilter);
43+
44+
return shouldNotFilter;
45+
}
46+
47+
@Override
48+
protected void doFilterInternal(
49+
@NonNull HttpServletRequest request,
50+
@NonNull HttpServletResponse response,
51+
@NonNull FilterChain filterChain) throws IOException, ServletException {
52+
53+
delegate.doFilter(request, response, filterChain);
54+
}
55+
}

src/main/resources/application-local.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ logging:
44
org.springframework: INFO
55
org.springframework.security: INFO
66
com.azure.spring.cloud.autoconfigure: INFO
7+
org.opendevstack.component_catalog.config.azure: INFO
78
org.opendevstack.component_catalog.config: INFO
89
org.opendevstack.component_catalog.server.controllers: INFO
910
org.opendevstack.component_catalog.server.api: INFO
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package org.opendevstack.component_catalog.config.azure;
2+
3+
import com.azure.spring.cloud.autoconfigure.implementation.aad.filter.AadAppRoleStatelessAuthenticationFilter;
4+
import jakarta.servlet.FilterChain;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.security.web.util.matcher.RequestMatcher;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
import static org.mockito.Mockito.*;
13+
14+
class ConditionalAadFilterTest {
15+
16+
private AadAppRoleStatelessAuthenticationFilter delegate;
17+
private RequestMatcher protectedEndpoints;
18+
private RequestMatcher whitelistedEndpoints;
19+
20+
private ConditionalAadFilter filter;
21+
22+
@BeforeEach
23+
void setup() {
24+
delegate = mock(AadAppRoleStatelessAuthenticationFilter.class);
25+
protectedEndpoints = mock(RequestMatcher.class);
26+
whitelistedEndpoints = mock(RequestMatcher.class);
27+
28+
filter = new ConditionalAadFilter(delegate, protectedEndpoints, whitelistedEndpoints);
29+
}
30+
31+
@Test
32+
void givenWhitelistedPath_whenShouldNotFilter_thenTrue() {
33+
// given
34+
HttpServletRequest request = mock(HttpServletRequest.class);
35+
when(whitelistedEndpoints.matches(request)).thenReturn(true);
36+
37+
// when
38+
boolean result = filter.shouldNotFilter(request);
39+
40+
// then
41+
assertThat(result).isTrue();
42+
}
43+
44+
@Test
45+
void givenProtectedAndNotWhitelisted_whenShouldNotFilter_thenFalse() {
46+
// given
47+
HttpServletRequest request = mock(HttpServletRequest.class);
48+
when(whitelistedEndpoints.matches(request)).thenReturn(false);
49+
when(protectedEndpoints.matches(request)).thenReturn(true);
50+
51+
// when
52+
boolean result = filter.shouldNotFilter(request);
53+
54+
// then
55+
assertThat(result).isFalse();
56+
}
57+
58+
@Test
59+
void givenNotProtectedAndNotWhitelisted_whenShouldNotFilter_thenTrue() {
60+
// given
61+
HttpServletRequest request = mock(HttpServletRequest.class);
62+
when(whitelistedEndpoints.matches(request)).thenReturn(false);
63+
when(protectedEndpoints.matches(request)).thenReturn(false);
64+
65+
// when
66+
boolean result = filter.shouldNotFilter(request);
67+
68+
// then
69+
assertThat(result).isTrue();
70+
}
71+
72+
@Test
73+
void givenAnyRequest_whenDoFilterInternal_thenDelegateIsCalled() throws Exception {
74+
// given
75+
HttpServletRequest request = mock(HttpServletRequest.class);
76+
HttpServletResponse response = mock(HttpServletResponse.class);
77+
FilterChain filterChain = mock(FilterChain.class);
78+
79+
// when
80+
filter.doFilterInternal(request, response, filterChain);
81+
82+
// then
83+
verify(delegate).doFilter(request, response, filterChain);
84+
}
85+
}

0 commit comments

Comments
 (0)