In this phase of our journey, we delve into the realm of middleware integration with gin and the implementation of authentication middleware using gocloak. Building upon the groundwork laid in previous sections, we now unify our efforts by integrating middleware seamlessly into our GraphQL server. With gin, a powerful HTTP web framework for Go, we enhance our server's capabilities by incorporating middleware functions to preprocess requests. Leveraging gocloak, a Go module for interfacing with Keycloak, we secure our server with authentication middleware. This pivotal stage marks the convergence of all preceding elements, culminating in the creation of the server run function, which orchestrates the execution of our GraphQL API server. Let's explore how these components harmonize to elevate our server's functionality and security.
Implementing Authentication Middleware with Keycloak and Gocloak
The final middleware we need to add to our server is authentication. In this example, we'll use Keycloak as our identity provider. To interface with Keycloak in Go, we'll use the gocloak module. By leveraging gocloak, we can perform authentication against Keycloak using Gin middleware.
To create the middleware, we begin by specifying the header that we want to inspect from the request. Keycloak leverages the OpenID Connect protocol, so we expect the Authorization
header to begin with the word "Bearer," followed by a space, and then the full token string. Below, we define the constant "Bearer "
for this purpose:
const headerPrefix = "Bearer "
Next, we need to verify the token. To achieve this, we create a function that accepts a Gorm database pointer (*gorm.DB
) and an HTTP request pointer (*http.Request
). This function will extract the Authorization header from the request, validate the token using Keycloak, and return a user if a match is found in the database. If the calls do not complete successfully, the function will return an error.
func ValidateToken(db *gorm.DB, req *http.Request) (*model.User, error) {
authToken := req.Header.Get("Authorization")
authToken = strings.TrimPrefix(authToken, headerPrefix) // Strip the "Auth " from the bearer token
keycloak := config.Config.Auth
// Make call to keycloak authenticating the token
client := gocloak.NewClient(keycloak.Endpoint)
// Add certificate verification if a certificate path is set
if len(keycloak.CertificatePath) > 0 {
log.Infof("Reading certificate from %s...", keycloak.CertificatePath)
cert, err := os.ReadFile(keycloak.CertificatePath)
if err != nil {
log.Errorf("[identity.cert] Unable to read certificate => %v", err)
return nil, err
}
certPool := x509.NewCertPool()
if ok := certPool.AppendCertsFromPEM(cert); !ok {
log.Errorf("[identity.cert] Unable to add cert to pool => %v", err)
return nil, err
}
restyClient := client.RestyClient()
restyClient.SetTLSClientConfig(&tls.Config{RootCAs: certPool})
log.Info("Imported certificate to keycloak client")
}
res, err := client.RetrospectToken(req.Context(), authToken, keycloak.ClientID, keycloak.ClientSecret, keycloak.RealmName)
if err != nil {
log.Errorf("unable to validate access token => %v", err)
return nil, err
}
log.Debugf("[auth] Access Token => %v", *res)
if !*res.Active {
err = errors.New("session is not active")
log.Errorf("session is not active => %v", err)
return nil, err
}
// fetch userinfo and query the database for the user
info, err := client.GetUserInfo(req.Context(), authToken, keycloak.RealmName)
if err != nil {
log.Errorf("unable to fetch user info => %v", err)
return nil, err
}
// add the user to the database if there is no current entry for the user
var user model.User
if err = db.FirstOrCreate(&user, model.User{
Username: *info.PreferredUsername,
Name: fmt.Sprintf("%s %s", *info.GivenName, *info.FamilyName),
}).Error; err != nil {
log.Errorf("unable to save user to database => %v", err)
return nil, err
}
log.Debug(user)
return &user, nil
}
The AuthenticationMiddleware
function is designed to integrate authentication into a Gin web server. This function takes a Gorm database pointer (*gorm.DB
) as an argument and returns a Gin handler function. Inside the handler, the middleware calls the ValidateToken
function, passing it the database pointer and the current HTTP request. If the token validation fails, an error is logged, and the request is aborted with an HTTP status of 403 (Forbidden). If the token is successfully validated, the user information is added to the request context, allowing downstream handlers to access it. Finally, the middleware calls c.Next()
to pass control to the next handler in the chain.
func AuthenticationMiddleware(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
user, err := ValidateToken(db, c.Request)
if err != nil {
log.Errorf("unable to authenticate token => %v", err)
err = c.AbortWithError(http.StatusForbidden, err)
log.Debug(err)
return
}
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), userKey, user))
c.Next()
}
}
Finally, the ForUser
function is designed to retrieve the authenticated user from the request context within a Gin web server. This function takes a context (ctx
) as an argument and attempts to extract the user information stored in the context using a predefined key (userKey
). It utilizes the ctx.Value
method to access the value associated with userKey
and performs a type assertion to convert it to a *model.User
. If the user information is not found or the type assertion fails, the function returns nil
. This utility function allows other parts of the application to conveniently access the authenticated user from the context, facilitating user-specific operations and data handling.
func ForUser(ctx context.Context) *model.User {
user, _ := ctx.Value(userKey).(*model.User)
return user
}
Finalizing Server Setup: The Run Function
With all the middleware components established throughout the series, it's time to bring everything together and finalize the server setup. The Run
function acts as the glue, orchestrating the integration of various middleware components and starting the Gin server. This function typically initializes the Gin router, applies the middleware layers in the desired order, and defines the routes or endpoints for handling incoming requests. It encapsulates the server configuration and provides a unified entry point for launching the web server. By consolidating the middleware setup and server initialization logic into a single function, we ensure consistency, maintainability, and ease of management for the entire server application.
The Run
function sets up the server configuration, defines routes, applies middleware, and starts the server. It initializes the server with Gin's default middleware, sets up endpoints for actuator, GraphQL playground, and GraphQL itself. The middleware stack includes services, data loader middleware, and authentication middleware to handle various aspects of request processing and security. Finally, it starts the server to listen on the configured host and port, logging the endpoint for reference.
func Run(db *gorm.DB) {
config := config.Config
endpoint := fmt.Sprintf("%s:%d", config.Service.Host, config.Service.Port)
r := gin.Default()
r.GET("/actuator/*endpoint", handlers.ActuatorHandler(db))
r.Use(middleware.Services(db, index.IndexConnection))
r.Use(middleware.DataloaderMiddleware())
r.GET(config.Service.PlaygroundPath, handlers.PlaygroundHandler())
r.Use(gin.Recovery())
secured := r.Group(config.Service.Path)
secured.Use(middleware.AuthenticationMiddleware(db))
secured.POST("/", handlers.GraphqlHandler())
log.Infof("Running @ http://%s", endpoint)
log.Fatal(r.Run(endpoint))
}
The Run
function serves as the centerpiece of our server logic, orchestrating the integration of GraphQL with Gin in Go. Encapsulated within the pkg/server
package, it represents the culmination of our efforts across various modules and middleware layers. At the bottom of our application entrypoint, housed in cmd/main.go
, we invoke server.Run
to kickstart the server and bring our GraphQL-powered application to life.
Finishing Touches: Revisiting GraphQL
This GraphqlHandler
function serves as the entry point for GraphQL requests in our server. It initializes a config
struct with the resolver functions provided by our graph
package. Additionally, it configures directives, such as validation, to be used during query execution. Finally, it creates a handler using handler.NewDefaultServer
, passing in the executable schema generated by gqlgen based on our schema and resolvers. This handler is then returned as a Gin middleware function, allowing it to process GraphQL requests coming to our server.
func GraphqlHandler() gin.HandlerFunc {
config := generated.Config{Resolvers: &graph.Resolver{}}
// Add directives
config.Directives.Validate = directives.Validate
h := handler.NewDefaultServer(generated.NewExecutableSchema(config))
return func(c *gin.Context) { h.ServeHTTP(c.Writer, c.Request) }
}
As we put the finishing touches on our GraphQL server, let's revisit one of our resolvers to demonstrate how we can seamlessly integrate middleware into our GraphQL operations. Middleware plays a crucial role in intercepting and augmenting requests before they reach our resolvers, allowing us to perform additional tasks such as authentication, logging, or data manipulation. By integrating middleware into our resolver functions, we can enhance the functionality and security of our GraphQL API without cluttering our resolver logic. Let's dive into the details of how middleware can be seamlessly incorporated into our GraphQL server architecture.
In this Go code snippet, we revisit the User resolver previously implemented in our GraphQL server. This resolver function, named Pantries, is responsible for fetching a list of pantries associated with a particular user. Within the function, we access the services layer through the context, leveraging a middleware function to retrieve the necessary service. Once obtained, we call the FetchPantriesByAuthor method from the PantryService to retrieve the pantries associated with the user. The function accepts optional parameters such as order, pagination details (startAt and size), and returns a PantryList along with any potential errors encountered during the process. This resolver exemplifies how middleware can seamlessly integrate with resolver functions to enhance the functionality of our GraphQL server.
func (r *userResolver) Pantries(ctx context.Context, obj *model.User, order *model.SearchOrder, startAt *int, size *int) (*model.PantryList, error) {
services := middleware.ForServices(ctx)
return services.PantryService.FetchPantriesByAuthor(order, &obj.ID, startAt, size)
}
Conclusion: Bringing it All Together
In conclusion, this article series has provided a comprehensive guide to building a robust GraphQL API server in Go. We began by setting up gqlgen for GraphQL integration, customized it to fit Go project conventions, and defined our GraphQL schema with resolvers. We abstracted our data model using services, integrated them using middleware, and implemented schema-level validation. Additionally, we optimized data retrieval with dataloaders, ensuring efficient query execution. Finally, we tied everything together with authentication middleware and a run function encapsulated in the server package. By following these steps, we've laid a solid foundation for creating powerful GraphQL APIs in Go, ready to handle various use cases and scale with ease.
References
- Keycloak golang webservices - https://mikebolshakov.medium.com/keycloak-with-go-web-services-why-not-f806c0bc820a
- Opinionated graphql server with go - https://dev.to/cmelgarejo/creating-an-opinionated-graphql-server-with-go-part-1-3g3l
Top comments (0)