Skip to content

Commit 3c73a25

Browse files
committed
feat: Implement Google authentication with session management and user handling
1 parent e102c0a commit 3c73a25

6 files changed

Lines changed: 186 additions & 14 deletions

File tree

cmd/api/main.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,7 @@ func gracefulShutdown(apiServer *http.Server, done chan bool) {
3939

4040
func main() {
4141

42-
server := server.NewServer()
43-
44-
// store := sessions.NewCookieStore([]byte(appHandler.Config.SessionSecret))
45-
// // Configure session options
46-
// store.MaxAge(int(12 * time.Hour / time.Second)) // session expiration time
47-
// store.Options.Path = "/"
48-
// store.Options.HttpOnly = true
49-
// store.Options.Secure = false // set to true in production (HTTPS)
50-
// store.Options.SameSite = http.SameSiteLaxMode // helps prevent CSRF
42+
server := server.New()
5143

5244
// Create a done channel to signal when the shutdown is complete
5345
done := make(chan bool, 1)

go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ require (
1111
go.mongodb.org/mongo-driver v1.17.6
1212
)
1313

14+
require (
15+
cloud.google.com/go/compute/metadata v0.7.0 // indirect
16+
github.com/go-chi/chi/v5 v5.2.2 // indirect
17+
github.com/gorilla/context v1.1.1 // indirect
18+
github.com/gorilla/mux v1.6.2 // indirect
19+
github.com/gorilla/securecookie v1.1.1 // indirect
20+
github.com/gorilla/sessions v1.1.1 // indirect
21+
golang.org/x/oauth2 v0.33.0 // indirect
22+
)
23+
1424
require (
1525
dario.cat/mergo v1.0.2 // indirect
1626
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
@@ -52,6 +62,7 @@ require (
5262
github.com/leodido/go-urn v1.4.0 // indirect
5363
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
5464
github.com/magiconair/properties v1.8.10 // indirect
65+
github.com/markbates/goth v1.82.0
5566
github.com/mattn/go-isatty v0.0.20 // indirect
5667
github.com/moby/docker-image-spec v1.3.1 // indirect
5768
github.com/moby/go-archive v0.1.0 // indirect

go.sum

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
2+
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
13
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
24
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
35
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
@@ -53,6 +55,8 @@ github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmn
5355
github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM=
5456
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
5557
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
58+
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
59+
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
5660
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
5761
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
5862
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -80,6 +84,14 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
8084
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
8185
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
8286
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
87+
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
88+
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
89+
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
90+
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
91+
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
92+
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
93+
github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE=
94+
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
8395
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
8496
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
8597
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@@ -100,6 +112,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
100112
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
101113
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
102114
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
115+
github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ=
116+
github.com/markbates/goth v1.82.0/go.mod h1:/DRlcq0pyqkKToyZjsL2KgiA1zbF1HIjE7u2uC79rUk=
103117
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
104118
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
105119
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@@ -221,6 +235,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
221235
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
222236
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
223237
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
238+
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
239+
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
240+
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
241+
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
224242
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
225243
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
226244
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=

internal/auth/google.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package auth
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/gin-gonic/gin"
7+
"github.com/markbates/goth/gothic"
8+
)
9+
10+
type GoogleHandler struct {
11+
sessionSecret string
12+
}
13+
14+
func NewGoogleHandler(sessionSecret string) *GoogleHandler {
15+
return &GoogleHandler{sessionSecret: sessionSecret}
16+
}
17+
18+
func (h *GoogleHandler) Signup(ctx *gin.Context) {
19+
query := ctx.Request.URL.Query()
20+
query.Add("provider", "google")
21+
ctx.Request.URL.RawQuery = query.Encode()
22+
23+
gothic.BeginAuthHandler(ctx.Writer, ctx.Request)
24+
}
25+
26+
func (h *GoogleHandler) HandleCallback(ctx *gin.Context) {
27+
query := ctx.Request.URL.Query()
28+
query.Add("provider", "google")
29+
ctx.Request.URL.RawQuery = query.Encode()
30+
31+
user, err := gothic.CompleteUserAuth(ctx.Writer, ctx.Request)
32+
if err != nil {
33+
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
34+
"success": false,
35+
"message": "Error authenticating user",
36+
"error": err.Error(),
37+
})
38+
return
39+
}
40+
41+
// store the user session
42+
session, err := gothic.Store.New(ctx.Request, h.sessionSecret)
43+
if err != nil {
44+
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
45+
"success": false,
46+
"message": "Error stroing user session",
47+
"error": err.Error(),
48+
})
49+
return
50+
}
51+
52+
// your logic for storing the user in database goes here
53+
// TODO
54+
55+
session.Values["user"] = user
56+
57+
// save the user session
58+
if err = session.Save(ctx.Request, ctx.Writer); err != nil {
59+
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
60+
"success": false,
61+
"message": "Error saving user session",
62+
"error": err.Error(),
63+
})
64+
return
65+
}
66+
67+
ctx.Redirect(http.StatusTemporaryRedirect, "/dashboard")
68+
}
69+
70+
func (h *GoogleHandler) GetAuthUser(ctx *gin.Context) {
71+
// Retrieve the session
72+
session, err := gothic.Store.Get(ctx.Request, h.sessionSecret)
73+
if err != nil {
74+
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
75+
"success": false,
76+
"message": "Error retrieving user session",
77+
"error": err.Error(),
78+
})
79+
return
80+
}
81+
82+
// Get user data from session
83+
user := session.Values["user"]
84+
if user == nil {
85+
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
86+
"success": false,
87+
"message": "No authenticated user found",
88+
})
89+
return
90+
}
91+
92+
// Return user info
93+
ctx.JSON(http.StatusOK, gin.H{
94+
"success": true,
95+
"message": "User fetched successfully",
96+
"data": user,
97+
})
98+
}
99+
100+
func (h *GoogleHandler) HandleUnauthorized(ctx *gin.Context) {
101+
ctx.JSON(http.StatusUnauthorized, gin.H{
102+
"success": false,
103+
"message": "Unauthorized access",
104+
})
105+
106+
// redirect to /unauthorized
107+
ctx.Redirect(http.StatusTemporaryRedirect, "/unauthorized")
108+
}

internal/server/routes.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@ package server
22

33
import (
44
"net/http"
5+
"os"
6+
"time"
7+
"trakrlog/internal/auth"
58

69
"github.com/gin-contrib/cors"
710
"github.com/gin-contrib/static"
811
"github.com/gin-gonic/gin"
12+
"github.com/gorilla/sessions"
13+
"github.com/markbates/goth"
14+
"github.com/markbates/goth/gothic"
15+
"github.com/markbates/goth/providers/google"
916
)
1017

1118
func (s *Server) RegisterRouter() http.Handler {
@@ -18,6 +25,20 @@ func (s *Server) RegisterRouter() http.Handler {
1825
AllowCredentials: true, // Enable cookies/auth
1926
}))
2027

28+
sessionSectret := os.Getenv("SESSION_SECRET")
29+
if sessionSectret == "" {
30+
panic("SESSION_SECRET environment variable is not set")
31+
}
32+
33+
s.setupGoth(sessionSectret)
34+
35+
googleHandler := auth.NewGoogleHandler(sessionSectret)
36+
auth := router.Group("/auth")
37+
auth.GET("google", googleHandler.Signup)
38+
auth.GET("google/callback", googleHandler.HandleCallback)
39+
auth.GET("is-auth", googleHandler.GetAuthUser)
40+
auth.GET("login/failed", googleHandler.HandleUnauthorized)
41+
2142
// Serve static files from the React app build directory
2243
router.Use(static.Serve("/", static.LocalFile("frontend/dist", true)))
2344

@@ -29,6 +50,30 @@ func (s *Server) RegisterRouter() http.Handler {
2950
return router
3051
}
3152

53+
func (s *Server) setupGoth(sessionSecret string) {
54+
store := sessions.NewCookieStore([]byte(sessionSecret))
55+
store.MaxAge(int(12 * time.Hour / time.Second))
56+
store.Options.Path = "/"
57+
store.Options.HttpOnly = true
58+
store.Options.Secure = os.Getenv("APP_ENV") == "production"
59+
// store.Options.SameSite = http.SameSiteLaxMode // helps prevent CSRF
60+
61+
gothic.Store = store
62+
63+
goth.UseProviders(
64+
google.New(
65+
os.Getenv("GOOGLE_CLIENT_ID"),
66+
os.Getenv("GOOGLE_CLIENT_SECRET"),
67+
os.Getenv("GOOGLE_CALLBACK_URL"),
68+
),
69+
// github.New(
70+
// os.Getenv("GITHUB_CLIENT_ID"),
71+
// os.Getenv("GITHUB_CLIENT_SECRET"),
72+
// os.Getenv("GITHUB_CALLBACK_URL"),
73+
// ),
74+
)
75+
}
76+
3277
// func (s *Server) HelloWorldHandler(c *gin.Context) {
3378
// resp := make(map[string]string)
3479
// resp["message"] = "Hello World"

internal/server/server.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@ import (
1515

1616
type Server struct {
1717
port int
18-
19-
db database.Service
18+
db database.Service
2019
}
2120

22-
func NewServer() *http.Server {
21+
func New() *http.Server {
2322
// Load .env file explicitly
2423
if err := godotenv.Load(); err != nil {
2524
log.Println("Warning: .env file not found, using system environment variables")
@@ -30,8 +29,7 @@ func NewServer() *http.Server {
3029
fmt.Printf("Starting server on port %d\n", port)
3130
NewServer := &Server{
3231
port: port,
33-
34-
db: database.New(),
32+
db: database.New(),
3533
}
3634

3735
// Declare Server config

0 commit comments

Comments
 (0)