Reputation: 932
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
:
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
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