|
| 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 | +} |
0 commit comments