-
Notifications
You must be signed in to change notification settings - Fork 47
client_oauth2
!-- Accessing to an OAuth2 GraphQL server -->
- Accessing to an OAuth2 GraphQL server
- JWT versus opaque token
- Note for Jersey users, and old users of the plugin (prior to 1.12)
- Sample
- The http error codes
- Some CURL command to check the OAuth configuration
- OAuth2 for releases v1.x
- OAuth2 for releases v2.x
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.
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.
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.
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.
- To start it, just execute the
- 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.
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.
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"
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>
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.
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.
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>
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
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();
}
}
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();
}
Creating a first app (non spring)
Connect to more than one GraphQL servers
Easily execute GraphQL requests with GraphQL Repositories
Access to an OAuth2 GraphQL server
How to personalize the client app
Howto personalize the generated code
Client migration from 1.x to 2.x
Implement an OAuth2 GraphQL server
Howto personalize the generated code
Server migration from 1.x to 2.x