Skip to content

Commit 2fb18ca

Browse files
authored
Chapter 34: Configure subscription Pre-Auth spring security (#7)
1 parent 599fbbe commit 2fb18ca

File tree

6 files changed

+146
-31
lines changed

6 files changed

+146
-31
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.learn.graphql.config.security;
2+
3+
import graphql.kickstart.execution.subscriptions.SubscriptionSession;
4+
import graphql.kickstart.execution.subscriptions.apollo.ApolloSubscriptionConnectionListener;
5+
import graphql.kickstart.execution.subscriptions.apollo.OperationMessage;
6+
import java.util.Map;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.security.core.context.SecurityContextHolder;
10+
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
11+
import org.springframework.stereotype.Component;
12+
13+
@Slf4j
14+
@Component
15+
public class AuthenticationConnectionListener implements ApolloSubscriptionConnectionListener {
16+
17+
public static final String AUTHENTICATION = "AUTHENTICATION";
18+
19+
/**
20+
* Chapter 34: Subscriptions Spring Security Pre-Auth. When using pre-auth, you must ensure that
21+
* all the graphql requests have been previously authorized/authenticated by an upstream service.
22+
* For example, all ingress traffic to this graphql server must bypass an upstream proxy node that
23+
* will validate the request's JWT token. This code alone performs no authorization. Read more
24+
* about Pre-auth before using this.
25+
*/
26+
@Override
27+
public void onConnect(SubscriptionSession session, OperationMessage message) {
28+
log.info("onConnect with payload {}", message.getPayload());
29+
30+
var payload = (Map<String, String>) message.getPayload();
31+
32+
// Get the user id, roles (or JWT etc) and perform authentication / rejection here
33+
var userId = payload.get(GraphQLSecurityConfig.USER_ID_PRE_AUTH_HEADER);
34+
var userRoles = payload.get(GraphQLSecurityConfig.USER_ROLES_PRE_AUTH_HEADER);
35+
var grantedAuthorities = GrantedAuthorityFactory.getAuthoritiesFrom(userRoles);
36+
37+
/**
38+
39+
Q: Why do not set the token/Authentication inside Spring Security SecurityContextHolder here?
40+
41+
If the start frame is not sent directly with the connection_init then the two frames may be serviced on different threads.
42+
The thread servicing the connection_init frame will check the websocket for any further inbound frames,
43+
if false the thread will move onto another websocket. Another thread is then free to service the following start frame.
44+
In this case, that thread not have the security context of the correct session/thread.
45+
46+
Same scenario happens for onStop. (Message can be executed on different thread).
47+
48+
This seems to be why some users are reporting intermittent failures with spring security.
49+
E.g. https://github.com/graphql-java-kickstart/graphql-java-servlet/discussions/134#discussioncomment-225980
50+
51+
With the NIO connector, a small number of threads will check sessions for new frames.
52+
If a session has a frame available, the session will be passed to another thread pool which will read frame, execute it, check for another frame, execute it (loop).
53+
The session will be released when there are no further frames available. With this, we know that at most one thread will concurrently access one socket,
54+
therefore frames will be read sequentially. We can therefore extract the auth credentials from onConnect and add them to the session.getUserProperties().
55+
These properties are available in the onStart and onStop callbacks. Inside these callbacks, we can add the token to the SecurityContextHolder if we decide to use method level security,
56+
or simply access the credentials inside the subscription resolver via DataFetchingEnvironment.
57+
58+
*/
59+
60+
var token = new PreAuthenticatedAuthenticationToken(userId, null, grantedAuthorities);
61+
session.getUserProperties().put(AUTHENTICATION, token);
62+
}
63+
64+
@Override
65+
public void onStart(SubscriptionSession session, OperationMessage message) {
66+
log.info("onStart with payload {}", message.getPayload());
67+
var authentication = (Authentication) session.getUserProperties().get(AUTHENTICATION);
68+
SecurityContextHolder.getContext().setAuthentication(authentication);
69+
}
70+
71+
@Override
72+
public void onStop(SubscriptionSession session, OperationMessage message) {
73+
log.info("onStop with payload {}", message.getPayload());
74+
}
75+
76+
@Override
77+
public void onTerminate(SubscriptionSession session, OperationMessage message) {
78+
log.info("onTerminate with payload {}", message.getPayload());
79+
}
80+
81+
}

src/main/java/com/learn/graphql/config/security/GrantedAuthoritiesAuthenticationDetailsSource.java

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,8 @@
22

33
import static com.learn.graphql.config.security.GraphQLSecurityConfig.USER_ROLES_PRE_AUTH_HEADER;
44

5-
import java.util.List;
6-
import java.util.Set;
7-
import java.util.stream.Collectors;
85
import javax.servlet.http.HttpServletRequest;
9-
import org.apache.commons.lang3.StringUtils;
106
import org.springframework.security.authentication.AuthenticationDetailsSource;
11-
import org.springframework.security.core.GrantedAuthority;
12-
import org.springframework.security.core.authority.SimpleGrantedAuthority;
137
import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails;
148

159
public class GrantedAuthoritiesAuthenticationDetailsSource implements
@@ -19,16 +13,8 @@ public class GrantedAuthoritiesAuthenticationDetailsSource implements
1913
public PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails buildDetails(
2014
HttpServletRequest request) {
2115
var userRoles = request.getHeader(USER_ROLES_PRE_AUTH_HEADER);
22-
var authorities = StringUtils.isBlank(userRoles) ? List.<GrantedAuthority>of() :
23-
getAuthorities(userRoles);
16+
var authorities = GrantedAuthorityFactory.getAuthoritiesFrom(userRoles);
2417
return new PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails(request, authorities);
2518
}
2619

27-
private List<GrantedAuthority> getAuthorities(String userRoles) {
28-
return Set.of(userRoles.split(","))
29-
.stream()
30-
.map(SimpleGrantedAuthority::new)
31-
.collect(Collectors.toList());
32-
}
33-
3420
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.learn.graphql.config.security;
2+
3+
import java.util.List;
4+
import java.util.Set;
5+
import java.util.stream.Collectors;
6+
import lombok.AccessLevel;
7+
import lombok.NoArgsConstructor;
8+
import org.apache.commons.lang3.StringUtils;
9+
import org.springframework.security.core.GrantedAuthority;
10+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
11+
12+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
13+
public class GrantedAuthorityFactory {
14+
15+
public static List<GrantedAuthority> getAuthoritiesFrom(String userRoles) {
16+
if (StringUtils.isBlank(userRoles)) {
17+
return List.of();
18+
}
19+
20+
return Set.of(userRoles.split(","))
21+
.stream()
22+
.map(SimpleGrantedAuthority::new)
23+
.collect(Collectors.toList());
24+
}
25+
26+
}

src/main/java/com/learn/graphql/config/security/GraphQLSecurityConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public void configure(WebSecurity web) {
7777
.antMatchers("/actuator/health")
7878
// Permit playground for development
7979
.antMatchers("/playground", "/vendor/playground/**")
80-
// Disable security for subscription example
80+
// Subscription are secured via AuthenticationConnectionListener
8181
.antMatchers("/subscriptions");
8282
}
8383

src/main/java/com/learn/graphql/resolver/bank/mutation/BankAccountMutation.java

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,20 @@ public class BankAccountMutation implements GraphQLMutationResolver {
2929
*/
3030
public BankAccount createBankAccount(@Valid CreateBankAccountInput input) {
3131
log.info("Creating bank account for {}", input);
32+
return getBankAccount(UUID.randomUUID());
33+
}
34+
35+
/**
36+
* Schema Directive Validation (Chapter 32)
37+
*/
38+
public BankAccount updateBankAccount(UUID id, String name, int age) {
39+
log.info("Updating bank account for {}. Name: {}, age: {}", id, name, age);
40+
return getBankAccount(id);
41+
}
3242

43+
private BankAccount getBankAccount(UUID id) {
3344
var bankAccount = BankAccount.builder()
34-
.id(UUID.randomUUID())
45+
.id(id)
3546
.currency(Currency.USD)
3647
.createdAt(ZonedDateTime.now(clock))
3748
.createdOn(LocalDate.now(clock))
@@ -45,17 +56,4 @@ public BankAccount createBankAccount(@Valid CreateBankAccountInput input) {
4556
return bankAccount;
4657
}
4758

48-
/**
49-
* Schema Directive Validation (Chapter 32)
50-
*/
51-
public BankAccount updateBankAccount(UUID id, String name, int age) {
52-
log.info("Updating bank account for {}. Name: {}, age: {}", id, name, age);
53-
return BankAccount.builder()
54-
.id(UUID.randomUUID())
55-
.currency(Currency.USD)
56-
.createdAt(ZonedDateTime.now(clock))
57-
.createdOn(LocalDate.now(clock))
58-
.build();
59-
}
60-
6159
}
Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,52 @@
11
package com.learn.graphql.resolver.bank.subscription;
22

3+
import com.learn.graphql.config.security.AuthenticationConnectionListener;
34
import com.learn.graphql.domain.bank.BankAccount;
45
import com.learn.graphql.publisher.BankAccountPublisher;
6+
import graphql.kickstart.servlet.context.GraphQLWebSocketContext;
57
import graphql.kickstart.tools.GraphQLSubscriptionResolver;
8+
import graphql.schema.DataFetchingEnvironment;
69
import java.util.UUID;
710
import lombok.RequiredArgsConstructor;
11+
import lombok.extern.slf4j.Slf4j;
812
import org.reactivestreams.Publisher;
13+
import org.springframework.security.access.prepost.PreAuthorize;
14+
import org.springframework.security.core.Authentication;
15+
import org.springframework.security.core.context.SecurityContextHolder;
916
import org.springframework.stereotype.Component;
1017

1118
/**
1219
* Subscription (Chapter 33)
1320
*/
21+
@Slf4j
1422
@Component
1523
@RequiredArgsConstructor
1624
public class BankAccountSubscription implements GraphQLSubscriptionResolver {
1725

1826
private final BankAccountPublisher bankAccountPublisher;
1927

28+
@PreAuthorize("hasAuthority('get:bank_account')")
2029
public Publisher<BankAccount> bankAccounts() {
2130
return bankAccountPublisher.getBankAccountPublisher();
2231
}
2332

24-
public Publisher<BankAccount> bankAccount(UUID id) {
33+
@PreAuthorize("hasAuthority('get:bank_account')")
34+
public Publisher<BankAccount> bankAccount(UUID id, DataFetchingEnvironment e) {
35+
log.info("Creating bank account publisher for user Id: {}",
36+
SecurityContextHolder.getContext().getAuthentication().getPrincipal());
37+
38+
// As an alternative to spring-security, you can access the authentication via the DataFetchingEnvironment
39+
GraphQLWebSocketContext context = getContext(e);
40+
var authentication = (Authentication) context.getSession().getUserProperties()
41+
.get(AuthenticationConnectionListener.AUTHENTICATION);
42+
log.info("Creating bank account publisher for user Id: {}",
43+
authentication.getPrincipal());
44+
2545
return bankAccountPublisher.getBankAccountPublisherFor(id);
2646
}
2747

48+
private <T> T getContext(DataFetchingEnvironment e) {
49+
return e.getContext();
50+
}
51+
2852
}

0 commit comments

Comments
 (0)