Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new method Match for auth #32532

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions routers/api/packages/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func CommonRoutes() *web.Router {

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

verifyAuth(r, []auth.Method{
&auth.AccessToken{},
&auth.Basic{},
&container.Auth{},
})
Expand Down
4 changes: 4 additions & 0 deletions routers/api/packages/chef/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ func (a *Auth) Name() string {
return "chef"
}

func (a *Auth) Match(req *http.Request) bool {
return true
}

// Verify extracts the user from the signed request
// If the request is signed with the user private key the user is verified.
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
Expand Down
4 changes: 4 additions & 0 deletions routers/api/packages/conan/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ func (a *Auth) Name() string {
return "conan"
}

func (a *Auth) Match(req *http.Request) bool {
return true
}

// Verify extracts the user from the Bearer token
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
packageMeta, err := packages.ParseAuthorizationRequest(req)
Expand Down
4 changes: 4 additions & 0 deletions routers/api/packages/container/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ func (a *Auth) Name() string {
return "container"
}

func (a *Auth) Match(req *http.Request) bool {
return true
}

// Verify extracts the user from the Bearer token
// If it's an anonymous session a ghost user is returned
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
Expand Down
4 changes: 4 additions & 0 deletions routers/api/packages/nuget/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ func (a *Auth) Name() string {
return "nuget"
}

func (a *Auth) Match(req *http.Request) bool {
return true
}

// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#request-parameters
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
token, err := auth_model.GetAccessTokenBySHA(req.Context(), req.Header.Get("X-NuGet-ApiKey"))
Expand Down
1 change: 1 addition & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,7 @@ func buildAuthGroup() *auth.Group {
group := auth.NewGroup(
&auth.OAuth2{},
&auth.HTTPSign{},
&auth.AccessToken{},
&auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API
)
if setting.Service.EnableReverseProxyAuthAPI {
Expand Down
3 changes: 2 additions & 1 deletion routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ func optionsCorsHandler() func(next http.Handler) http.Handler {
func buildAuthGroup() *auth_service.Group {
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.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 {
group.Add(&auth_service.ReverseProxy{}) // reverseproxy should before Session, otherwise the header will be ignored if user has login
Expand Down
113 changes: 113 additions & 0 deletions services/auth/access_token.go
Original file line number Diff line number Diff line change
@@ -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
}
29 changes: 4 additions & 25 deletions services/auth/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
)
Expand All @@ -27,7 +26,6 @@ var (
// BasicMethodName is the constant name of the basic authentication method
const (
BasicMethodName = "basic"
AccessTokenMethodName = "access_token"
OAuth2TokenMethodName = "oauth2_token"
ActionTokenMethodName = "action_token"
)
Expand All @@ -42,6 +40,10 @@ func (b *Basic) Name() string {
return BasicMethodName
}

func (b *Basic) Match(req *http.Request) bool {
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.
Expand Down Expand Up @@ -92,29 +94,6 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
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
task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken)
if err == nil && task != nil {
Expand Down
16 changes: 14 additions & 2 deletions services/auth/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,22 @@ func (b *Group) Name() string {
return strings.Join(names, ",")
}

func (b *Group) Match(req *http.Request) bool {
return true
}

func (b *Group) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
// Try to sign in with each of the enabled plugins
var retErr error
// find all methods that match the request
matchedMethods := make([]Method, 0, len(b.methods))
for _, m := range b.methods {
if m.Match(req) {
matchedMethods = append(matchedMethods, m)
}
}

var retErr error
// Try to sign in with each of the matched plugins
for _, m := range matchedMethods {
user, err := m.Verify(req, w, store, sess)
if err != nil {
if retErr == nil {
Expand Down
4 changes: 4 additions & 0 deletions services/auth/httpsign.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ func (h *HTTPSign) Name() string {
return "httpsign"
}

func (h *HTTPSign) Match(req *http.Request) bool {
return true
}

// Verify extracts and validates HTTPsign from the Signature header of the request and returns
// the corresponding user object on successful validation.
// Returns nil if header is empty or validation fails.
Expand Down
6 changes: 4 additions & 2 deletions services/auth/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ type Method interface {
// If verification is successful returns either an existing user object (with id > 0)
// or a new user object (with id = 0) populated with the information that was found
// in the authentication data (username or email).
// Second argument returns err if verification fails, otherwise
// Third argument returns err if verification fails, otherwise
// Second return argument returns true
// First return argument returns nil if no matched verification condition
Verify(http *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error)

// Match returns true if the request is a match for this method
Match(*http.Request) bool
Name() string
}

Expand Down
13 changes: 13 additions & 0 deletions services/auth/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat
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 {
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
// or the "Authorization" header and returns the corresponding user object for that ID.
// If verification is successful returns an existing user object.
Expand Down
4 changes: 4 additions & 0 deletions services/auth/reverseproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ func (r *ReverseProxy) getUserFromAuthEmail(req *http.Request) *user_model.User
return user
}

func (r *ReverseProxy) Match(req *http.Request) bool {
return true
}

// Verify attempts to load a user object based on headers sent by the reverse proxy.
// First it will attempt to load it based on the username (see docs for getUserFromAuthUser),
// and failing that it will attempt to load it based on the email (see docs for getUserFromAuthEmail).
Expand Down
4 changes: 4 additions & 0 deletions services/auth/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ func (s *Session) Name() string {
return "session"
}

func (s *Session) Match(req *http.Request) bool {
return true
}

// Verify checks if there is a user uid stored in the session and returns the user
// object for that uid.
// Returns nil if there is no user uid stored in the session.
Expand Down
4 changes: 4 additions & 0 deletions services/auth/sspi.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ func (s *SSPI) Name() string {
return "sspi"
}

func (s *SSPI) Match(req *http.Request) bool {
return true
}

// Verify uses SSPI (Windows implementation of SPNEGO) to authenticate the request.
// If authentication is successful, returns the corresponding user object.
// If negotiation should continue or authentication fails, immediately returns a 401 HTTP
Expand Down
Loading