
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
@authdirectives 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
ADMINrole. - 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
Authorizationheader. - 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.
