Implementing an OAuth2 secured GraphQL server

Introduction

Thanks to Spring magic, it's quite easy to secure a GraphQL server with OAuth. It's just a matter of adding some Spring beans, and let the Spring Boot autoconfiguration work.

The only issue is to test your server, as graphiql won't be able to generate an OAuth token.

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.

Sample

This page is based on the graphql-maven-plugin-samples-allGraphQLCases-client sample.

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.
  • You can use the graphql-maven-plugin-samples-allGraphQLCases-client module, to test the access to the graphql-maven-plugin-samples-allGraphQLCases-server GraphQL server.

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.

This 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"

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-server-dependencies:pom, in your dependencies, everything should be ok. This includes this dependencies:

                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.springframework.security.oauth.boot</groupId>
                        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                        <version>2.4.0</version>
                </dependency>

There is an issue here: take care at the version for the spring-security-oauth2-autoconfigure dependency. As of 2.4.0, its version is not managed. That is: there is no default value for it. So you must give its version a value. The issue is that this version must the same as the version for the other versions of Spring Boot autoconfiguration.

So, when you update the graphql plugin version, take a look at the effective pom, and check the org.springframework.boot:spring-boot-autoconfigure version. In eclipse, once a pom file is open, you see the effective pom by clicking on Effective POM, in the lower part of the window.

You can also check the pom.xml file in github, but take care that this is the last version, and it may be "in advance", compared to the plugin version you're using.

Configure OAuth2 on server side, with Spring Boot

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

# Changing the port for the GraphQL server
server:
  port: 8180

# https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/html5/#oauth2-boot-resource-server-token-info
security:
  oauth2:
    client:
      client-id: clientId
      client-secret: secret
    resource:
      introspection-uri: http://localhost:8181/oauth/check_token

The first part is the GraphQL configuration part.

The second part deals with OAuth, and how to access the authorization server:

  • A basic authentication
  • The URL to check the opaque server

You'll find tons of docs on the web, on how to configure the Spring for other kind of tokens, including JWT.

Enable OAuth2 on server side

To enable OAuth on server side, it's just a matter of activating and configuring Spring Security. To do that, you add a class marked by the @Component annotation, in a subpackage where the code is generated. That is:

  • If you didn't define the packageName plugin parameter, nor the scanBasePackages: then this class must be in the com.generated.graphql package, or in one of its subpackages.
  • If you defined only the packageName plugin parameter: then this class must be in this package, or in one of its subpackages.
  • If you defined the scanBasePackages plugin parameter: then this class must be in one of these packages, or in one of their subpackages.

Don't forget the @Component annotation!

Here is a sample:

package org.allGraphQLCases.server.oauth2;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.resource.FixedAuthoritiesExtractor;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * 
 * @author etienne-sf
 * @see https://docs.spring.io/spring-security/site/docs/5.4.2/reference/html5/#servlet-authorization-filtersecurityinterceptor
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Value("${security.oauth2.resource.introspection-uri}")
        String introspectionUri;

        @Value("${security.oauth2.client.client-id}")
        String clientId;

        @Value("${security.oauth2.client.client-secret}")
        String clientSecret;

        FixedAuthoritiesExtractor s;

        @Override
        protected void configure(HttpSecurity http) throws Exception {
                http
                        // Disabling CORS and CSRF makes POST on the graphql URL work properly. Double-check that before entering in production
                                .cors().and().csrf().disable()
                                .authorizeRequests(authz -> authz
                                                .antMatchers(HttpMethod.GET, "/graphql/subscription").authenticated()// .access("hasRole('ROLE_CLIENT')")
                                                .antMatchers(HttpMethod.POST, "/graphql").authenticated()// .access("hasRole('ROLE_CLIENT')")
                                                .antMatchers(HttpMethod.GET, "/graphiql").permitAll()
                                                // All other URL accesses are prohibited
                                                .anyRequest().denyAll())
                                .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(
                                                // When all lines below are commented, then a custom OpaqueTokenIntrospector is used. See CustomAuthoritiesOpaqueTokenIntrospector
                                                token -> token
                                                                .introspectionUri(this.introspectionUri)
                                                                .introspectionClientCredentials(this.clientId, this.clientSecret)
                                )
                        );
        }

        public String getIntrospectionUri() {
                return introspectionUri;
        }

        public void setIntrospectionUri(String introspectionUri) {
                this.introspectionUri = introspectionUri;
        }

        public String getClientId() {
                return clientId;
        }

        public void setClientId(String clientId) {
                this.clientId = clientId;
        }

        public String getClientSecret() {
                return clientSecret;
        }

        public void setClientSecret(String clientSecret) {
                this.clientSecret = clientSecret;
        }
}

For more information, don't hesitate to browse the web: there are tons of information on how to manage this, with Spring.

And the GraphQL server is just a standard Spring App!