Extract AccessToken from Basic auth

This commit is contained in:
Lunny Xiao 2024-11-16 18:08:19 -08:00
parent 5ac876a728
commit a584ba7ed2
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
6 changed files with 128 additions and 27 deletions

View File

@ -115,6 +115,7 @@ func CommonRoutes() *web.Router {
verifyAuth(r, []auth.Method{ verifyAuth(r, []auth.Method{
&auth.OAuth2{}, &auth.OAuth2{},
&auth.AccessToken{},
&auth.Basic{}, &auth.Basic{},
&nuget.Auth{}, &nuget.Auth{},
&conan.Auth{}, &conan.Auth{},
@ -671,6 +672,7 @@ func ContainerRoutes() *web.Router {
r.Use(context.PackageContexter()) r.Use(context.PackageContexter())
verifyAuth(r, []auth.Method{ verifyAuth(r, []auth.Method{
&auth.AccessToken{},
&auth.Basic{}, &auth.Basic{},
&container.Auth{}, &container.Auth{},
}) })

View File

@ -739,6 +739,7 @@ func buildAuthGroup() *auth.Group {
group := auth.NewGroup( group := auth.NewGroup(
&auth.OAuth2{}, &auth.OAuth2{},
&auth.HTTPSign{}, &auth.HTTPSign{},
&auth.AccessToken{},
&auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API &auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API
) )
if setting.Service.EnableReverseProxyAuthAPI { if setting.Service.EnableReverseProxyAuthAPI {

View File

@ -99,7 +99,8 @@ func optionsCorsHandler() func(next http.Handler) http.Handler {
func buildAuthGroup() *auth_service.Group { func buildAuthGroup() *auth_service.Group {
group := auth_service.NewGroup() group := auth_service.NewGroup()
group.Add(&auth_service.OAuth2{}) // FIXME: this should be removed and only applied in download and oauth related routers group.Add(&auth_service.OAuth2{}) // FIXME: this should be removed and only applied in download and oauth related routers
group.Add(&auth_service.Basic{}) // FIXME: this should be removed and only applied in download and git/lfs routers group.Add(&auth_service.AccessToken{})
group.Add(&auth_service.Basic{}) // FIXME: this should be removed and only applied in download and git/lfs routers
if setting.Service.EnableReverseProxyAuth { if setting.Service.EnableReverseProxyAuth {
group.Add(&auth_service.ReverseProxy{}) // reverseproxy should before Session, otherwise the header will be ignored if user has login group.Add(&auth_service.ReverseProxy{}) // reverseproxy should before Session, otherwise the header will be ignored if user has login

View File

@ -0,0 +1,113 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"net/http"
"strings"
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web/middleware"
)
// Ensure the struct implements the interface.
var (
_ Method = &AccessToken{}
)
// BasicMethodName is the constant name of the basic authentication method
const (
AccessTokenMethodName = "access_token"
)
// AccessToken implements the Auth interface and authenticates requests (API requests
// only) by looking for access token
type AccessToken struct{}
// Name represents the name of auth method
func (b *AccessToken) Name() string {
return AccessTokenMethodName
}
// Match returns true if the request matched AccessToken requirements
// TODO: remove path check once AccessToken will not be a global middleware but only
// for specific routes
func (b *AccessToken) Match(req *http.Request) bool {
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
return false
}
baHead := req.Header.Get("Authorization")
if baHead == "" {
return false
}
auths := strings.SplitN(baHead, " ", 2)
if len(auths) != 2 || (strings.ToLower(auths[0]) != "basic") {
return false
}
return true
}
// Verify extracts and validates Basic data (username and password/token) from the
// "Authorization" header of the request and returns the corresponding user object for that
// name/token on successful validation.
// Returns nil if header is empty or validation fails.
func (b *AccessToken) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
// Basic authentication should only fire on API, Download or on Git or LFSPaths
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
return nil, nil
}
baHead := req.Header.Get("Authorization")
if len(baHead) == 0 {
return nil, nil
}
auths := strings.SplitN(baHead, " ", 2)
if len(auths) != 2 || (strings.ToLower(auths[0]) != "basic") {
return nil, nil
}
uname, passwd, _ := base.BasicAuthDecode(auths[1])
// Check if username or password is a token
isUsernameToken := len(passwd) == 0 || passwd == "x-oauth-basic"
// Assume username is token
authToken := uname
if !isUsernameToken {
log.Trace("Basic Authorization: Attempting login for: %s", uname)
// Assume password is token
authToken = passwd
} else {
log.Trace("Basic Authorization: Attempting login with username as token")
}
// check personal access token
token, err := auth_model.GetAccessTokenBySHA(req.Context(), authToken)
if err == nil {
log.Trace("Basic Authorization: Valid AccessToken for user[%d]", token.UID)
u, err := user_model.GetUserByID(req.Context(), token.UID)
if err != nil {
log.Error("GetUserByID: %v", err)
return nil, err
}
token.UpdatedUnix = timeutil.TimeStampNow()
if err = auth_model.UpdateAccessToken(req.Context(), token); err != nil {
log.Error("UpdateAccessToken: %v", err)
}
store.GetData()["LoginMethod"] = AccessTokenMethodName
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = token.Scope
return u, nil
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
log.Error("GetAccessTokenBySha: %v", err)
}
return nil, nil
}

View File

@ -14,7 +14,6 @@ import (
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
) )
@ -27,7 +26,6 @@ var (
// BasicMethodName is the constant name of the basic authentication method // BasicMethodName is the constant name of the basic authentication method
const ( const (
BasicMethodName = "basic" BasicMethodName = "basic"
AccessTokenMethodName = "access_token"
OAuth2TokenMethodName = "oauth2_token" OAuth2TokenMethodName = "oauth2_token"
ActionTokenMethodName = "action_token" ActionTokenMethodName = "action_token"
) )
@ -96,29 +94,6 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
return u, nil return u, nil
} }
// check personal access token
token, err := auth_model.GetAccessTokenBySHA(req.Context(), authToken)
if err == nil {
log.Trace("Basic Authorization: Valid AccessToken for user[%d]", uid)
u, err := user_model.GetUserByID(req.Context(), token.UID)
if err != nil {
log.Error("GetUserByID: %v", err)
return nil, err
}
token.UpdatedUnix = timeutil.TimeStampNow()
if err = auth_model.UpdateAccessToken(req.Context(), token); err != nil {
log.Error("UpdateAccessToken: %v", err)
}
store.GetData()["LoginMethod"] = AccessTokenMethodName
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = token.Scope
return u, nil
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
log.Error("GetAccessTokenBySha: %v", err)
}
// check task token // check task token
task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken) task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken)
if err == nil && task != nil { if err == nil && task != nil {

View File

@ -131,8 +131,17 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat
return t.UID return t.UID
} }
// Match returns true if the request matched OAuth2 requirements
// TODO: remove path check once AccessToken will not be a global middleware but only
// for specific routes
func (o *OAuth2) Match(req *http.Request) bool { func (o *OAuth2) Match(req *http.Request) bool {
return true if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) &&
!isGitRawOrAttachPath(req) && !isArchivePath(req) {
return false
}
_, ok := parseToken(req)
return ok
} }
// Verify extracts the user ID from the OAuth token in the query parameters // Verify extracts the user ID from the OAuth token in the query parameters