Netflix / dgs-framework

GraphQL for Java with Spring Boot made easy.
https://netflix.github.io/dgs
Apache License 2.0
3.06k stars 295 forks source link

bug: security context cleared before websocket subscription is initialized #1294

Closed andre-aktivconsultancy closed 1 week ago

andre-aktivconsultancy commented 1 year ago

I have previously posted this on Stackoverflow, however I am more and more convinced that this is a bug on DGS therefore I decided to open this issue.

I am working on a Graphql API and want to authenticate and authorize Graphql requests. I have the setup working just fine for queries/mutations. However, with subscriptions I am running into some issues.

Expected behavior

I expect the @Secured annotation to work on a @DgsSubscription method.

Actual behavior

The Security Context is cleared before the websocket for the subscription is initialized.

Versions

graphql-dgs-platform-dependencies: 5.2.4 spring-boot-starter-parent: 2.7.3

Steps to reproduce

I have this datafetcher:

@DgsComponent
@RequiredArgsConstructor
public class StocksDataFetcher implements SupportsGlobalObjectIdentification {
    private final StocksService service;

    @DgsSubscription
    @Secured("ROLE_myRole")
    public Publisher<Stock> stocks(DataFetchingEnvironment dfe) {
        final var sctx = SecurityContextHolder.getContext();

        return service.getStocksPublisher();
    }
}

This SecurityFilter chain:

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        final var resolver = new DefaultBearerTokenResolver();
        resolver.setAllowUriQueryParameter(true);

       http.cors()
            .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeHttpRequests().anyRequest().authenticated()
            .and()
            .oauth2ResourceServer()
            .bearerTokenResolver(resolver)
            .jwt()
        ;
        return http.build();
    }
}

I have setup the graphql client to pass the jwt token as access_token query parameter.

I get the following logs:

2022-10-25 16:21:54.888 DEBUG 197007 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Securing GET /subscriptions?access_token=[TOKEN_REDACTED]
2022-10-25 16:21:54.892 DEBUG 197007 --- [nio-8080-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2022-10-25 16:21:55.211 DEBUG 197007 --- [nio-8080-exec-1] o.s.s.o.s.r.a.JwtAuthenticationProvider  : Authenticated token
2022-10-25 16:21:55.211 DEBUG 197007 --- [nio-8080-exec-1] .o.s.r.w.BearerTokenAuthenticationFilter : Set SecurityContextHolder to JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@2ba427df, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_offline_access, ROLE_uma_authorization, ROLE_default-roles-poc, ROLE_myRole]]
2022-10-25 16:21:55.219 DEBUG 197007 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Secured GET /subscriptions?access_token=[TOKEN_REDACTED]
2022-10-25 16:21:55.252 DEBUG 197007 --- [nio-8080-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2022-10-25 16:21:55.369  INFO 197007 --- [nio-8080-exec-2] .d.s.w.WebsocketGraphQLWSProtocolHandler : Initialized connection for 65fe0882-1224-d39b-92f3-17eccf63c948
2022-10-25 16:21:55.574  WARN 197007 --- [nio-8080-exec-2] n.g.e.SimpleDataFetcherExceptionHandler  : Exception while fetching data (/stocks) : An Authentication object was not found in the SecurityContext

org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:336) ~[spring-security-core-5.7.3.jar:5.7.3]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:200) ~[spring-security-core-5.7.3.jar:5.7.3]
[...]

Additional info

I deliberately tested this using the WebsocketGraphQLWSProtocolHandler. This one contains code to set the SecurityContext. The WebsocketGraphQLTransportWSProtocolHandler does not contain such code, which makes me wonder if that is a bug by itself.

srinivasankavitha commented 1 year ago

Thanks for reporting @andre-aktivconsultancy. What you identified with the missing code to set the SecurityContext is very likely the issue. We will look into a fix in the coming week/next release. In the meantime, if you are blocked on this, we do appreciate any PR contributions as well if you have cycles to fix. We are a bit constrained on time due to conflicting priorities since we do not use websocket subscriptions internally.

andre-aktivconsultancy commented 1 year ago

@srinivasankavitha I'd like to emphasize that I tested against the handler that does contain the SecurityContext related code. The logs shared are with that handler. My knowledge of Spring Security is limited, I'd appreciate any pointers on what could be the issue. I am suspicious of the fact that the Initialized connection for ... log comes from a different thread, but really not sure if that is relevant.

paulbakker commented 1 week ago

Closing because websocket support now comes from Spring Graphql. See https://netflix.github.io/dgs/spring-graphql-integration/.