Skip to content

Commit

Permalink
Simplify Gothic to use our session store instead of creating a differ…
Browse files Browse the repository at this point in the history
…ent store (#17507)

* Simplify Gothic to use our session store instead of creating a different store

We have been using xormstore to provide a separate session store for our OAuth2 logins
however, this relies on using gorilla context and some doubling of our session storing.
We can however, simplify and simply use our own chi-based session store. Thus removing
a cookie and some of the weirdness with missing contexts.

Signed-off-by: Andrew Thornton <[email protected]>

* as per review

Signed-off-by: Andrew Thornton <[email protected]>

* as per review

Signed-off-by: Andrew Thornton <[email protected]>

* Handle MaxTokenLength

Signed-off-by: Andrew Thornton <[email protected]>

* oops

Signed-off-by: Andrew Thornton <[email protected]>

Co-authored-by: techknowlogick <[email protected]>
Co-authored-by: Lauris BH <[email protected]>
  • Loading branch information
3 people authored Nov 3, 2021
1 parent 95da01c commit 9d855bd
Show file tree
Hide file tree
Showing 25 changed files with 110 additions and 934 deletions.
15 changes: 7 additions & 8 deletions cmd/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/install"

context2 "github.com/gorilla/context"
"github.com/urfave/cli"
ini "gopkg.in/ini.v1"
)
Expand Down Expand Up @@ -71,7 +70,7 @@ func runHTTPRedirector() {
http.Redirect(w, r, target, http.StatusTemporaryRedirect)
})

var err = runHTTP("tcp", source, "HTTP Redirector", context2.ClearHandler(handler))
var err = runHTTP("tcp", source, "HTTP Redirector", handler)

if err != nil {
log.Fatal("Failed to start port redirection: %v", err)
Expand Down Expand Up @@ -209,10 +208,10 @@ func listen(m http.Handler, handleRedirector bool) error {
if handleRedirector {
NoHTTPRedirector()
}
err = runHTTP("tcp", listenAddr, "Web", context2.ClearHandler(m))
err = runHTTP("tcp", listenAddr, "Web", m)
case setting.HTTPS:
if setting.EnableLetsEncrypt {
err = runLetsEncrypt(listenAddr, setting.Domain, setting.LetsEncryptDirectory, setting.LetsEncryptEmail, context2.ClearHandler(m))
err = runLetsEncrypt(listenAddr, setting.Domain, setting.LetsEncryptDirectory, setting.LetsEncryptEmail, m)
break
}
if handleRedirector {
Expand All @@ -222,22 +221,22 @@ func listen(m http.Handler, handleRedirector bool) error {
NoHTTPRedirector()
}
}
err = runHTTPS("tcp", listenAddr, "Web", setting.CertFile, setting.KeyFile, context2.ClearHandler(m))
err = runHTTPS("tcp", listenAddr, "Web", setting.CertFile, setting.KeyFile, m)
case setting.FCGI:
if handleRedirector {
NoHTTPRedirector()
}
err = runFCGI("tcp", listenAddr, "FCGI Web", context2.ClearHandler(m))
err = runFCGI("tcp", listenAddr, "FCGI Web", m)
case setting.UnixSocket:
if handleRedirector {
NoHTTPRedirector()
}
err = runHTTP("unix", listenAddr, "Web", context2.ClearHandler(m))
err = runHTTP("unix", listenAddr, "Web", m)
case setting.FCGIUnix:
if handleRedirector {
NoHTTPRedirector()
}
err = runFCGI("unix", listenAddr, "Web", context2.ClearHandler(m))
err = runFCGI("unix", listenAddr, "Web", m)
default:
log.Fatal("Invalid protocol: %s", setting.Protocol)
}
Expand Down
3 changes: 1 addition & 2 deletions cmd/web_letsencrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"code.gitea.io/gitea/modules/setting"

"github.com/caddyserver/certmagic"
context2 "github.com/gorilla/context"
)

func runLetsEncrypt(listenAddr, domain, directory, email string, m http.Handler) error {
Expand Down Expand Up @@ -67,7 +66,7 @@ func runLetsEncrypt(listenAddr, domain, directory, email string, m http.Handler)
}()
}

return runHTTPSWithTLSConfig("tcp", listenAddr, "Web", tlsConfig, context2.ClearHandler(m))
return runHTTPSWithTLSConfig("tcp", listenAddr, "Web", tlsConfig, m)
}

func runLetsEncryptFallbackHandler(w http.ResponseWriter, r *http.Request) {
Expand Down
3 changes: 1 addition & 2 deletions contrib/pr/checkout.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import (
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
context2 "github.com/gorilla/context"
"xorm.io/xorm"
)

Expand Down Expand Up @@ -138,7 +137,7 @@ func runPR() {
*/

//Start the server
http.ListenAndServe(":8080", context2.ClearHandler(c))
http.ListenAndServe(":8080", c)

log.Printf("[PR] Cleaning up ...\n")
/*
Expand Down
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,9 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-github/v39 v39.2.0
github.com/google/uuid v1.2.0
github.com/gorilla/context v1.1.1
github.com/gorilla/feeds v1.1.1
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/gorilla/sessions v1.2.1
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/hashicorp/go-version v1.3.1
Expand All @@ -73,7 +72,6 @@ require (
github.com/klauspost/compress v1.13.1
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/lafriks/xormstore v1.4.0
github.com/lib/pq v1.10.2
github.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
github.com/markbates/goth v1.68.0
Expand Down
9 changes: 0 additions & 9 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/ProtonMail/go-crypto v0.0.0-20210705153151-cc34b1f6908b h1:BF5p87XWvmgdrTPPzcRMwC0TMQbviwQ+uBKfNfWJy50=
github.com/ProtonMail/go-crypto v0.0.0-20210705153151-cc34b1f6908b/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/goquery v1.7.0 h1:O5SP3b9JWqMSVMG69zMfj577zwkSNpxrFf7ybS74eiw=
github.com/PuerkitoBio/goquery v1.7.0/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
Expand Down Expand Up @@ -256,7 +255,6 @@ github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
Expand Down Expand Up @@ -801,14 +799,11 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lafriks/xormstore v1.4.0 h1:DX1yS9WUhVY+MTHGaOJ2tDVpwL1w/247iro5KR0BQEQ=
github.com/lafriks/xormstore v1.4.0/go.mod h1:5a3wJ6Ro0TFJmJcH1ywtHO/fBEIWYfSfO4WTYmM7qEk=
github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
Expand Down Expand Up @@ -858,7 +853,6 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
Expand Down Expand Up @@ -1260,7 +1254,6 @@ golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
Expand Down Expand Up @@ -1761,9 +1754,7 @@ sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs=
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/builder v0.3.9 h1:Sd65/LdWyO7LR8+Cbd+e7mm3sK/7U9k0jS3999IDHMc=
xorm.io/builder v0.3.9/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.0.6/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
xorm.io/xorm v1.2.5 h1:tqN7OhN8P9xi52qBb76I8m5maAJMz/SSbgK2RGPCPbo=
xorm.io/xorm v1.2.5/go.mod h1:fTG8tSjk6O1BYxwuohZUK+S1glnRycsCF05L1qQyEU0=
18 changes: 0 additions & 18 deletions models/db/store.go

This file was deleted.

2 changes: 1 addition & 1 deletion models/login/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
// Session represents a session compatible for go-chi session
type Session struct {
Key string `xorm:"pk CHAR(16)"` // has to be Key to match with go-chi/session
Data []byte `xorm:"BLOB"`
Data []byte `xorm:"BLOB"` // on MySQL this has a maximum size of 64Kb - this may need to be increased
Expiry timeutil.TimeStamp // has to be Expiry to match with go-chi/session
}

Expand Down
2 changes: 1 addition & 1 deletion routers/web/user/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *login.Source, u *models.Us
func oAuth2UserLoginCallback(loginSource *login.Source, request *http.Request, response http.ResponseWriter) (*models.User, goth.User, error) {
gothUser, err := loginSource.Cfg.(*oauth2.Source).Callback(request, response)
if err != nil {
if err.Error() == "securecookie: the value is too long" {
if err.Error() == "securecookie: the value is too long" || strings.Contains(err.Error(), "Data too long") {
log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
err = fmt.Errorf("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength)
}
Expand Down
25 changes: 7 additions & 18 deletions services/auth/source/oauth2/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,21 @@
package oauth2

import (
"encoding/gob"
"net/http"
"sync"

"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/login"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"

"github.com/google/uuid"
"github.com/gorilla/sessions"
"github.com/markbates/goth/gothic"
)

var gothRWMutex = sync.RWMutex{}

// SessionTableName is the table name that OAuth2 will use to store things
const SessionTableName = "oauth2_session"

// UsersStoreKey is the key for the store
const UsersStoreKey = "gitea-oauth2-sessions"

Expand All @@ -34,23 +32,14 @@ func Init() error {
return err
}

store, err := db.CreateStore(SessionTableName, UsersStoreKey)
if err != nil {
return err
}

// according to the Goth lib:
// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
// securecookie: the value is too long
// when using OpenID Connect , since this can contain a large amount of extra information in the id_token

// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
store.MaxLength(setting.OAuth2.MaxTokenLength)

// Lock our mutex
gothRWMutex.Lock()

gothic.Store = store
gob.Register(&sessions.Session{})

gothic.Store = &SessionsStore{
maxLength: int64(setting.OAuth2.MaxTokenLength),
}

gothic.SetState = func(req *http.Request) string {
return uuid.New().String()
Expand Down
91 changes: 91 additions & 0 deletions services/auth/source/oauth2/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package oauth2

import (
"encoding/gob"
"fmt"
"net/http"

"code.gitea.io/gitea/modules/log"
chiSession "gitea.com/go-chi/session"
"github.com/gorilla/sessions"
)

// SessionsStore creates a gothic store from our session
type SessionsStore struct {
maxLength int64
}

// Get should return a cached session.
func (st *SessionsStore) Get(r *http.Request, name string) (*sessions.Session, error) {
return st.getOrNew(r, name, false)
}

// New should create and return a new session.
//
// Note that New should never return a nil session, even in the case of
// an error if using the Registry infrastructure to cache the session.
func (st *SessionsStore) New(r *http.Request, name string) (*sessions.Session, error) {
return st.getOrNew(r, name, true)
}

// getOrNew gets the session from the chi-session if it exists. Override permits the overriding of an unexpected object.
func (st *SessionsStore) getOrNew(r *http.Request, name string, override bool) (*sessions.Session, error) {
chiStore := chiSession.GetSession(r)

session := sessions.NewSession(st, name)

rawData := chiStore.Get(name)
if rawData != nil {
oldSession, ok := rawData.(*sessions.Session)
if ok {
session.ID = oldSession.ID
session.IsNew = oldSession.IsNew
session.Options = oldSession.Options
session.Values = oldSession.Values

return session, nil
} else if !override {
log.Error("Unexpected object in session at name: %s: %v", name, rawData)
return nil, fmt.Errorf("unexpected object in session at name: %s", name)
}
}

session.ID = chiStore.ID() // Simply copy the session id from the chi store

return session, chiStore.Set(name, session)
}

// Save should persist session to the underlying store implementation.
func (st *SessionsStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
chiStore := chiSession.GetSession(r)

if err := chiStore.Set(session.Name(), session); err != nil {
return err
}

if st.maxLength > 0 {
sizeWriter := &sizeWriter{}

_ = gob.NewEncoder(sizeWriter).Encode(session)
if sizeWriter.size > st.maxLength {
return fmt.Errorf("encode session: Data too long: %d > %d", sizeWriter.size, st.maxLength)
}
}

return chiStore.Release()
}

type sizeWriter struct {
size int64
}

func (s *sizeWriter) Write(data []byte) (int, error) {
s.size += int64(len(data))
return len(data), nil
}

var _ (sessions.Store) = &SessionsStore{}

This file was deleted.

27 changes: 0 additions & 27 deletions vendor/github.com/gorilla/context/LICENSE

This file was deleted.

Loading

0 comments on commit 9d855bd

Please sign in to comment.