From d30327817ed90877d73d1952cfa0207adecca595 Mon Sep 17 00:00:00 2001 From: Maximilian Friedersdorff Date: Wed, 10 Dec 2025 20:40:41 +0000 Subject: [PATCH] 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")