Переглянути джерело

auth: enable authentication by token from password (#7198)

Co-authored-by: Joe Chen <[email protected]>
Yang Liu 2 роки тому
батько
коміт
b9f5cfddc1

+ 1 - 0
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)

+ 22 - 0
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
+}

+ 5 - 0
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

+ 19 - 20
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
+				}
 			}
 		}
 

+ 26 - 1
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) {

+ 19 - 20
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