Skip to content

client_oauth2

etienne-sf edited this page Jun 22, 2024 · 25 revisions

!-- Accessing to an OAuth2 GraphQL server -->

Accessing to an OAuth2 GraphQL server

Since the 1.12 release, the generated code for the client side allows to use Spring. This also allows to use various Spring capabilities, like Spring Security, and easy configuration with Spring Boot.

To access to a GraphQL server protected by OAuth2, you'll need to create a basic Spring app, and this page describes how to add the OAuth2 capability to this basic Spring app.

This page details how to use Spring Boot to configure Spring Security. This allows to:

  • Get an OAuth token from an Authorization server
  • Access to a GraphQL server that is protected by OAuth, that is, a GraphQL server that expect to receive a token that can be validated against the above Autorization server.

This page won't detail more the OAuth protocol. You'll find more information on the web. And a good idea is to start by OAuth presentation.

JWT versus opaque token

The OAuth protocol doesn't specify what is actually the OAuth token. There are two main options:

  • Opaque token, used in the 1.x version. The token contain no information. Only the OAuth server knows what data maps to this token.
  • JWT, used in the 2.x version. The token ecnrypts a set of data about the user that is behind this token.

Note for Jersey users, and old users of the plugin (prior to 1.12)

To connect to an OAuth server, you must use Spring capabilities: the Jersey implementation of this plugin can't do that.

If you're using the (now old) QueryExecutorImpl class, you won't be able to use this feature. Since the 1.12 release, the default QueryExecutor is the QueryExecutorSpringReactiveImpl which is based on Spring, with Spring reactive. Once the configuration below is done, this allows to access to OAuth protected urls for queries, mutations and subscriptions.

If you don't know if you're using QueryExecutorImpl class or not, that is, if we use the plugin runtime as is, then all this is transparent. By building with the graphql plugin in its 1.12 (or newer) version, then you'll use the QueryExecutorSpringReactiveImpl and all its capabilities. Including what's documented below.

Sample

This page is based on the graphql-maven-plugin-samples-allGraphQLCases-client sample. The link for this sample on the 1.x branch is graphql-maven-plugin-samples-allGraphQLCases-client.

This sample can be executed, when the servers below are started. They are available in the maven plugin project:

  • The OAuth2 Authorization server is in the graphql-maven-plugin-samples-OAuth-authorization-server module.
    • To start it, just execute the com.marcosbarbero.lab.sec.oauth.opaque.OAuth2OpaqueAuthorizationServerApp class, as a java application.
    • This server listen on the 8181 port. If you want to change it, you'll have to change its application.yml properties file, and the other application properties files accordingly.
  • The GraphQL server is in the graphql-maven-plugin-samples-allGraphQLCases-server module.
    • To start it, you'll have to build it, then execute the org.allGraphQLCases.server.util.GraphQLServerMain, generated by the graphql plugin in the target/generated-sources/graphql-maven-plugin folder.
    • This server listen on the 8180 port. If you want to change it, you'll have to change its application.yml properties file, and the other application properties files accordingly.

The http error codes

Here is an important notice about the HTTP status code that you may encounter here:

  • 401 means there is no OAuth authorization. There is an issue with the configuration, and probably no OAuth token is given to the resource server. Or the token is invalid.
  • 403 means that the resource server receives the OAuth authorization, and the token is valid. But for some reason, you're not allowed to access to this resource.

Some CURL command to check the OAuth configuration

As usual, it may happen (hum, ok, it always happens :) ) that some issue prevents your code to work as expected. So here are some useful commands, to check if the whole system is properly configured or not.

All these commands are based on the graphql-maven-plugin-samples-allGraphQLCases resource server, and the graphql-maven-plugin-samples-OAuth-authorization-server authorization server, as provided in the maven GraphQL plugin project.

These commands:

  • Are based on the curl command line tool.
  • The -i parameter allows to show the full response
  • The --noproxy "*" is necessary in my config, to avoid curl to send all these commands in the proxy, as the servers are local ones.
  • Contains tokens that you'll need to update, according to the one you'll get from the first request.

A sample query, to get an OAuth token:

curl -u "clientId:secret" -X POST "http://localhost:8181/oauth/token?grant_type=client_credentials" --noproxy "*" -i

Then, reuse the previous token in the next query:

curl -i -X POST "http://localhost:8180/graphql" --noproxy "*" -H "Authorization: Bearer 8c8e4a5b-d903-4ed6-9738-6f7f364b87ec"

And, to check the token:

curl -i -X GET "http://localhost:8181/profile/me" --noproxy "*" -H "Authorization: Bearer 8c8e4a5b-d903-4ed6-9738-6f7f364b87ec"

OAuth2 for releases v1.x

Enable the proper dependencies

Of course, in order to make all this work, you need some code in your classpath.

If you included com.graphql-java-generator:graphql-java-client-dependencies:pom, in your dependencies, everything should be ok.

If not, you need to include this dependency:

		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-oauth2-client</artifactId>
		</dependency>

Configure OAuth2 on client side, with Spring Boot

To configure OAuth2, there are tons of documentations on the web. But you should start from this sample. Below is the application.properties file of the graphql-maven-plugin-samples-allGraphQLCases-client module. This file must on the root of the resource folder. Here it is:

graphql.endpoint.url = http://localhost:8180/graphql
graphql.endpoint.subscriptionUrl = http://localhost:8180/graphql/subscription

# We don't need the Netty web server to start
spring.main.web-application-type = none



# Configuration for OAuth2, for our local OAuth authorization server
spring.security.oauth2.client.registration.provider_test.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.provider_test.client-id=clientId
spring.security.oauth2.client.registration.provider_test.client-secret=secret

spring.security.oauth2.client.provider.provider_test.token-uri=http://localhost:8181/oauth/token

The first part is the GraphQL configuration part.

The second part deals with OAuth:

  • This sample uses the client_credentials OAuth grant type.
  • We're using the provider_test OAuth provider. You find the provider's name in each spring configuration properties. It is given to spring when defining the serverOAuth2AuthorizedClientExchangeFilterFunction Spring bean (see below)

If you need some other grant types, check on the web, for more information on the Spring Boot configuration to do that. All the job is done in this configuration file. There should be no impact on the code, out of defining the `ServerOAuth2AuthorizedClientExchangeFilterFunction as explained below.

Enable OAuth2 on client side, with Spring Boot

We're starting with the basic Spring app.

Adding the OAuth2 capability is as simple as adding the ServerOAuth2AuthorizedClientExchangeFilterFunction, like in the sample below:

@SpringBootApplication(scanBasePackageClasses = { MinimalOAuthApp.class, GraphQLConfiguration.class, QueryExecutor.class })
public class MinimalOAuthApp implements CommandLineRunner {

	@Autowired
	QueryExecutor queryExecutor;

	public static void main(String[] args) {
		SpringApplication.run(MinimalOAuthApp.class, args);
	}

	/**
	 * This method is started by Spring, once the Spring context has been loaded. This is run, as this class implements
	 * {@link CommandLineRunner}
	 */
	@Override
	public void run(String... args) throws Exception {
		String query = "{appearsIn name }";
		System.out.println("Executing this query: '" + query + "'");
		System.out.println(queryExecutor.withoutParameters(query));
	}

	// If you're using the springBeanSuffix plugin parameter, this suffix MUST be added to
	// the serverOAuth2AuthorizedClientExchangeFilterFunction bean name
	@Bean
	ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction(
			ReactiveClientRegistrationRepository clientRegistrations) {
		ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
				clientRegistrations, new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
		oauth.setDefaultClientRegistrationId("provider_test");
		return oauth;
	}
}

What happened there?

Just some Spring magic... ! :)

The serverOAuth2AuthorizedClientExchangeFilterFunction (or serverOAuth2AuthorizedClientExchangeFilterFunctionYourSuffix if you're using the springBeanSuffix plugin parameter) is marked as Spring bean (same as the @Component on a class). So its auto loaded into the Spring's beans container. This tells Spring Boot to configure Spring Securities.

The serverOAuth2AuthorizedClientExchangeFilterFunction is responsible for each subsequence http request to:

  • Check that there is a valid OAuth2 token
    • If there is no token, a request to the OAuth authorization server is issued
    • If there is an expired token, a refresh request is issued
  • Add the relevant header into the http request

The important point, here, is to define the provider's name as it is named in the application.properties file. In this sample, its name is provider_test.

OAuth2 for releases v2.x

Enable the proper dependencies

The 2.x version is based on spring WebFlux. The only dependency needed for OAuth2 is:

		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-oauth2-client</artifactId>
		</dependency>

Configure OAuth2 on client side, with Spring Boot

To configure OAuth, you'll have to add these properties to the application.properties file:

# Configuration for OAuth2, for our local OAuth authorization server
spring.security.oauth2.client.registration.provider_test.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.provider_test.client-id=clientId
spring.security.oauth2.client.registration.provider_test.client-secret=secret

# Definition of the token provider url
spring.security.oauth2.client.provider.provider_test.token-uri=http://localhost:8181/oauth2/token

Enable OAuth2 on client side, with Spring Boot 2.x and Spring Framework 5.x

The graphql-maven-plugin-samples-allGraphQLCases-client on the 2.x contains the SpringConfig the defines the necessary beans for OAuth authorization. This sample uses the bean suffix, that allows to call more than one GraphQL servers.

The below version is a simplified version, when you need to call only one GraphQL server:

/**
 * 
 */
package org.allGraphQLCases.demo;

import java.net.URI;
import java.util.Collections;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.graphql.client.GraphQlClient;
import org.springframework.graphql.client.WebSocketGraphQlClient;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.client.web.server.UnAuthenticatedServerOAuth2AuthorizedClientRepository;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;

import com.graphql_java_generator.client.OAuthTokenExtractor;

import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;

@Configuration
public class SpringConfig {

	private static Logger logger = LoggerFactory.getLogger(SpringConfig.class);

	@Bean
	@Primary
	ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction(
			ReactiveClientRegistrationRepository clientRegistrations) {
		ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
				clientRegistrations, new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
		oauth.setDefaultClientRegistrationId("provider_test");
		return oauth;
	}

	@Bean
	@Primary
	public WebClient webClient(String graphqlEndpoint, //
			CodecCustomizer defaultCodecCustomizer, //
			@Autowired(required = false) HttpClient httpClient,
			@Autowired(required = false) ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction) {
		return WebClient.builder()//
				.baseUrl(graphqlEndpoint)//
				.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
				.defaultUriVariables(Collections.singletonMap("url", graphqlEndpoint))
				.filter(serverOAuth2AuthorizedClientExchangeFilterFunction)//
				.build();
	}
}

If you use Web Socket, which is mandatory for GraphQL subscriptions, than you must also add this bean definition:

	/**
	 * This overrides the default one provided by the plugin, to add OAuth2 capacity to Web Socket connection.<br/>
	 * This bean is USELESS if you don't use web socket transport, that is in general: if you don't use subscriptions.
	 * 
	 * @param graphqlEndpoint
	 *            The endpoint that is protected by OAuth
	 * @param serverOAuth2AuthorizedClientExchangeFilterFunction
	 *            the {@link ExchangeFilterFunction} that adds the OAuth capability to the http {@link WebClient} for
	 *            this endpoint. It used by the {@link OAuthTokenExtractor} to retrieve a OAuth bearer token, that will
	 *            be used for WebSocket connections.
	 * @return
	 */
	@Bean
	@Primary
	GraphQlClient webSocketGraphQlClient(String graphqlEndpoint,
			@Qualifier("serverOAuth2AuthorizedClientExchangeFilterFunction") ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction) {

		logger.debug("Creating SpringConfig webSocketGraphQlClient");

		// Creation of an OAuthTokenExtractor based on this OAuth ExchangeFilterFunction
		OAuthTokenExtractor oAuthTokenExtractor = new OAuthTokenExtractor(
				serverOAuth2AuthorizedClientExchangeFilterFunction);

		// The OAuth token must be checked at each execution. The spring WebSocketClient doesn't provide any way to
		// update the headers just before the request execution. So we override the WebSocketClient to add this
		// capability:
		ReactorNettyWebSocketClient client = new ReactorNettyWebSocketClient() {
			@Override
			public Mono<Void> execute(URI url, HttpHeaders requestHeaders, WebSocketHandler handler) {
				// Let's retrieve the valid OAuth token
				String authorizationHeaderValue = oAuthTokenExtractor.getAuthorizationHeaderValue();

				// Then we apply it to the given headers
				if (requestHeaders == null) {
					requestHeaders = new HttpHeaders();
				} else {
					requestHeaders.remove(OAuthTokenExtractor.AUTHORIZATION_HEADER_NAME);
				}
				logger.trace("Adding the bearer token to the Subscription websocket request");
				requestHeaders.add(OAuthTokenExtractor.AUTHORIZATION_HEADER_NAME, authorizationHeaderValue);

				// Then, let's execute the Web Socket request
				return super.execute(url, requestHeaders, handler);
			}
		};

		return WebSocketGraphQlClient.builder(graphqlEndpoint, client).build();
	}

}

Enable OAuth2 on client side, with Spring Boot 3.x / Spring Framework 6.x

package com.graphql_java_generator.samples.forum.test;

import java.net.URI;
import java.util.Collections;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Primary;
import org.springframework.graphql.client.GraphQlClient;
import org.springframework.graphql.client.WebSocketGraphQlClient;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;

import com.graphql_java_generator.client.GraphqlClientUtils;
import com.graphql_java_generator.client.OAuthTokenExtractor;
import com.graphql_java_generator.client.graphqlrepository.EnableGraphQLRepositories;
import com.graphql_java_generator.samples.forum.client.DirectQueriesWithFieldInputParameters;
import com.graphql_java_generator.samples.forum.client.graphql.PartialPreparedRequests;
import com.graphql_java_generator.samples.forum.client.graphql.forum.client.Query;

import reactor.core.publisher.Mono;

@TestConfiguration
@SpringBootApplication
@ComponentScan(basePackageClasses = { GraphqlClientUtils.class, Query.class, PartialPreparedRequests.class,
		DirectQueriesWithFieldInputParameters.class })
@EnableGraphQLRepositories({ "com.graphql_java_generator.samples.forum.client.graphql" })
public class SpringTestConfig {

	private static Logger logger = LoggerFactory.getLogger(SpringTestConfig.class);

	@Bean
	@Primary
	ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction(
			ReactiveClientRegistrationRepository clientRegistrations) {
		InMemoryReactiveOAuth2AuthorizedClientService clientService = new InMemoryReactiveOAuth2AuthorizedClientService(
				clientRegistrations);
		AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
				clientRegistrations, clientService);
		ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
				authorizedClientManager);
		oauth.setDefaultClientRegistrationId("provider_test"); // Defines our custom OAuth2 provider
		return oauth;
	}

	@Bean
	@Primary
	public WebClient webClient(//
			ReactiveClientRegistrationRepository clientRegistrations, //
			String graphqlEndpoint, //
			CodecCustomizer defaultCodecCustomizer, //
			@Autowired ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction) {

		return WebClient.builder()//
				.baseUrl(graphqlEndpoint)//
				.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
				.defaultUriVariables(Collections.singletonMap("url", graphqlEndpoint))//
				.filter(serverOAuth2AuthorizedClientExchangeFilterFunction)//
				.build();
	}
}

If you use Web Socket, which is mandatory for GraphQL subscriptions, than you must also add this bean definition:

	@Bean
	@Primary
	GraphQlClient webSocketGraphQlClient(String graphqlEndpoint,
			@Qualifier("serverOAuth2AuthorizedClientExchangeFilterFunction") ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction) {

		logger.debug("Creating SpringConfig webSocketGraphQlClient");

		// Creation of an OAuthTokenExtractor based on this OAuth ExchangeFilterFunction
		OAuthTokenExtractor oAuthTokenExtractor = new OAuthTokenExtractor(
				serverOAuth2AuthorizedClientExchangeFilterFunction);

		// The OAuth token must be checked at each execution. The spring WebSocketClient doesn't provide any way to
		// update the headers just before the request execution. So we override the WebSocketClient to add this
		// capability:
		ReactorNettyWebSocketClient client = new ReactorNettyWebSocketClient() {
			@Override
			public Mono<Void> execute(URI url, HttpHeaders requestHeaders, WebSocketHandler handler) {
				// Let's retrieve the valid OAuth token
				String authorizationHeaderValue = oAuthTokenExtractor.getAuthorizationHeaderValue();

				// Then we apply it to the given headers
				if (requestHeaders == null) {
					requestHeaders = new HttpHeaders();
				} else {
					requestHeaders.remove(OAuthTokenExtractor.AUTHORIZATION_HEADER_NAME);
				}
				logger.trace("Adding the bearer token to the Subscription websocket request");
				requestHeaders.add(OAuthTokenExtractor.AUTHORIZATION_HEADER_NAME, authorizationHeaderValue);

				// Then, let's execute the Web Socket request
				return super.execute(url, requestHeaders, handler);
			}
		};

		return WebSocketGraphQlClient.builder(graphqlEndpoint, client).build();
	}
Clone this wiki locally