DEV Community

mikeyGlitz
mikeyGlitz

Posted on

Bringing It All Together: Integrating GraphQL with Gin in Go

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 "
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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) }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)