aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYang Liu <50459973+ly4096x@users.noreply.github.com>2022-10-22 09:25:36 -0400
committerGitHub <noreply@github.com>2022-10-22 21:25:36 +0800
commitb9f5cfddc18c55eee62e1fb64a254fee9782c46c (patch)
tree01aca35598976c86a63243135ada1abc48275196
parentfd5874b07b73687f90e8cb7b171b67457364ebe9 (diff)
auth: enable authentication by token from password (#7198)
Co-authored-by: Joe Chen <jc@unknwon.io>
-rw-r--r--CHANGELOG.md1
-rw-r--r--internal/context/auth.go22
-rw-r--r--internal/db/access_tokens.go5
-rw-r--r--internal/route/lfs/route.go39
-rw-r--r--internal/route/lfs/route_test.go27
-rw-r--r--internal/route/repo/http.go39
6 files changed, 92 insertions, 41 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b480f91e..c6a71759 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 9cb37863..775a3dd8 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 7ed44ae9..58f77858 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 c00f7374..bdacc6da 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 dd8f5cde..eb0225d5 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 af51a076..89c7fa24 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