From 667345bc9e455c572f90689e48b76a4dc4897b54 Mon Sep 17 00:00:00 2001 From: Achim Rohn Date: Wed, 17 Sep 2025 17:32:50 +0200 Subject: [PATCH] Add authentication middleware --- authentication/auth.go | 110 ++++++++++++++++++++++ middleware.go | 4 +- starter/google/google_auth.go | 166 ++++++++++++++++++++++++++++++++-- starter/main.go | 2 +- starter/routes/routing.go | 21 ++++- 5 files changed, 291 insertions(+), 12 deletions(-) create mode 100644 authentication/auth.go diff --git a/authentication/auth.go b/authentication/auth.go new file mode 100644 index 0000000..ef63192 --- /dev/null +++ b/authentication/auth.go @@ -0,0 +1,110 @@ +package authentication + +import ( + . "ersteller-lib" + "net/http" + "strings" + + "github.com/gorilla/sessions" + "golang.org/x/net/context" +) + +const sessionName = "session" +const EmailKey = "email" +const UserIdKey = "userId" +const AuthContextKey = "authContext" + +type AuthContext struct { + Email string + UserId int +} + +func SaveEmailAndUserIdToSessionStore(request *http.Request, writer http.ResponseWriter, store *sessions.CookieStore, email string, userId int) error { + session, err := store.New(request, sessionName) + if err != nil { + LogError("Failed to create session: %v", err) + return err + } + session.Values = map[interface{}]interface{}{ + EmailKey: email, + UserIdKey: userId, + } + err = session.Save(request, writer) + if err != nil { + LogError("Failed to save session: %v", err) + return err + } + return nil +} + +func SetUserIdAndEmailFromSessionStore(request *http.Request, store *sessions.CookieStore) (bool, *http.Request, error) { + session, err := store.Get(request, sessionName) + if err != nil { + LogError("Failed to get session: %v", err) + return false, nil, err + } + email, ok := session.Values[EmailKey].(string) + if !ok { + return false, nil, nil + } + userId, ok := session.Values[UserIdKey].(int) + if !ok { + return false, nil, nil + } + ctx := context.WithValue(request.Context(), AuthContextKey, AuthContext{ + Email: email, + UserId: userId, + }) + return true, request.WithContext(ctx), nil +} + +func LogoutSession(writer http.ResponseWriter, request *http.Request, sessionStore *sessions.CookieStore, redirectUrl string) { + // Clear the session + session, err := sessionStore.Get(request, sessionName) // Using default session name + if err != nil { + LogError("Failed to get session: %v", err) + http.Redirect(writer, request, redirectUrl, http.StatusTemporaryRedirect) + return + } + + session.Options.MaxAge = -1 + err = session.Save(request, writer) + if err != nil { + LogError("Failed to save session: %v", err) + http.Error(writer, "Failed to clear session", http.StatusInternalServerError) + return + } + + http.Redirect(writer, request, redirectUrl, http.StatusTemporaryRedirect) +} + +func Middleware(store *sessions.CookieStore, excludedPrefixes []string, redirectUrl string) MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Debug("Authenticating") + if r.Method == http.MethodOptions { + next.ServeHTTP(w, r) + return + } + for _, prefix := range excludedPrefixes { + if strings.HasPrefix(r.URL.Path, prefix) { + next.ServeHTTP(w, r) + return + } + } + ok, r, err := SetUserIdAndEmailFromSessionStore(r, store) + if err != nil { + LogError("Failed to set user id and email from session: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + if ok { + next.ServeHTTP(w, r) + } else { + Debug("Redirecting to login because there is no session") + http.Redirect(w, r, redirectUrl, http.StatusTemporaryRedirect) + } + }) + } + +} diff --git a/middleware.go b/middleware.go index f558c43..667c7b9 100644 --- a/middleware.go +++ b/middleware.go @@ -31,8 +31,10 @@ func MakeGzipHandler(fn http.Handler) http.Handler { }) } +type MiddlewareFunc func(next http.Handler) http.Handler + // https://www.jvt.me/posts/2023/09/01/golang-nethttp-global-middleware/ -func UseMiddleware(r HtmxServer, middlewares ...func(next http.Handler) http.Handler) http.Handler { +func UseMiddleware(r HtmxServer, middlewares ...MiddlewareFunc) http.Handler { var s http.Handler s = r.GetHttpServer() diff --git a/starter/google/google_auth.go b/starter/google/google_auth.go index db6473d..82fbf3e 100644 --- a/starter/google/google_auth.go +++ b/starter/google/google_auth.go @@ -1,14 +1,21 @@ package google import ( + "context" "crypto/rand" "encoding/base64" + "encoding/json" . "ersteller-lib" + "ersteller-lib/authentication" "ersteller-lib/starter/ent" + "ersteller-lib/starter/ent/user" "ersteller-lib/starter/env" + "io/ioutil" "net/http" "time" + "github.com/gorilla/sessions" + "golang.org/x/oauth2" "golang.org/x/oauth2/google" ) @@ -17,22 +24,36 @@ const oAuthStateCookieName = "oauthstate" const GoogleLogin = "/login/google" const GoogleLoginCallback = "/google/authenticated" -type GoogleAuth struct { - db *ent.Client - server *http.ServeMux - environment env.Environment - config oauth2.Config +const oauthGoogleUrlAPI = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + +type GoogleUserData struct { + Email string `json:"email"` + Token *oauth2.Token `json:"-"` } -func NewGoogleAuth(db *ent.Client, server *http.ServeMux, environment env.Environment) *GoogleAuth { +type GoogleAuth struct { + db *ent.Client + server *http.ServeMux + environment env.Environment + config oauth2.Config + sessionStore *sessions.CookieStore +} + +func NewGoogleAuth(db *ent.Client, server *http.ServeMux, environment env.Environment, sessionStore *sessions.CookieStore) *GoogleAuth { config := oauth2.Config{ ClientID: environment.GoogleClientId, ClientSecret: environment.GoogleClientSecret, Endpoint: google.Endpoint, RedirectURL: environment.BaseUrl + GoogleLoginCallback, - Scopes: []string{}, + Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"}, + } + return &GoogleAuth{ + db: db, + server: server, + environment: environment, + config: config, + sessionStore: sessionStore, } - return &GoogleAuth{db: db, server: server, environment: environment, config: config} } func (g *GoogleAuth) AddRoutes() { @@ -41,6 +62,135 @@ func (g *GoogleAuth) AddRoutes() { url := g.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce) http.Redirect(writer, request, url, http.StatusTemporaryRedirect) }) + + // OAuth callback handler + g.server.HandleFunc("GET "+GoogleLoginCallback, func(writer http.ResponseWriter, request *http.Request) { + LogDebug("email authenticated called") + + // Get OAuth state cookie + oauthstateCookie, err := request.Cookie(oAuthStateCookieName) + if err != nil { + LogError("Failed to get cookie: %v", err) + http.Error(writer, "Failed to get OAuth state cookie", http.StatusBadRequest) + return + } + + // Verify OAuth state + if request.FormValue("state") != oauthstateCookie.Value { + LogError("Failed to verify google oauth state") + http.Error(writer, "Invalid OAuth state", http.StatusBadRequest) + return + } + + // Get authorization code + code := request.FormValue("code") + data, err := g.ParseUserData(code) + if err != nil { + LogError("Failed to parse user data: %v", err) + http.Error(writer, "Failed to parse user data", http.StatusInternalServerError) + return + } + + // Get or create user + userId, err := g.db.User.Query().Where(user.Email(data.Email)).OnlyID(request.Context()) + if err != nil { + LogError("Failed to get user id: %v", err) + newUser, err := g.db.User.Create().SetEmail(data.Email).Save(request.Context()) + if err != nil { + LogError("Failed to create user: %v", err) + http.Error(writer, "Failed to create user", http.StatusInternalServerError) + return + } + userId = newUser.ID + } + + // Save email to session + err = authentication.SaveEmailAndUserIdToSessionStore(request, writer, g.sessionStore, data.Email, userId) + if err != nil { + LogError("Failed to save session: %v", err) + http.Error(writer, "Failed to save session", http.StatusInternalServerError) + return + } + + // Save credentials + err = g.SaveCredentials(userId, data.Token) + if err != nil { + Error("failed to save credentials for user ", userId, ": ", err) + http.Error(writer, "Failed to save credentials", http.StatusInternalServerError) + return + } + + Debug("saved credentials for user", userId) + http.Redirect(writer, request, "/", http.StatusTemporaryRedirect) + }) + + // Logout handler + g.server.HandleFunc("GET /logout", func(writer http.ResponseWriter, request *http.Request) { + // Clear the session + session, err := g.sessionStore.Get(request, "session") // Using default session name + if err != nil { + LogError("Failed to get session: %v", err) + http.Redirect(writer, request, "/", http.StatusTemporaryRedirect) + return + } + + session.Options.MaxAge = -1 + err = session.Save(request, writer) + if err != nil { + LogError("Failed to save session: %v", err) + http.Error(writer, "Failed to clear session", http.StatusInternalServerError) + return + } + + http.Redirect(writer, request, "/", http.StatusTemporaryRedirect) + }) +} + +func (g *GoogleAuth) ParseUserData(code string) (GoogleUserData, error) { + data, err := g.getUserDataFromGoogle(code) + if err != nil { + LogError("failed getting user data from Google: %v", err) + return GoogleUserData{}, err + } + LogDebug("user data: %v", data) + return data, nil +} + +func (g *GoogleAuth) getUserDataFromGoogle(code string) (GoogleUserData, error) { + // Use code to get token and get user info from Google. + token, err := g.config.Exchange(context.Background(), code) + if err != nil { + LogError("code exchange wrong: %v", err) + return GoogleUserData{}, err + } + + response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken) + if err != nil { + LogError("failed getting user info: %v", err) + return GoogleUserData{}, err + } + defer response.Body.Close() + contents, err := ioutil.ReadAll(response.Body) + if err != nil { + LogError("failed read response: %v", err) + return GoogleUserData{}, err + } + userData := GoogleUserData{} + err = json.Unmarshal(contents, &userData) + if err != nil { + LogError("failed to unmarshal user data: %v", err) + return GoogleUserData{}, err + } + userData.Token = token + + return userData, nil +} + +func (g *GoogleAuth) SaveCredentials(userId int, token *oauth2.Token) error { + Debug("saving google credentials for user ", userId) + // For now, we'll just log this - in a real implementation you'd save to database + Debug("saved credentials for user", userId) + return nil } func (g *GoogleAuth) generateStateOauthCookie(w http.ResponseWriter) string { diff --git a/starter/main.go b/starter/main.go index d83d690..2a2e784 100644 --- a/starter/main.go +++ b/starter/main.go @@ -33,6 +33,6 @@ func main() { } Debug("starting white label app on port 8090") - handler := routes.CreateApi() + handler := routes.CreateApi(environment, client) log.Fatal(http.ListenAndServe(":8090", handler)) } diff --git a/starter/routes/routing.go b/starter/routes/routing.go index 35589e1..8504208 100644 --- a/starter/routes/routing.go +++ b/starter/routes/routing.go @@ -2,20 +2,35 @@ package routes import ( . "ersteller-lib" + "ersteller-lib/authentication" "ersteller-lib/starter/about" "ersteller-lib/starter/contact" + "ersteller-lib/starter/ent" + "ersteller-lib/starter/env" + "ersteller-lib/starter/google" "ersteller-lib/starter/index" "ersteller-lib/starter/page" "net/http" + "github.com/gorilla/sessions" . "maragu.dev/gomponents" ) -func CreateApi() http.Handler { +func CreateApi(environment env.Environment, db *ent.Client) http.Handler { server := NewHtmxServer() HtmxRouteDebugTrace = true + sessionStore := sessions.NewCookieStore([]byte(environment.SessionSecret)) + //sessionStore.Options.Secure = false + if !environment.IsDev { + sessionStore.Options.Secure = true + sessionStore.Options.HttpOnly = true + } + + googleAuth := google.NewGoogleAuth(db, server.GetHttpServer(), environment, sessionStore) + googleAuth.AddRoutes() + indexActivePath := NewActivePath(map[Language]string{ En: "Home", De: "Startseite", @@ -54,7 +69,9 @@ func CreateApi() http.Handler { _ = about.NewPage(createPageFunc, server, &aboutActivePath) _ = contact.NewPage(createPageFunc, server, &contactActivePath) - serverWithMiddleWare := UseMiddleware(server, LoggingMiddleware, MakeGzipHandler) + serverWithMiddleWare := UseMiddleware(server, LoggingMiddleware, MakeGzipHandler, + authentication.Middleware(sessionStore, + []string{"/login", google.GoogleLogin, google.GoogleLoginCallback}, "/")) return serverWithMiddleWare }