As it turns out we made a typo in our GraphQL Spring Boot configuration file. After correction, all works perfectly fine. I'm attaching Spring Boot GraphQL config + source code if any would face a similar case.
application.yaml
server:
port: ${GRAPHQL_PORT:4000}
spring:
graphql:
path: ${GRAPHQL_PATH:/graphql}
websocket:
path: ${GRAPHQL_PATH:/subscription}
graphiql:
enabled: ${GRAPHQL_DASHBOARD:true}
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${COGNITO_URI:https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_xxxxxxxxxx}
audiences: ${COGNITO_CLIENT:xxxxxxxxxx}
GraphqlHttpSecurityAdapter.java
@Slf4j
@Configuration
@EnableWebSecurity
@EnableMethodSecurity()
public class GraphqlHttpSecurityAdapter {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
@Bean
public DefaultSecurityFilterChain graphqlSecurityChain(HttpSecurity http) throws Exception {
return http.cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.oauth2ResourceServer(
oauthConfigurer ->
oauthConfigurer.jwt(
jwtConfigurer -> jwtConfigurer.decoder(JwtDecoders.fromIssuerLocation(issuer))))
.exceptionHandling(
exHandlingConfigurer ->
exHandlingConfigurer.accessDeniedHandler(
(request, response, accessDeniedException) ->
log.error(accessDeniedException.getMessage(), accessDeniedException)))
.build();
}
}
GraphqlSubscriptionInterceptor.java
@Slf4j
@Configuration
@RequiredArgsConstructor
public class GraphqlSubscriptionInterceptor implements WebSocketGraphQlInterceptor {
public static final String BEARER_TOKEN_PREFIX = "Bearer";
private static final String AUTHENTICATION_KEY = "Authorization";
private final @NonNull ConfigurableJWTProcessor<SecurityContext> jwtProcessor;
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
@Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
log.trace("Intercepted request -> {}", request);
return WebSocketGraphQlInterceptor.super.intercept(request, chain);
}
@Override
public Mono<Object> handleConnectionInitialization(
WebSocketSessionInfo sessionInfo, Map<String, Object> connectionInitPayload) {
log.debug("Initialized subscription - session-id: {}", sessionInfo.getId());
var token =
StringUtils.removeStart(
(String)
Optional.ofNullable(connectionInitPayload.get(AUTHENTICATION_KEY))
.orElseThrow(() -> new MissingTokenException("Missing JWT token !")),
BEARER_TOKEN_PREFIX);
try {
jwtProcessor.process(token, null);
} catch (ParseException | BadJOSEException | JOSEException e) {
throw new InvalidTokenException("Invalid JWT token !", e);
}
var jwtToken = JwtDecoders.fromIssuerLocation(issuer).decode(token);
var jwtAuthenticationToken = new JwtAuthenticationToken(jwtToken);
SecurityContextHolder.getContext().setAuthentication(jwtAuthenticationToken);
return WebSocketGraphQlInterceptor.super.handleConnectionInitialization(
sessionInfo, connectionInitPayload);
}
@Override
public @NonNull Mono<Void> handleCancelledSubscription(
WebSocketSessionInfo sessionInfo, String subscriptionId) {
log.debug("Cancelled subscription - session-id: {}", sessionInfo.getId());
return WebSocketGraphQlInterceptor.super.handleCancelledSubscription(
sessionInfo, subscriptionId);
}
@Override
public void handleConnectionClosed(
WebSocketSessionInfo sessionInfo, int statusCode, Map<String, Object> connectionInitPayload) {
log.debug(
"Closed subscription - session-id: {} - status-code: {}", sessionInfo.getId(), statusCode);
WebSocketGraphQlInterceptor.super.handleConnectionClosed(
sessionInfo, statusCode, connectionInitPayload);
}
}
JwtConfig.java
@Configuration
public class JwtConfig {
public static final String WELL_KNOWN_JWKS_JSON = "/.well-known/jwks.json";
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
@Value("${spring.security.oauth2.resourceserver.jwt.audiences}")
private String audiences;
@Bean
public ConfigurableJWTProcessor<SecurityContext> jwtProcessor() throws MalformedURLException {
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
JWKSource<SecurityContext> jwkSource =
new RemoteJWKSet<>(new URL(issuer + WELL_KNOWN_JWKS_JSON));
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256;
JWSKeySelector<SecurityContext> jwsKeySelector =
new JWSVerificationKeySelector<>(jwsAlgorithm, jwkSource);
jwtProcessor.setJWSKeySelector(jwsKeySelector);
jwtProcessor.setJWTClaimsSetVerifier(
new DefaultJWTClaimsVerifier<>(
new JWTClaimsSet.Builder().issuer(issuer).claim("client_id", audiences).build(),
Set.of(
JwtClaimNames.SUB,
JwtClaimNames.ISS,
JwtClaimNames.EXP,
JwtClaimNames.IAT,
"client_id",
"token_use",
"scope",
"auth_time")));
return jwtProcessor;
}
}
Additionally, if any would be interested in general how to implement GraphQL subscriptions using Spring for GraphQL I've made a post on Medium -> https://medium.com/@mielczarek.lukasz.karol/implementing-graphql-subscriptions-using-spring-for-graphql-and-redis-80f89a95c94c
Regards