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
}
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()
}
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.
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"),
)
}
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()
}
}
}
- 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()
}
}
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)
}
}
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})
}
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})
}
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.
Stay tuned for the next installment, and happy coding!
Top comments (0)