DEV Community

BHARGAB KALITA
BHARGAB KALITA

Posted on

How to implement Oauth in Go? - Part 1

Hi guys, this article will guide you through the process of implementing OAuth in Go, leveraging the power of Gin for routing, Redis for session management, and Goth for handling OAuth providers. Before you begin, make sure to install and set up a Redis server on your local machine by following this link.

We are going to use the below packages

  • Gin - github.com/gin-gonic/gin
  • Redis - github.com/redis/go-redis/v9
  • Goth - github.com/markbates/goth
  • Gorilla Session - github.com/gorilla/sessions

Setting Up Redis for Session Storage:

To store user sessions, we'll initialize a Redis client and define functions for setting and retrieving key-value pairs. Ensure to replace the placeholder values with your own Redis configuration.

import (
    "context"

    "github.com/redis/go-redis/v9"
)

var ctx = context.Background()

// Replace these values with your own configuration
const (
    redisAddr     = "localhost:6379"
    redisPassword = ""
)

var rdb *redis.Client

func InitRedis() {
    // Set up Redis session store
    rdb = redis.NewClient(&redis.Options{
        Addr:     redisAddr,
        Password: redisPassword, // no password set
        DB:       0,             // use default DB
    })
}

func GetRedisClient() *redis.Client {
    return rdb
}

func SetValue(key string, value string) {
    err := rdb.Set(ctx, key, value, 0).Err()
    if err != nil {
        panic(err)
    }
}

func GetValue(key string) (string, error) {
    val, err := rdb.Get(ctx, key).Result()
    if err != nil {
        return "", err
    }
    return val, nil
}
Enter fullscreen mode Exit fullscreen mode

Session Management Helper Functions:

Next, implement helper functions for managing user sessions. These functions create, retrieve, and delete sessions, ensuring a seamless authentication experience.

package helpers

import (
    "context"
    "encoding/json"
    "time"

    "auth-go/model"

    "github.com/google/uuid"
    "github.com/markbates/goth"
    "github.com/redis/go-redis/v9"
)

var ctx = context.Background()

func CreateSession(client *redis.Client, user *goth.User) (*model.Session, error) {
    sessionID := generateSessionID() // Generate a unique ID
    session := &model.Session{
        ID:           sessionID,
        UserID:       user.UserID,
        RefreshToken: user.RefreshToken,
        ExpiresAt:    time.Now().Add(time.Hour * 24), // Set expiration time
    }

    sessionData, err := json.Marshal(session)
    if err != nil {
        return nil, err
    }

    err = client.Set(ctx, sessionID, sessionData, session.ExpiresAt.Sub(time.Now())).Err()
    if err != nil {
        return nil, err
    }

    return session, nil
}

func GetSession(client *redis.Client, sessionID string) (*model.Session, error) {
    sessionData, err := client.Get(ctx, sessionID).Result()
    if err == redis.Nil {
        return nil, nil // Session not found
    } else if err != nil {
        return nil, err
    }

    var session model.Session
    err = json.Unmarshal([]byte(sessionData), &session)
    if err != nil {
        return nil, err
    }

    // Check if session is expired
    if session.ExpiresAt.Before(time.Now()) {
        return nil, nil // Session expired
    }

    return &session, nil
}

func DeleteSession(client *redis.Client, sessionID string) error {
    return client.Del(ctx, sessionID).Err()
}

// Helper functions
func generateSessionID() string {
    return uuid.New().String()
}

Enter fullscreen mode Exit fullscreen mode

Initializing Authentication Configuration:

Now, let's set up the authentication configuration using the Goth library and configure Google as the OAuth provider. The Goth package supports a variety of providers, allowing you to explore and integrate additional providers according to your needs. This flexibility is crucial for handling user authentication seamlessly.

Before proceeding, ensure you have created a project in the Google Console and obtained the GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET. For testing purposes, make sure to add test users to the "Test Users" section in the Google Console, as this step is essential for the proper functioning of the authentication.

Image description

package auth

import (
    "os"

    "github.com/markbates/goth"
    "github.com/markbates/goth/gothic"
    gothGoogle "github.com/markbates/goth/providers/google"
    "github.com/gorilla/sessions"
)

func InitAuth() {
    // Replace with your SESSION_SECRET or a secure secret key
    sessionSecretKey := "Secret-session-key"

    // Session configuration
    maxAge := 86400 * 30 // 30 days
    isProd := false      // Set to true when serving over HTTPS

    // Create a new CookieStore for session management
    store := sessions.NewCookieStore([]byte(sessionSecretKey))
    store.Options.Path = "/"
    store.Options.HttpOnly = true // HttpOnly should always be enabled
    store.Options.Secure = isProd
    store.MaxAge(maxAge)

    // Set the store for the Goth library
    gothic.Store = store

    // Configure Google OAuth provider
    googleClientID := os.Getenv("GOOGLE_CLIENT_ID")
    googleClientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")

    goth.UseProviders(
        gothGoogle.New(googleClientID, googleClientSecret, "http://localhost:3000/auth/google/callback", "email", "profile"),
    )

Enter fullscreen mode Exit fullscreen mode

}

Middleware Setup:

Two essential middlewares are introduced – CORS middleware for handling cross-origin requests and an authentication middleware to verify user sessions.

  • CORS Middleware
package middleware

import (
    "github.com/gin-gonic/gin"
)

func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173")
        c.Writer.Header().Set("Access-Control-Max-Age", "86400")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding, x-auth")
        c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length")
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(200)
        } else {
            c.Next()
        }
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Auth Middleware

In addition to the authentication middleware, consider enhancing your security by adding access-token (JWT) verification in the headers. It adds an extra layer of validation, ensuring the integrity and authenticity of the tokens.

It's essential to note a key concept of JWT – the ability to validate the token without contacting the issuer every time. By checking the ID and verifying the signature of the token with the known public key of the certificate Google used to sign the token, you can achieve efficient and secure validation. This approach eliminates the need to call Google APIs for validation, enhancing the performance of your authentication system.

For a detailed guide on consuming a Google ID token from a server, you can refer to this insightful article. The article provides valuable insights into the JWT validation process and can serve as a helpful resource in implementing secure token verification.

Ensure that you adapt the JWT validation process based on your specific requirements and security considerations.

package middleware

import (
    "net/http"
    "auth-go/model"
    "auth-go/redis"
    "auth-go/utils/ids"
    "auth-go/utils/messages"

    "github.com/gin-gonic/gin"
)

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        sessionId, err := c.Request.Cookie("session_id")
        if err != nil {
            c.AbortWithStatusJSON(http.StatusBadRequest, model.BaseResponse{Message: messages.INVALID_SESSION, Code: ids.INVALID_SESSION})
        }
        // Verify session data in Redis
        _, err = redis.GetValue(sessionId.Value)
        if err != nil {
            c.AbortWithStatusJSON(http.StatusBadRequest, model.BaseResponse{Message: messages.INVALID_SESSION, Code: ids.INVALID_SESSION})
        }
        // Session is valid, continue processing
        c.Next()
    }
}

Enter fullscreen mode Exit fullscreen mode

Routing Setup:

Now, let's define routes and initialize the necessary components in the main function.

func init() {
    // Load environment variables from .env file
    err := godotenv.Load()
    if err != nil {
        fmt.Println("Error loading .env file:", err)
        return
    }
    // initialize redis client
    redis.InitRedis()
}

func main() {

    router := gin.Default()

    auth.InitAuth()

    router.Use(middleware.CORSMiddleware())
    router.GET("/auth/:provider/callback", controllers.HandleAuthCallback)
    router.GET("/auth/:provider", controllers.HandleAuthProvider)
    router.GET("/logout/:provider", controllers.LogoutHandler)
    router.Use(middleware.AuthMiddleware())
    router.GET("/check", controllers.CheckHandler)

    fmt.Println("Listening on localhost:3000")
    if err := router.Run(":3000"); err != nil {
        fmt.Println("Error starting server:", err)
    }
}

Enter fullscreen mode Exit fullscreen mode

Authentication Controllers:

Implement controllers for handling authentication callbacks, provider initiation, and user logout.

package controllers

import (
    "context"

    "net/http"
    "auth-go/helpers"
    "auth-go/model"
    "auth-go/redis"
    "auth-go/utils/ids"
    "auth-go/utils/messages"

    "github.com/gin-gonic/gin"
    "github.com/markbates/goth/gothic"
)

func HandleAuthCallback(c *gin.Context) {
    res := c.Writer
    req := c.Request
    // Get the value of the "provider" path parameter
    provider := c.Param("provider")
    // Set the provider value in the request context
    ctx := context.WithValue(req.Context(), "provider", provider)
    req = req.WithContext(ctx)
    user, err := gothic.CompleteUserAuth(res, req)
    if err != nil {
        // c.String(http.StatusInternalServerError, fmt.Sprintf("Authentication error: %s", err))
        c.AbortWithStatusJSON(http.StatusInternalServerError, model.BaseResponse{Message: messages.AUTH_ERROR, Code: ids.AUTH_ERROR})
        return
    }
    session, err := helpers.CreateSession(redis.GetRedisClient(), &user)
    if err != nil {
        // Handle error
        c.AbortWithStatusJSON(http.StatusInternalServerError, model.BaseResponse{Message: messages.WENT_WRONG, Code: ids.WENT_WRONG})
    }
    // Set the session ID in the cookie
    c.SetCookie("session_id", session.ID, 60*60, "/", "localhost", true, false)
    // disable-caching
    c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
    // user.AccessToken
    c.Redirect(http.StatusFound, "http://localhost:5173")
}

func HandleAuthProvider(c *gin.Context) {
    res := c.Writer
    req := c.Request
    provider := c.Param("provider")
    // Set the provider value in the request context
    ctx := context.WithValue(req.Context(), "provider", provider)
    req = req.WithContext(ctx)
    gothic.BeginAuthHandler(res, req)
}

func LogoutHandler(c *gin.Context) {
    sessionId, err := c.Request.Cookie("session_id")
    if err != nil {
        c.AbortWithStatusJSON(http.StatusBadRequest, model.BaseResponse{Message: messages.INVALID_SESSION, Code: ids.INVALID_SESSION})
    }
    helpers.DeleteSession(redis.GetRedisClient(), sessionId.Value)
    c.JSON(http.StatusOK, model.BaseResponse{Message: messages.SUCCESS, Code: ids.SUCCESS})
}

Enter fullscreen mode Exit fullscreen mode

Check Endpoint Controller:

Finally, a controller is provided to check if the authentication is working after login.

package controllers

import (
    "net/http"

    "auth-go/model"
    "auth-go/utils/ids"
    "auth-go/utils/messages"

    "github.com/gin-gonic/gin"
)

func CheckHandler(c *gin.Context) {
    c.JSON(http.StatusOK, model.BaseResponse{Message: messages.SUCCESS, Code: ids.SUCCESS})
}

Enter fullscreen mode Exit fullscreen mode

GitHub Repository:

To access the complete codebase for this OAuth implementation in Go, including the backend and frontend components, please visit the dedicated GitHub repository:

GitHub Repository - OAuth in Go

Feel free to clone the repository, explore the code, and use it as a reference for your own projects. If you encounter any issues or have questions, don't hesitate to open an issue on the repository. Your feedback and contributions are highly appreciated.

Conclusion:

Congratulations on successfully implementing OAuth in Go using Gin, Redis, and Goth! We've covered the essential steps for authentication, set up middleware, and even explored the concept of JWT token verification for enhanced security.

If you have any questions, need clarification, or want to discuss any aspect of the implementation, feel free to reach out. Your feedback is valuable, and I'm here to assist you in any way possible.

In the next article, we'll take the next step and explore how to deploy your OAuth-enabled Go application. From setting up the production environment to ensuring smooth deployment, we'll cover the essential aspects of making your authentication system live.

Stay tuned for the next installment, and happy coding!

Top comments (0)