Wrapper
Wrapper

Reputation: 932

GraphQL and Spring Security using @PreAuthorize?

I have a problem setting up spring security and disabling/enabling access to jwt-authenticated role-based users for graphql services. All other REST endpoints are properly protected and JWT authentication and role-based authorization are working correctly.

What I have so far:

In my WebSecurityConfigurerAdapter class, I have following code:

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.csrf().disable().cors()
                         .and()
                         .authorizeRequests().antMatchers(HttpMethod.OPTIONS, "**/student-service/auth/**").permitAll().antMatchers("**/student-service/auth/**").authenticated()
                         .and()
                         .authorizeRequests().antMatchers(HttpMethod.OPTIONS, "**/graphql/**").permitAll().antMatchers("**/graphql/**").authenticated()
                         .and()
                         .exceptionHandling()
            .authenticationEntryPoint(entryPoint).and().sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

    http.addFilterBefore(authenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    http.headers().cacheControl();

}

And on graphql service, I have a @PreAuthorize:

@Component 
public class UserResolver implements GraphQLQueryResolver{
    
    @Autowired
    UserRepo repo;

    @PreAuthorize("hasAnyAuthority('ADMIN')")
    public User findUser(int id) {
        return User.builder()
                    .id(1)
                   .email("[email protected]")
                   .password("123")
                   .username("John")
                   .bankAccount(BankAccount.builder()
                                            .id(1)
                                            .accountName("some account name")
                                            .accountNumber("some account number")
                                            .build())
                    .build();
    }
}

After getting JWT on localhost:8080/login and sending graphql query, with above configuration and code, I got:

org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:73) ~[spring-security-core-5.4.5.jar:5.4.5]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.attemptAuthorization(AbstractSecurityInterceptor.java:238) ~[spring-security-core-5.4.5.jar:5.4.5]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:208) ~[spring-security-core-5.4.5.jar:5.4.5]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:58) ~[spring-security-core-5.4.5.jar:5.4.5]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.5.jar:5.3.5]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.5.jar:5.3.5]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) ~[spring-aop-5.3.5.jar:5.3.5]

This is how request looks like from Postman:

enter image description here

GraphQL query:

query {
  findUser(id : 1) {
    id
    email
  }
}

And response:

{
    "errors": [
        {
            "message": "Access is denied",
            "locations": [
                {
                    "line": 2,
                    "column": 1
                }
            ],
            "path": [
                "findUser"
            ],
            "extensions": {
                "type": "AccessDeniedException",
                "classification": "DataFetchingException"
            }
        }
    ],
    "data": {
        "findUser": null
    }
}

application.yml file:

graphql:
  servlet:
    max-query-depth: 100
    exception-handlers-enabled: true
  playground:
    headers:
      Authorization: Bearer TOKEN 

query.graphqls file:

type Query {
    
    findUser(id: ID): User
    
}

type User {
    
    id: ID!
    username: String
    password: String
    email: String
    bankAccount: BankAccount
}

type BankAccount {
    id: ID!
    accountName: String
    accountNumber: String
    
}

Upvotes: 3

Views: 3677

Answers (1)

Mike Melusky
Mike Melusky

Reputation: 555

I spent a day trying to figure this out. In your data fetching environment if you invoke a call to

environment.getContext()

You should get back an instance of GraphQLContext which has the HTTP request and headers with the authorization. For me this was essentially an empty HashMap with no details about the request. After digging around and trying everything from AOP changes, I found a suggestion from auth0 to make a class that implements GraphQLInvocation. Here is my solution which places an instance of Spring Security context into the data fetching environment context object. I'm at least able to authenticate the data fetchers now since I have a Spring Security context to work with (with granted authorities and such.) I'd rather have a filter integrated with Spring Security (and I can get preAuthorize methods working like you are doing), but I'm rolling with this for the time for now.

@Primary
@Component
@Internal
public class SecurityContextGraphQLInvocation implements GraphQLInvocation {

    private final GraphQL graphQL;
    private final AuthenticationManager authenticationManager;

    public SecurityContextGraphQLInvocation(GraphQL graphQL, AuthenticationManager authenticationManager) {
        this.graphQL = graphQL;
        this.authenticationManager = authenticationManager;
    }

    @Override
    public CompletableFuture<ExecutionResult> invoke(GraphQLInvocationData invocationData, WebRequest webRequest) {
        final String header = webRequest.getHeader("Authorization");
        SecurityContext securityContext;
        if (header == null || !header.startsWith("Bearer ")) {
            securityContext = new SecurityContextImpl();
        } else {
            String authToken = header.substring(7);
            JwtAuthenticationToken authRequest = new JwtAuthenticationToken(authToken);
            final var authentication = authenticationManager.authenticate(authRequest);
            securityContext = new SecurityContextImpl(authentication);
        }

        ExecutionInput executionInput = ExecutionInput.newExecutionInput()
                .query(invocationData.getQuery())
                .context(securityContext)
                .operationName(invocationData.getOperationName())
                .variables(invocationData.getVariables())
                .build();
        return graphQL.executeAsync(executionInput);
    }
}

Upvotes: 1

Related Questions