Implementing the skipAuth Directive in GraphQL with Golang
June 1, 2023
Introduction
GraphQL is a powerful query language for APIs that provides flexibility and efficiency in fetching data. One of its notable features is the ability to define custom directives that can modify the execution of queries. In this article, we will explore how to implement the @skipAuth directive in GraphQL using the Golang, specifically leveraging the gqlparser and gqlgen libraries.
I ran into a GraphQL @skipAuth issue at work and realized that there were limited resources available on how to handle this scenario effectively, especially when needing to execute the directive logic before the middleware. To address this, I opened up an issue on the gqlgen GitHub repository, which you can find here. In this article, I will share the approach I discovered to implement the @skipAuth directive in GraphQL using Golang.
Overview:
The @skipAuth directive allows you to conditionally skip the authentication logic for certain fields or operations in a GraphQL schema. This can be useful in scenarios where you want to provide public access to specific data without requiring authentication.
Prerequisites:
Before we dive into the implementation, make sure you have the following components set up:
- Golang environment installed on your machine.
- Basic understanding of GraphQL concepts.
- Familiarity with the gqlparser and gqlgen libraries in Golang.
Implementation Steps:
- Define the @skipAuth Directive:
Start by defining the @skipAuth directive in your GraphQL schema. This directive will accept a single argument of type Boolean to control the conditional skipping of authentication logic. Here's an example schema definition:
directive @skipAuth on FIELD_DEFINITION
- Add the Directive to the Schema:
Next, add the @skipAuth directive to the relevant fields or operations in your schema where you want to conditionally skip authentication. For instance, consider the following example:
type Query {
publicData: [String!]! @skipAuth
...
}
- Generate Code with gqlgen:
Run the code generation command for gqlgen to generate the necessary Go code based on your schema and resolvers. Execute the following command in your terminal:
go run github.com/99designs/gqlgen generate
This command will generate the boilerplate code, resolver interfaces, and types needed to implement the GraphQL API.
- Implement the Resolver Middleware:
To handle the @skipAuth directive in the resolver middleware, follow these steps:
- Retrieve the parsed query: Use
gqlparser.LoadQuery()
to load and parse the GraphQL query received in the HTTP request. - Traverse the parsed query: Iterate over the parsed query to find fields with the @skipAuth directive.
- Handle the directive: Implement the necessary logic to skip the authentication if the directive is present and the argument evaluates to true.
Here’s an example code snippet:
func skipAuth(graphqlExecutableSchema graphql.ExecutableSchema, r *http.Request) (bool, error) {
var skipAuth bool
var req struct {
Query string `json:"query"`
}
buf, err := ioutil.ReadAll(r.Body)
if err != nil {
return skipAuth, err
}
body := ioutil.NopCloser(bytes.NewBuffer(buf))
copyBody := ioutil.NopCloser(bytes.NewBuffer(buf))
if err := json.NewDecoder(body).Decode(&req); err != nil {
return skipAuth, err
}
r.Body = copyBody
schema := graphqlExecutableSchema.Schema()
parsedQuery, gerr := gqlparser.LoadQuery(schema, req.Query)
if gerr != nil {
return skipAuth, gerr
}
// Retrieve the directive argument value
loop:
for _, op := range parsedQuery.Operations {
for _, sel := range op.SelectionSet {
if field, ok := sel.(*ast.Field); ok {
objDefinition := field.ObjectDefinition
if objDefinition == nil {
skipAuth = false
break loop
}
for _, f := range objDefinition.Fields {
schemaQuery := schema.Query
if schemaQuery != nil {
if info := schema.Query.Fields.ForName(f.Name); info != nil {
if skipAuthDirective := info.Directives.ForName("skipAuth"); skipAuthDirective != nil {
skipAuth = true
break loop
}
}
}
schemaMutation := schema.Mutation
if schemaMutation != nil {
if info := schema.Mutation.Fields.ForName(f.Name); info != nil {
if skipAuthDirective := info.Directives.ForName("skipAuth"); skipAuthDirective != nil {
skipAuth = true
break loop
}
}
}
}
}
}
}
return skipAuth, nil
}
func Middleware(next http.Handler, graphqlExecutableSchema graphql.ExecutableSchema) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ok, _ := skipAuth(graphqlExecutableSchema, r); ok {
next.ServeHTTP(w, r)
return
}
// other programs goes here
})
}
In this example, we read the GraphQL query from the request, parse it using gqlparser.LoadQuery()
, and then traverse the parsed query to find fields with the @skipAuth directive. Inside the directive handling logic, you can implement the necessary authentication skipping logic based on the directive's argument value.
- Apply the Middleware to gqlgen:
Finally, apply the middleware to gqlgen to incorporate the @skipAuth directive handling in the GraphQL API. Here's an example of how to apply the middleware:
generatedGQL := generated.Config{
Resolvers: &graph.Resolver{},
}
generatedGQL.Directives.SkipAuth = func(ctx context.Context, obj interface{}, next graphql.Resolver) (interface{}, error) {
return next(ctx)
}
graphqlExecutableSchema := generated.NewExecutableSchema(generatedGQL)
srv := handler.NewDefaultServer(graphqlExecutableSchema)
mux := http.NewServeMux()
mux.Handle("/", playground.Handler("GraphQL playground", "/query"))
mux.Handle("/query", Middleware(srv, graphqlExecutableSchema))
In this example, the Middleware function wraps the GraphQL handler to inject the directive handling logic.
Here’s the full executable code that includes the implementation of the @skipAuth directive using gqlparser and gqlgen in Golang:
package main
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/gandalf-gate/backend/graph"
"github.com/gandalf-gate/backend/graph/generated"
gqlparser "github.com/vektah/gqlparser/v2"
"github.com/vektah/gqlparser/v2/ast"
)
const (
ContentTypeMultipartFormData = "multipart/form-data"
ContentTypeApplicationJson = "application/json"
)
func skipAuth(graphqlExecutableSchema graphql.ExecutableSchema, r *http.Request) (bool, error) {
var skipAuth bool
var req struct {
Query string `json:"query"`
}
contentType := r.Header.Get("Content-Type")
buf, err := ioutil.ReadAll(r.Body)
if err != nil {
return skipAuth, err
}
body := ioutil.NopCloser(bytes.NewBuffer(buf))
copyBody := ioutil.NopCloser(bytes.NewBuffer(buf))
if strings.Contains(contentType, ContentTypeApplicationJson) {
if err := json.NewDecoder(body).Decode(&req); err != nil {
return skipAuth, err
}
} else if strings.Contains(contentType, ContentTypeMultipartFormData) {
// Get the query from form-data
req.Query = r.FormValue("query")
}
r.Body = copyBody
schema := graphqlExecutableSchema.Schema()
parsedQuery, gerr := gqlparser.LoadQuery(schema, req.Query)
if gerr != nil {
fmt.Println(gerr)
return skipAuth, gerr
}
// Retrieve the directive argument value
loop:
for _, op := range parsedQuery.Operations {
for _, sel := range op.SelectionSet {
if field, ok := sel.(*ast.Field); ok {
objDefinition := field.ObjectDefinition
if objDefinition == nil {
skipAuth = false
break loop
}
for _, f := range objDefinition.Fields {
schemaQuery := schema.Query
if schemaQuery != nil {
if info := schema.Query.Fields.ForName(f.Name); info != nil {
if skipAuthDirective := info.Directives.ForName("skipAuth"); skipAuthDirective != nil {
skipAuth = true
break loop
}
}
}
schemaMutation := schema.Mutation
if schemaMutation != nil {
if info := schema.Mutation.Fields.ForName(f.Name); info != nil
{
if skipAuthDirective := info.Directives.ForName("skipAuth"); skipAuthDirective != nil {
skipAuth = true
break loop
}
}
}
}
}
}
}
return skipAuth, nil
}
func Middleware(next http.Handler, graphqlExecutableSchema graphql.ExecutableSchema) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ok, _ := skipAuth(graphqlExecutableSchema, r); ok {
next.ServeHTTP(w, r)
return
}
// other programs goes here
})
}
func main() {
generatedGQL := generated.Config{
Resolvers: &graph.Resolver{},
}
generatedGQL.Directives.SkipAuth = func(ctx context.Context, obj interface{}, next graphql.Resolver) (interface{}, error) {
return next(ctx)
}
graphqlExecutableSchema := generated.NewExecutableSchema(generatedGQL)
srv := handler.NewDefaultServer(graphqlExecutableSchema)
mux := http.NewServeMux()
mux.Handle("/", playground.Handler("GraphQL playground", "/query"))
mux.Handle("/query", Middleware(srv, graphqlExecutableSchema))
}
Conclusion:
In this article, we explored how to implement the @skipAuth directive in GraphQL using Golang and the gqlparser and gqlgen libraries. By following these steps, you can conditionally skip the authentication logic for specific fields or operations in your GraphQL API, providing public access to certain data without requiring authentication.
Feel free to customize the directive’s behavior to suit your authentication requirements and build secure and efficient GraphQL APIs with Golang!
For more information, check out: https://gqlgen.com/reference/directives/