diff --git a/CHANGELOG.md b/CHANGELOG.md index b480f91ec..c6a71759b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to Gogs are documented in this file. ### Added +- Support using personal access token in the password field. [#3866](https://github.com/gogs/gogs/issues/3866) - An unlisted option is added when create or migrate a repository. Unlisted repositories are public but not being listed for users without direct access in the UI. [#5733](https://github.com/gogs/gogs/issues/5733) - New configuration option `[git.timeout] DIFF` for customizing operation timeout of `git diff`. [#6315](https://github.com/gogs/gogs/issues/6315) - New configuration option `[server] SSH_SERVER_MACS` for setting list of accepted MACs for connections to builtin SSH server. [#6434](https://github.com/gogs/gogs/issues/6434) diff --git a/internal/context/auth.go b/internal/context/auth.go index 9cb378631..775a3dd8a 100644 --- a/internal/context/auth.go +++ b/internal/context/auth.go @@ -5,12 +5,14 @@ package context import ( + "context" "net/http" "net/url" "strings" "github.com/go-macaron/csrf" "github.com/go-macaron/session" + "github.com/pkg/errors" gouuid "github.com/satori/go.uuid" "gopkg.in/macaron.v1" log "unknwon.dev/clog/v2" @@ -229,3 +231,23 @@ func authenticatedUser(ctx *macaron.Context, sess session.Store) (_ *db.User, is } return u, false, isTokenAuth } + +// AuthenticateByToken attempts to authenticate a user by the given access +// token. It returns db.ErrAccessTokenNotExist when the access token does not +// exist. +func AuthenticateByToken(ctx context.Context, token string) (*db.User, error) { + t, err := db.AccessTokens.GetBySHA1(ctx, token) + if err != nil { + return nil, errors.Wrap(err, "get access token by SHA1") + } + if err = db.AccessTokens.Touch(ctx, t.ID); err != nil { + // NOTE: There is no need to fail the auth flow if we can't touch the token. + log.Error("Failed to touch access token [id: %d]: %v", t.ID, err) + } + + user, err := db.Users.GetByID(ctx, t.UserID) + if err != nil { + return nil, errors.Wrapf(err, "get user by ID [user_id: %d]", t.UserID) + } + return user, nil +} diff --git a/internal/db/access_tokens.go b/internal/db/access_tokens.go index 7ed44ae9f..58f77858f 100644 --- a/internal/db/access_tokens.go +++ b/internal/db/access_tokens.go @@ -144,6 +144,11 @@ func (ErrAccessTokenNotExist) NotFound() bool { } func (db *accessTokens) GetBySHA1(ctx context.Context, sha1 string) (*AccessToken, error) { + // No need to waste a query for an empty SHA1. + if sha1 == "" { + return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}} + } + sha256 := cryptoutil.SHA256(sha1) token := new(AccessToken) err := db.WithContext(ctx).Where("sha256 = ?", sha256).First(token).Error diff --git a/internal/route/lfs/route.go b/internal/route/lfs/route.go index c00f7374b..bdacc6da6 100644 --- a/internal/route/lfs/route.go +++ b/internal/route/lfs/route.go @@ -8,12 +8,14 @@ import ( "net/http" "strings" + "github.com/pkg/errors" "gopkg.in/macaron.v1" log "unknwon.dev/clog/v2" "gogs.io/gogs/internal/auth" "gogs.io/gogs/internal/authutil" "gogs.io/gogs/internal/conf" + "gogs.io/gogs/internal/context" "gogs.io/gogs/internal/db" "gogs.io/gogs/internal/lfsutil" ) @@ -70,29 +72,26 @@ func authenticate() macaron.Handler { return } - // If username and password authentication failed, try again using username as an access token. + // If username and password combination failed, try again using either username + // or password as the token. if auth.IsErrBadCredentials(err) { - token, err := db.AccessTokens.GetBySHA1(c.Req.Context(), username) - if err != nil { - if db.IsErrAccessTokenNotExist(err) { - askCredentials(c.Resp) - } else { - internalServerError(c.Resp) - log.Error("Failed to get access token [sha: %s]: %v", username, err) - } - return - } - if err = db.AccessTokens.Touch(c.Req.Context(), token.ID); err != nil { - log.Error("Failed to touch access token: %v", err) - } - - user, err = db.Users.GetByID(c.Req.Context(), token.UserID) - if err != nil { - // Once we found the token, we're supposed to find its related user, - // thus any error is unexpected. + user, err = context.AuthenticateByToken(c.Req.Context(), username) + if err != nil && !db.IsErrAccessTokenNotExist(errors.Cause(err)) { internalServerError(c.Resp) - log.Error("Failed to get user [id: %d]: %v", token.UserID, err) + log.Error("Failed to authenticate by access token via username: %v", err) return + } else if db.IsErrAccessTokenNotExist(errors.Cause(err)) { + // Try again using the password field as the token. + user, err = context.AuthenticateByToken(c.Req.Context(), password) + if err != nil { + if db.IsErrAccessTokenNotExist(errors.Cause(err)) { + askCredentials(c.Resp) + } else { + c.Status(http.StatusInternalServerError) + log.Error("Failed to authenticate by access token via password: %v", err) + } + return + } } } diff --git a/internal/route/lfs/route_test.go b/internal/route/lfs/route_test.go index dd8f5cdef..eb0225d53 100644 --- a/internal/route/lfs/route_test.go +++ b/internal/route/lfs/route_test.go @@ -108,7 +108,7 @@ func Test_authenticate(t *testing.T) { expBody: "ID: 1, Name: unknwon", }, { - name: "authenticate by access token", + name: "authenticate by access token via username", header: http.Header{ "Authorization": []string{"Basic dXNlcm5hbWU="}, }, @@ -127,6 +127,31 @@ func Test_authenticate(t *testing.T) { expHeader: http.Header{}, expBody: "ID: 1, Name: unknwon", }, + { + name: "authenticate by access token via password", + header: http.Header{ + "Authorization": []string{"Basic dXNlcm5hbWU6cGFzc3dvcmQ="}, + }, + mockUsersStore: func() db.UsersStore { + mock := NewMockUsersStore() + mock.AuthenticateFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{}) + mock.GetByIDFunc.SetDefaultReturn(&db.User{ID: 1, Name: "unknwon"}, nil) + return mock + }, + mockAccessTokensStore: func() db.AccessTokensStore { + mock := NewMockAccessTokensStore() + mock.GetBySHA1Func.SetDefaultHook(func(ctx context.Context, sha1 string) (*db.AccessToken, error) { + if sha1 == "password" { + return &db.AccessToken{}, nil + } + return nil, db.ErrAccessTokenNotExist{} + }) + return mock + }, + expStatusCode: http.StatusOK, + expHeader: http.Header{}, + expBody: "ID: 1, Name: unknwon", + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/internal/route/repo/http.go b/internal/route/repo/http.go index af51a076b..89c7fa244 100644 --- a/internal/route/repo/http.go +++ b/internal/route/repo/http.go @@ -17,11 +17,13 @@ import ( "strings" "time" + "github.com/pkg/errors" "gopkg.in/macaron.v1" log "unknwon.dev/clog/v2" "gogs.io/gogs/internal/auth" "gogs.io/gogs/internal/conf" + "gogs.io/gogs/internal/context" "gogs.io/gogs/internal/db" "gogs.io/gogs/internal/lazyregexp" "gogs.io/gogs/internal/pathutil" @@ -130,29 +132,26 @@ func HTTPContexter() macaron.Handler { return } - // If username and password combination failed, try again using username as a token. + // If username and password combination failed, try again using either username + // or password as the token. if authUser == nil { - token, err := db.AccessTokens.GetBySHA1(c.Req.Context(), authUsername) - if err != nil { - if db.IsErrAccessTokenNotExist(err) { - askCredentials(c, http.StatusUnauthorized, "") - } else { - c.Status(http.StatusInternalServerError) - log.Error("Failed to get access token [sha: %s]: %v", authUsername, err) - } - return - } - if err = db.AccessTokens.Touch(c.Req.Context(), token.ID); err != nil { - log.Error("Failed to touch access token: %v", err) - } - - authUser, err = db.Users.GetByID(c.Req.Context(), token.UserID) - if err != nil { - // Once we found token, we're supposed to find its related user, - // thus any error is unexpected. + authUser, err = context.AuthenticateByToken(c.Req.Context(), authUsername) + if err != nil && !db.IsErrAccessTokenNotExist(errors.Cause(err)) { c.Status(http.StatusInternalServerError) - log.Error("Failed to get user [id: %d]: %v", token.UserID, err) + log.Error("Failed to authenticate by access token via username: %v", err) return + } else if db.IsErrAccessTokenNotExist(errors.Cause(err)) { + // Try again using the password field as the token. + authUser, err = context.AuthenticateByToken(c.Req.Context(), authPassword) + if err != nil { + if db.IsErrAccessTokenNotExist(errors.Cause(err)) { + askCredentials(c, http.StatusUnauthorized, "") + } else { + c.Status(http.StatusInternalServerError) + log.Error("Failed to authenticate by access token via password: %v", err) + } + return + } } } else if authUser.IsEnabledTwoFactor() { askCredentials(c, http.StatusUnauthorized, `User with two-factor authentication enabled cannot perform HTTP/HTTPS operations via plain username and password