From d30327817ed90877d73d1952cfa0207adecca595 Mon Sep 17 00:00:00 2001 From: Maximilian Friedersdorff Date: Wed, 10 Dec 2025 20:40:41 +0000 Subject: [PATCH 1/3] Refactor oauth login --- cmd/server/main.go | 18 ++++-- internal/auth/oauth.go | 84 ++------------------------ internal/conf/templates/base.tmpl.html | 4 +- internal/middleware/session.go | 74 ++++++++++++++++++++++- 4 files changed, 90 insertions(+), 90 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 0a39c4d..7f6d173 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -8,23 +8,30 @@ import ( "os" "time" - "forgejo.gwairfelin.com/max/gonotes/internal/auth" "forgejo.gwairfelin.com/max/gonotes/internal/conf" "forgejo.gwairfelin.com/max/gonotes/internal/middleware" "forgejo.gwairfelin.com/max/gonotes/internal/notes" "forgejo.gwairfelin.com/max/gonotes/internal/notes/views" + "golang.org/x/oauth2" ) func main() { var confFile string - sessions := middleware.NewSessionStore() - flag.StringVar(&confFile, "c", "/etc/gonotes/conf.toml", "Specify path to config file.") flag.Parse() conf.LoadConfig(confFile) + oauth := &oauth2.Config{ + ClientID: conf.Conf.OIDC.ClientID, + ClientSecret: conf.Conf.OIDC.ClientSecret, + Endpoint: oauth2.Endpoint{AuthURL: conf.Conf.OIDC.AuthURL, TokenURL: conf.Conf.OIDC.TokenURL}, + RedirectURL: conf.Conf.OIDC.RedirectURL, + } + + sessions := middleware.NewSessionStore(oauth, "/auth") + err := notes.Init() if err != nil { log.Fatal(err) @@ -34,7 +41,7 @@ func main() { router := http.NewServeMux() notesRouter := views.GetRoutes("/notes") - authRouter := auth.GetRoutes("/auth", sessions.Login) + sessionRouter := sessions.Routes.GetRouter() cacheExpiration, err := time.ParseDuration("24h") if err != nil { @@ -43,10 +50,9 @@ func main() { etag := middleware.NewETag("static", cacheExpiration) - router.Handle("/logout/", sessions.AsMiddleware(middleware.LoggingMiddleware(http.HandlerFunc(sessions.Logout)))) router.Handle("/", middleware.LoggingMiddleware(http.RedirectHandler("/notes/", http.StatusFound))) router.Handle("/notes/", sessions.AsMiddleware(middleware.LoggingMiddleware(http.StripPrefix("/notes", notesRouter)))) - router.Handle("/auth/", sessions.AsMiddleware(middleware.LoggingMiddleware(http.StripPrefix("/auth", authRouter)))) + router.Handle("/auth/", sessions.AsMiddleware(middleware.LoggingMiddleware(http.StripPrefix("/auth", sessionRouter)))) router.Handle( "/static/", middleware.LoggingMiddleware( diff --git a/internal/auth/oauth.go b/internal/auth/oauth.go index d3b9992..a746c48 100644 --- a/internal/auth/oauth.go +++ b/internal/auth/oauth.go @@ -9,108 +9,32 @@ import ( "log" "net/http" - urls "forgejo.gwairfelin.com/max/gispatcho" "forgejo.gwairfelin.com/max/gonotes/internal/conf" "golang.org/x/oauth2" ) -var myurls urls.URLs -var oauthConfig *oauth2.Config -var loginFunction func(user string, w http.ResponseWriter) string - type userInfo struct { preferred_username string } -func GetRoutes(prefix string, _loginFunction func(user string, w http.ResponseWriter) string) *http.ServeMux { - loginFunction = _loginFunction - - oauthConfig = &oauth2.Config{ - ClientID: conf.Conf.OIDC.ClientID, - ClientSecret: conf.Conf.OIDC.ClientSecret, - Endpoint: oauth2.Endpoint{AuthURL: conf.Conf.OIDC.AuthURL, TokenURL: conf.Conf.OIDC.TokenURL}, - RedirectURL: conf.Conf.OIDC.RedirectURL, - } - - myurls = urls.URLs{ - Prefix: prefix, - URLs: map[string]urls.URL{ - "login": {Path: "/oauth/login/", Protocol: "GET", Handler: oauthLogin}, - "callback": {Path: "/oauth/callback/", Protocol: "GET", Handler: oauthCallback}, - }, - } - return myurls.GetRouter() -} - -func oauthLogin(w http.ResponseWriter, r *http.Request) { - log.Printf("%+v", *oauthConfig) - - oauthState := generateStateOAUTHCookie(w) - - url := oauthConfig.AuthCodeURL(oauthState) - log.Printf("Redirecting to %s", url) - http.Redirect(w, r, url, http.StatusTemporaryRedirect) -} - -func generateStateOAUTHCookie(w http.ResponseWriter) string { +func GenerateStateOAUTHCookie(w http.ResponseWriter, prefix string) string { b := make([]byte, 16) rand.Read(b) state := base64.URLEncoding.EncodeToString(b) cookie := http.Cookie{ Name: "oauthstate", Value: state, - MaxAge: 30, Secure: true, HttpOnly: true, Path: "/auth/oauth/", + MaxAge: 30, Secure: true, HttpOnly: true, Path: prefix, } http.SetCookie(w, &cookie) return state } -func oauthCallback(w http.ResponseWriter, r *http.Request) { - // Read oauthState from Cookie - oauthState, err := r.Cookie("oauthstate") - if err != nil { - log.Printf("An error occured during login: %s", err) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - log.Printf("%v", oauthState) - - if r.FormValue("state") != oauthState.Value { - log.Println("invalid oauth state") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - data, err := getUserData(r.FormValue("code")) - if err != nil { - log.Println(err.Error()) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - username, ok := data["preferred_username"] - if !ok { - log.Println("No username in auth response") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - userStr, ok := username.(string) - if !ok { - log.Println("Username not interpretable as string") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - loginFunction(userStr, w) - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) -} - -func getUserData(code string) (map[string]any, error) { +func GetUserData(code string, oauth *oauth2.Config) (map[string]any, error) { // Use code to get token and get user info from Google. - token, err := oauthConfig.Exchange(context.Background(), code) + token, err := oauth.Exchange(context.Background(), code) if err != nil { return nil, fmt.Errorf("code exchange wrong: %s", err.Error()) } diff --git a/internal/conf/templates/base.tmpl.html b/internal/conf/templates/base.tmpl.html index 8ce885a..6d776ff 100644 --- a/internal/conf/templates/base.tmpl.html +++ b/internal/conf/templates/base.tmpl.html @@ -64,11 +64,11 @@ {{template "navLinks" .}} {{if eq .user "anon"}}
  • - Login + Login
  • {{else}}
  • - Logout {{.user}} + Logout {{.user}}
  • {{end}} diff --git a/internal/middleware/session.go b/internal/middleware/session.go index 8a11c83..5a7770a 100644 --- a/internal/middleware/session.go +++ b/internal/middleware/session.go @@ -4,7 +4,12 @@ package middleware import ( "context" "crypto/rand" + "log" "net/http" + + urls "forgejo.gwairfelin.com/max/gispatcho" + "forgejo.gwairfelin.com/max/gonotes/internal/auth" + "golang.org/x/oauth2" ) type Session struct { @@ -13,12 +18,26 @@ type Session struct { type SessionStore struct { sessions map[string]Session + oauth *oauth2.Config + Routes urls.URLs } type ContextKey string -func NewSessionStore() SessionStore { - return SessionStore{sessions: make(map[string]Session, 10)} +func NewSessionStore(oauth *oauth2.Config, prefix string) SessionStore { + store := SessionStore{ + sessions: make(map[string]Session, 10), + oauth: oauth, + } + store.Routes = urls.URLs{ + Prefix: prefix, + URLs: map[string]urls.URL{ + "login": {Path: "/login/", Protocol: "GET", Handler: store.LoginViewOAUTH}, + "callback": {Path: "/callback/", Protocol: "GET", Handler: store.CallbackViewOAUTH}, + "logout": {Path: "/logout/", Protocol: "GET", Handler: store.Logout}, + }, + } + return store } func (s *SessionStore) Login(user string, w http.ResponseWriter) string { @@ -47,6 +66,57 @@ func (s *SessionStore) Logout(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusTemporaryRedirect) } +func (s *SessionStore) LoginViewOAUTH(w http.ResponseWriter, r *http.Request) { + log.Printf("%+v", *s.oauth) + + oauthState := auth.GenerateStateOAUTHCookie(w, s.Routes.Prefix) + + url := s.oauth.AuthCodeURL(oauthState) + log.Printf("Redirecting to %s", url) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func (s *SessionStore) CallbackViewOAUTH(w http.ResponseWriter, r *http.Request) { + // Read oauthState from Cookie + oauthState, err := r.Cookie("oauthstate") + if err != nil { + log.Printf("An error occured during login: %s", err) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + log.Printf("%v", oauthState) + + if r.FormValue("state") != oauthState.Value { + log.Println("invalid oauth state") + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + data, err := auth.GetUserData(r.FormValue("code"), s.oauth) + if err != nil { + log.Println(err.Error()) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + username, ok := data["preferred_username"] + if !ok { + log.Println("No username in auth response") + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + userStr, ok := username.(string) + if !ok { + log.Println("Username not interpretable as string") + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + s.Login(userStr, w) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) +} + func (s *SessionStore) AsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sessionCookie, err := r.Cookie("id") From a01f6dec23002313590d80267d3b48758f2810b9 Mon Sep 17 00:00:00 2001 From: Maximilian Friedersdorff Date: Wed, 10 Dec 2025 20:45:33 +0000 Subject: [PATCH 2/3] Comment some session functions --- internal/middleware/session.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/middleware/session.go b/internal/middleware/session.go index 5a7770a..bea2354 100644 --- a/internal/middleware/session.go +++ b/internal/middleware/session.go @@ -34,12 +34,13 @@ func NewSessionStore(oauth *oauth2.Config, prefix string) SessionStore { URLs: map[string]urls.URL{ "login": {Path: "/login/", Protocol: "GET", Handler: store.LoginViewOAUTH}, "callback": {Path: "/callback/", Protocol: "GET", Handler: store.CallbackViewOAUTH}, - "logout": {Path: "/logout/", Protocol: "GET", Handler: store.Logout}, + "logout": {Path: "/logout/", Protocol: "GET", Handler: store.LogoutView}, }, } return store } +// Log a user in func (s *SessionStore) Login(user string, w http.ResponseWriter) string { sessionID := rand.Text() s.sessions[sessionID] = Session{User: user} @@ -53,7 +54,8 @@ func (s *SessionStore) Login(user string, w http.ResponseWriter) string { return sessionID } -func (s *SessionStore) Logout(w http.ResponseWriter, r *http.Request) { +// View to logout a user +func (s *SessionStore) LogoutView(w http.ResponseWriter, r *http.Request) { session := r.Context().Value(ContextKey("session")).(string) delete(s.sessions, session) @@ -66,6 +68,7 @@ func (s *SessionStore) Logout(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusTemporaryRedirect) } +// View to log in a user via oauth func (s *SessionStore) LoginViewOAUTH(w http.ResponseWriter, r *http.Request) { log.Printf("%+v", *s.oauth) @@ -76,6 +79,7 @@ func (s *SessionStore) LoginViewOAUTH(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, url, http.StatusTemporaryRedirect) } +// Oauth callback view func (s *SessionStore) CallbackViewOAUTH(w http.ResponseWriter, r *http.Request) { // Read oauthState from Cookie oauthState, err := r.Cookie("oauthstate") @@ -117,6 +121,8 @@ func (s *SessionStore) CallbackViewOAUTH(w http.ResponseWriter, r *http.Request) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) } +// Turn the session store into a middleware. +// Sets the user on the context based on the available session cookie func (s *SessionStore) AsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sessionCookie, err := r.Cookie("id") From a1c5827641f4c679b82d0541f729f86b657332d5 Mon Sep 17 00:00:00 2001 From: Maximilian Friedersdorff Date: Wed, 10 Dec 2025 20:56:17 +0000 Subject: [PATCH 3/3] Refactor forgejo user interaction --- internal/auth/oauth.go | 26 ++++++++++++++------------ internal/middleware/session.go | 17 ++--------------- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/internal/auth/oauth.go b/internal/auth/oauth.go index a746c48..6455a12 100644 --- a/internal/auth/oauth.go +++ b/internal/auth/oauth.go @@ -6,17 +6,12 @@ import ( "encoding/base64" "encoding/json" "fmt" - "log" "net/http" "forgejo.gwairfelin.com/max/gonotes/internal/conf" "golang.org/x/oauth2" ) -type userInfo struct { - preferred_username string -} - func GenerateStateOAUTHCookie(w http.ResponseWriter, prefix string) string { b := make([]byte, 16) @@ -31,23 +26,23 @@ func GenerateStateOAUTHCookie(w http.ResponseWriter, prefix string) string { return state } -func GetUserData(code string, oauth *oauth2.Config) (map[string]any, error) { +func GetUserFromForgejo(code string, oauth *oauth2.Config) (string, error) { // Use code to get token and get user info from Google. token, err := oauth.Exchange(context.Background(), code) if err != nil { - return nil, fmt.Errorf("code exchange wrong: %s", err.Error()) + return "", fmt.Errorf("code exchange wrong: %s", err.Error()) } request, err := http.NewRequest("GET", conf.Conf.OIDC.UserinfoURL, nil) if err != nil { - return nil, fmt.Errorf("failed to init http client for userinfo: %s", err.Error()) + return "", fmt.Errorf("failed to init http client for userinfo: %s", err.Error()) } request.Header.Set("Authorization", fmt.Sprintf("token %s", token.AccessToken)) response, err := http.DefaultClient.Do(request) if err != nil { - return nil, fmt.Errorf("failed getting user info: %s", err.Error()) + return "", fmt.Errorf("failed getting user info: %s", err.Error()) } defer response.Body.Close() @@ -55,10 +50,17 @@ func GetUserData(code string, oauth *oauth2.Config) (map[string]any, error) { err = json.NewDecoder(response.Body).Decode(&uInf) if err != nil { - return nil, fmt.Errorf("failed to parse response as json: %s", err.Error()) + return "", fmt.Errorf("failed to parse response as json: %s", err.Error()) } - log.Printf("Contents of user data response %s", uInf) + username, ok := uInf["preferred_username"] + if !ok { + return "", fmt.Errorf("no username in response: %s", err.Error()) + } + userStr, ok := username.(string) + if !ok { + return "", fmt.Errorf("username not a string: %s", err.Error()) + } - return uInf, nil + return userStr, nil } diff --git a/internal/middleware/session.go b/internal/middleware/session.go index bea2354..a3af9fc 100644 --- a/internal/middleware/session.go +++ b/internal/middleware/session.go @@ -97,27 +97,14 @@ func (s *SessionStore) CallbackViewOAUTH(w http.ResponseWriter, r *http.Request) return } - data, err := auth.GetUserData(r.FormValue("code"), s.oauth) + username, err := auth.GetUserFromForgejo(r.FormValue("code"), s.oauth) if err != nil { log.Println(err.Error()) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) return } - username, ok := data["preferred_username"] - if !ok { - log.Println("No username in auth response") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - userStr, ok := username.(string) - if !ok { - log.Println("Username not interpretable as string") - http.Redirect(w, r, "/", http.StatusTemporaryRedirect) - return - } - - s.Login(userStr, w) + s.Login(username, w) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) }