Securing GraphQL in Golang using Directives for Authentication & Authorization

When building an API, securing your data is just as important as exposing it. This post walks you through how to implement authentication and role-based access control in GraphQL using Golang, with a powerful feature called GraphQL Directives.

We’ll learn how to:

  • Authenticate users using JWT
  • Restrict query access using roles
  • Use @auth directives to handle access control directly in the schema

📘 What are GraphQL Directives?

GraphQL directives are annotations that can be added to your schema to change the behavior of fields or queries at runtime.

For more details : https://gqlgen.com/reference/directives/

You can use built-in directives (like @include) or define your own.

For example:

type Query {
getUserData: User @auth(roles: ["ADMIN", "USER"])
}

Here, we’ve added a custom @auth directive that restricts access to users who have either an ADMIN or USER role.

💪 Let’s Implement It in Golang

We’ll use gqlgen, a powerful GraphQL server generator for Go.

✍️ Step 1: Define the @auth Directive in Your Schema :

In your schema.graphql file, declare the directive at the top:

directive @auth(roles: [String!]) on FIELD_DEFINITION

📝 This tells gqlgen:

“We want to attach @auth to any field in the schema, and it will accept an optional list of roles.”

🔒 Step 2: Use the @auth Directive in Queries

Apply the directive to fields you want to protect:

type Query {
listUsers: [User]! @auth(roles: ["ADMIN"])
currentUserProfile: User @auth
}
  • The first query allows access only if the user has the ADMIN role.
  • The second query allows any authenticated user (even without role restrictions).

This makes access control declarative and readable at the schema level.

⚙️ Step 3: Generate Code Using gqlgen :

Run the following command:

go run github.com/99designs/gqlgen generate

This command regenerates Go code based on your schema — including function placeholders for your directives. You’ll find a new method signature like:

Auth func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []string) (res interface{}, err error)

🧱 Step 4: Middleware for JWT Authentication :

Before we handle the directive logic, we need to validate JWTs and extract user info.

Here’s the middleware:

func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
authHeader := c.Request().Header.Get("Authorization")
tokenString := strings.TrimPrefix(authHeader, "Bearer ")

if tokenString == "" {
return next(c) // let unauthenticated access through if the route doesn't require it
}

// Validate JWT token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte("your_secret"), nil
})
if err != nil || !token.Valid {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid token")
}

claims := token.Claims.(jwt.MapClaims)
user := UserDetails{
Email: claims["email"].(string),
Role: claims["role"].(string),
}

// Store user info in request context
ctx := context.WithValue(c.Request().Context(), userContextKey, user)
c.SetRequest(c.Request().WithContext(ctx))

return next(c)
}
}

🧠 What this does:

  • Reads the JWT from the Authorization header.
  • Validates it.
  • Extracts claims like email and role.
  • Saves them into the request context — so GraphQL resolvers and directives can access them later.

📊 Step 5: Store & Retrieve User in Context :

Create a struct and helper function:

type UserDetails struct {
Email string
Role string
}

var userContextKey = &contextKey{"user"}

func GetUserFromContext(ctx context.Context) (UserDetails, bool) {
user, ok := ctx.Value(userContextKey).(UserDetails)
return user, ok
}

This makes it easy to retrieve the authenticated user in our GraphQL logic.

🧠 Step 6: Implement the @auth Directive Function :

In your directives.go file:

func AuthDirective(ctx context.Context, obj interface{}, next graphql.Resolver, roles []string) (interface{}, error) {
user, ok := GetUserFromContext(ctx)
if !ok {
return nil, fmt.Errorf("unauthenticated")
}

// If no specific roles required, just check if user is authenticated
if len(roles) == 0 {
return next(ctx)
}

// Check if user's role matches one of the allowed roles
for _, role := range roles {
if user.Role == role {
return next(ctx)
}
}

return nil, fmt.Errorf("unauthorized: requires roles %v", roles)
}

🎯 What’s happening here:

  • Checks if the user exists in the context (i.e., is authenticated).
  • If no roles are specified in @auth, any authenticated user is allowed.
  • If roles are specified, the user must match one of them, or access is denied.

🔗 Step 7: Register the Directive :

Register the AuthDirective function during GraphQL schema configuration:

func GetGraphQLConfig(resolver *graph.Resolver) generated.Config {
return generated.Config{
Resolvers: resolver,
Directives: generated.DirectiveRoot{
Auth: AuthDirective,
},
}
}

Update gqlgen.yml file with below code :

directives:
auth:
implementation: github.com/your/module/path/yourpackage.AuthDirective

✅ This tells gqlgen to use your custom function and pass the roles argument correctly.

🚀 Final Thoughts

Using GraphQL directives for authentication & authorization provides:

  • Clarity: Access control is visible directly in your schema
  • Reusability: One directive implementation secures many fields
  • Separation of Concerns: Business logic and access rules stay cleanly separated

If you’re building a secure GraphQL API in Go, this pattern scales beautifully.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.