diff --git a/.golangci.yml b/.golangci.yml index b16bb8beb2..5fa6c13860 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -347,21 +347,6 @@ linters: - path: services/actions/trust.go linters: - nilnil - - path: services/auth/basic.go - linters: - - nilnil - - path: services/auth/httpsign.go - linters: - - nilnil - - path: services/auth/oauth2.go - linters: - - nilnil - - path: services/auth/reverseproxy.go - linters: - - nilnil - - path: services/auth/session.go - linters: - - nilnil - path: services/contexttest/context_tests.go linters: - nilnil diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 0b46cc66b0..2163badb49 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -38,48 +38,46 @@ import ( "forgejo.org/routers/api/packages/swift" "forgejo.org/routers/api/packages/vagrant" "forgejo.org/services/auth" + auth_method "forgejo.org/services/auth/method" "forgejo.org/services/context" ) func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { return func(ctx *context.Context) { - if ctx.Data["IsApiToken"] == true { - scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) - if ok { // it's a personal access token but not oauth2 token - scopeMatched := false - var err error - switch accessMode { - case perm.AccessModeRead: - scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeReadPackage) - if err != nil { - ctx.Error(http.StatusInternalServerError, "HasScope", err.Error()) - return - } - case perm.AccessModeWrite: - scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeWritePackage) - if err != nil { - ctx.Error(http.StatusInternalServerError, "HasScope", err.Error()) - return - } - } - if !scopeMatched { - ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`) - ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") - return - } - - // check if scope only applies to public resources - publicOnly, err := scope.PublicOnly() + if hasScope, scope := ctx.Authentication.Scope().Get(); hasScope { + scopeMatched := false + var err error + switch accessMode { + case perm.AccessModeRead: + scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeReadPackage) if err != nil { - ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) + ctx.Error(http.StatusInternalServerError, "HasScope", err.Error()) return } + case perm.AccessModeWrite: + scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeWritePackage) + if err != nil { + ctx.Error(http.StatusInternalServerError, "HasScope", err.Error()) + return + } + } + if !scopeMatched { + ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`) + ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") + return + } - if publicOnly { - if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public packages") - return - } + // check if scope only applies to public resources + publicOnly, err := scope.PublicOnly() + if err != nil { + ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) + return + } + + if publicOnly { + if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() { + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public packages") + return } } } @@ -116,38 +114,48 @@ func enforcePackagesQuota() func(ctx *context.Context) { func verifyAuth(r *web.Route, authMethods []auth.Method) { if setting.Service.EnableReverseProxyAuth { - authMethods = append(authMethods, &auth.ReverseProxy{}) + authMethods = append(authMethods, &auth_method.ReverseProxy{}) } - authGroup := auth.NewGroup(authMethods...) + authGroup := auth_method.NewGroup(authMethods...) r.Use(func(ctx *context.Context) { - var err error - ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + authResult, err := authGroup.Verify(ctx.Req, ctx.Resp, ctx.Session) if err != nil { log.Info("Failed to verify user: %v", err) ctx.Error(http.StatusUnauthorized, "authGroup.Verify") return } + if authResult == nil { + ctx.Error(http.StatusInternalServerError, "verifyAuth nil authentication result") + return + } + ctx.Doer = authResult.User() ctx.IsSigned = ctx.Doer != nil + ctx.Authentication = authResult }) } func verifyContainerAuth(r *web.Route, authMethods []auth.Method) { if setting.Service.EnableReverseProxyAuth { - authMethods = append(authMethods, &auth.ReverseProxy{}) + authMethods = append(authMethods, &auth_method.ReverseProxy{}) } - authGroup := auth.NewGroup(authMethods...) + authGroup := auth_method.NewGroup(authMethods...) r.Use(func(ctx *context.Context) { - var err error - ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) + authResult, err := authGroup.Verify(ctx.Req, ctx.Resp, ctx.Session) if err != nil { log.Info("Failed to verify user: %v", err) container.APIUnauthorizedError(ctx) ctx.Error(http.StatusUnauthorized, "authGroup.Verify") return } + if authResult == nil { + ctx.Error(http.StatusInternalServerError, "verifyContainerAuth nil authentication result") + return + } + ctx.Doer = authResult.User() ctx.IsSigned = ctx.Doer != nil + ctx.Authentication = authResult }) } @@ -159,8 +167,8 @@ func CommonRoutes() *web.Route { r.Use(context.PackageContexter()) verifyAuth(r, []auth.Method{ - &auth.OAuth2{}, - &auth.Basic{}, + &auth_method.OAuth2{}, + &auth_method.Basic{}, &nuget.Auth{}, &conan.Auth{}, &chef.Auth{}, @@ -804,7 +812,7 @@ func ContainerRoutes() *web.Route { r.Use(context.PackageContexter()) - verifyContainerAuth(r, []auth.Method{&auth.Basic{}, &container.Auth{}}) + verifyContainerAuth(r, []auth.Method{&auth_method.Basic{}, &container.Auth{}}) r.Get("", container.ReqContainerAccess, container.DetermineSupport) r.Group("/token", func() { diff --git a/routers/api/packages/chef/auth.go b/routers/api/packages/chef/auth.go index 62523df02d..b9ff4b25f8 100644 --- a/routers/api/packages/chef/auth.go +++ b/routers/api/packages/chef/auth.go @@ -39,9 +39,19 @@ var ( versionPattern = regexp.MustCompile(`version=(\d+\.\d+)`) authorizationPattern = regexp.MustCompile(`\AX-Ops-Authorization-(\d+)`) - _ auth.Method = &Auth{} + _ auth.Method = &Auth{} + _ auth.AuthenticationResult = &chefAuthenticationResult{} ) +type chefAuthenticationResult struct { + *auth.BaseAuthenticationResult + user *user_model.User +} + +func (r *chefAuthenticationResult) User() *user_model.User { + return r.user +} + // Documentation: // https://docs.chef.io/server/api_chef_server/#required-headers // https://github.com/chef-boneyard/chef-rfc/blob/master/rfc065-sign-v1.3.md @@ -55,13 +65,13 @@ func (a *Auth) Name() string { // 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) { +func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) (auth.AuthenticationResult, error) { u, err := getUserFromRequest(req) if err != nil { return nil, err } if u == nil { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } pub, err := getUserPublicKey(req.Context(), u) @@ -82,7 +92,7 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS return nil, err } - return u, nil + return &chefAuthenticationResult{user: u}, nil } func getUserFromRequest(req *http.Request) (*user_model.User, error) { diff --git a/routers/api/packages/conan/auth.go b/routers/api/packages/conan/auth.go index 1f5af77304..df0283167f 100644 --- a/routers/api/packages/conan/auth.go +++ b/routers/api/packages/conan/auth.go @@ -6,13 +6,32 @@ package conan import ( "net/http" + auth_model "forgejo.org/models/auth" user_model "forgejo.org/models/user" "forgejo.org/modules/log" + "forgejo.org/modules/optional" "forgejo.org/services/auth" "forgejo.org/services/packages" ) -var _ auth.Method = &Auth{} +var ( + _ auth.Method = &Auth{} + _ auth.AuthenticationResult = &conanAuthenticationResult{} +) + +type conanAuthenticationResult struct { + *auth.BaseAuthenticationResult + user *user_model.User + scope optional.Option[auth_model.AccessTokenScope] +} + +func (r *conanAuthenticationResult) Scope() optional.Option[auth_model.AccessTokenScope] { + return r.scope +} + +func (r *conanAuthenticationResult) User() *user_model.User { + return r.user +} type Auth struct{} @@ -21,7 +40,7 @@ func (a *Auth) Name() string { } // 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) { +func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) (auth.AuthenticationResult, error) { uid, scope, err := packages.ParseAuthorizationToken(req) if err != nil { log.Trace("ParseAuthorizationToken: %v", err) @@ -29,13 +48,13 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS } if uid == 0 { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } // Propagate scope of the authorization token. + authScope := optional.None[auth_model.AccessTokenScope]() if scope != "" { - store.GetData()["IsApiToken"] = true - store.GetData()["ApiTokenScope"] = scope + authScope = optional.Some(scope) } u, err := user_model.GetUserByID(req.Context(), uid) @@ -44,5 +63,5 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS return nil, err } - return u, nil + return &conanAuthenticationResult{user: u, scope: authScope}, nil } diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go index 3a95a883c5..cc894aec2f 100644 --- a/routers/api/packages/conan/conan.go +++ b/routers/api/packages/conan/conan.go @@ -11,7 +11,6 @@ import ( "strings" "time" - auth_model "forgejo.org/models/auth" "forgejo.org/models/db" packages_model "forgejo.org/models/packages" conan_model "forgejo.org/models/packages/conan" @@ -119,7 +118,7 @@ func Authenticate(ctx *context.Context) { } // If there's an API scope, ensure it propagates. - scope, _ := ctx.Data.GetData()["ApiTokenScope"].(auth_model.AccessTokenScope) + scope := ctx.Authentication.Scope().ValueOrZeroValue() token, err := packages_service.CreateAuthorizationToken(ctx.Doer, scope) if err != nil { diff --git a/routers/api/packages/container/auth.go b/routers/api/packages/container/auth.go index 71c237326e..67310de948 100644 --- a/routers/api/packages/container/auth.go +++ b/routers/api/packages/container/auth.go @@ -6,13 +6,32 @@ package container import ( "net/http" + auth_model "forgejo.org/models/auth" user_model "forgejo.org/models/user" "forgejo.org/modules/log" + "forgejo.org/modules/optional" "forgejo.org/services/auth" "forgejo.org/services/packages" ) -var _ auth.Method = &Auth{} +var ( + _ auth.Method = &Auth{} + _ auth.AuthenticationResult = &containerAuthenticationResult{} +) + +type containerAuthenticationResult struct { + *auth.BaseAuthenticationResult + user *user_model.User + scope optional.Option[auth_model.AccessTokenScope] +} + +func (r *containerAuthenticationResult) Scope() optional.Option[auth_model.AccessTokenScope] { + return r.scope +} + +func (r *containerAuthenticationResult) User() *user_model.User { + return r.user +} type Auth struct{} @@ -22,7 +41,7 @@ func (a *Auth) Name() string { // 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) { +func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) (auth.AuthenticationResult, error) { uid, scope, err := packages.ParseAuthorizationToken(req) if err != nil { log.Trace("ParseAuthorizationToken: %v", err) @@ -30,13 +49,13 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS } if uid == 0 { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } // Propagate scope of the authorization token. + authScope := optional.None[auth_model.AccessTokenScope]() if scope != "" { - store.GetData()["IsApiToken"] = true - store.GetData()["ApiTokenScope"] = scope + authScope = optional.Some(scope) } u, err := user_model.GetPossibleUserByID(req.Context(), uid) @@ -45,5 +64,5 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS return nil, err } - return u, nil + return &containerAuthenticationResult{user: u, scope: authScope}, nil } diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 6781f511c8..eb81773b4c 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -13,7 +13,6 @@ import ( "regexp" "strconv" - auth_model "forgejo.org/models/auth" packages_model "forgejo.org/models/packages" container_model "forgejo.org/models/packages/container" user_model "forgejo.org/models/user" @@ -158,7 +157,7 @@ func Authenticate(ctx *context.Context) { } // If there's an API scope, ensure it propagates. - scope, _ := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) + scope := ctx.Authentication.Scope().ValueOrZeroValue() token, err := packages_service.CreateAuthorizationToken(u, scope) if err != nil { diff --git a/routers/api/packages/nuget/auth.go b/routers/api/packages/nuget/auth.go index 139f84a0c6..c33799734c 100644 --- a/routers/api/packages/nuget/auth.go +++ b/routers/api/packages/nuget/auth.go @@ -12,6 +12,20 @@ import ( "forgejo.org/services/auth" ) +var ( + _ auth.Method = &Auth{} + _ auth.AuthenticationResult = &nugetAuthenticationResult{} +) + +type nugetAuthenticationResult struct { + *auth.BaseAuthenticationResult + user *user_model.User +} + +func (r *nugetAuthenticationResult) User() *user_model.User { + return r.user +} + var _ auth.Method = &Auth{} type Auth struct{} @@ -21,14 +35,14 @@ func (a *Auth) Name() string { } // 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) { +func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) (auth.AuthenticationResult, error) { token, err := auth_model.GetAccessTokenBySHA(req.Context(), req.Header.Get("X-NuGet-ApiKey")) if err != nil { if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) { log.Error("GetAccessTokenBySHA: %v", err) return nil, err } - return nil, nil + return &auth.UnauthenticatedResult{}, nil } u, err := user_model.GetUserByID(req.Context(), token.UID) @@ -41,5 +55,5 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS log.Error("UpdateLastUsed: %v", err) } - return u, nil + return &nugetAuthenticationResult{user: u}, nil } diff --git a/routers/api/shared/middleware.go b/routers/api/shared/middleware.go index f657dae026..7e935970ec 100644 --- a/routers/api/shared/middleware.go +++ b/routers/api/shared/middleware.go @@ -4,6 +4,7 @@ package shared import ( + "errors" "fmt" "net/http" @@ -12,6 +13,7 @@ import ( "forgejo.org/modules/setting" "forgejo.org/routers/common" "forgejo.org/services/auth" + auth_method "forgejo.org/services/auth/method" "forgejo.org/services/authz" "forgejo.org/services/context" @@ -43,14 +45,14 @@ func Middlewares() (stack []any) { ) } -func buildAuthGroup() *auth.Group { - group := auth.NewGroup( - &auth.OAuth2{}, - &auth.HTTPSign{}, - &auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API +func buildAuthGroup() *auth_method.Group { + group := auth_method.NewGroup( + &auth_method.OAuth2{}, + &auth_method.HTTPSign{}, + &auth_method.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API ) if setting.Service.EnableReverseProxyAuthAPI { - group.Add(&auth.ReverseProxy{}) + group.Add(&auth_method.ReverseProxy{}) } return group @@ -63,15 +65,18 @@ func apiAuthentication(authMethod auth.Method) func(*context.APIContext) { ctx.Error(http.StatusUnauthorized, "APIAuth", err) return } - ctx.Doer = ar.Doer - ctx.IsSigned = ar.Doer != nil - ctx.IsBasicAuth = ar.IsBasicAuth + if ar == nil { + ctx.Error(http.StatusInternalServerError, "apiAuthentication nil authentication result", errors.New("nil authentication result")) + return + } + ctx.Doer = ar.User() + ctx.IsSigned = ctx.Doer != nil + ctx.Authentication = ar } } func apiAuthorization(ctx *context.APIContext) { - scope, scopeExists := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) - if scopeExists { + if hasScope, scope := ctx.Authentication.Scope().Get(); hasScope { publicOnly, err := scope.PublicOnly() if err != nil { ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) @@ -80,13 +85,13 @@ func apiAuthorization(ctx *context.APIContext) { ctx.PublicOnly = publicOnly } - reducer, reducerExists := ctx.Data["ApiTokenReducer"].(authz.AuthorizationReducer) - if reducerExists { + reducer := ctx.Authentication.Reducer() + if reducer != nil { ctx.Reducer = reducer } else { - // No "ApiTokenReducer" will be populated if the auth method wasn't an PAT. In this case, we populate - // `ctx.Reducer` so no nil checks are needed, and we respect the scope `PublicOnly()` so that it it's safe to - // just rely on `ctx.Reducer` to account for public-only access: + // No Reducer will be populated if the auth method wasn't an PAT. In this case, we populate `ctx.Reducer` so no + // nil checks are needed, and we respect the scope `PublicOnly()` so that it it's safe to just rely on + // `ctx.Reducer` to account for public-only access: if ctx.PublicOnly { ctx.Reducer = &authz.PublicReposAuthorizationReducer{} } else { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 715aa9003b..8910babc29 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -85,7 +85,6 @@ import ( "forgejo.org/routers/api/v1/settings" "forgejo.org/routers/api/v1/user" "forgejo.org/services/actions" - "forgejo.org/services/auth" "forgejo.org/services/context" "forgejo.org/services/forms" redirect_service "forgejo.org/services/redirect" @@ -180,8 +179,8 @@ func repoAssignment() func(ctx *context.APIContext) { repo.Owner = owner ctx.Repo.Repository = repo - if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID { - taskID := ctx.Data["ActionsTaskID"].(int64) + if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID && ctx.Authentication.ActionsTaskID().Has() { + _, taskID := ctx.Authentication.ActionsTaskID().Get() task, err := actions_model.GetTaskByID(ctx, taskID) if err != nil { ctx.Error(http.StatusInternalServerError, "actions_model.GetTaskByID", err) @@ -330,8 +329,8 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC } // Need OAuth2 token to be present. - scope, scopeExists := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) - if ctx.Data["IsApiToken"] != true || !scopeExists { + hasScope, scope := ctx.Authentication.Scope().Get() + if !hasScope { return } @@ -372,7 +371,7 @@ func tokenRequiresRepoOwnerScope(ctx *context.APIContext) { func reqToken() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { // If actions token is present - if true == ctx.Data["IsActionsToken"] { + if ctx.Authentication.ActionsTaskID().Has() { return } @@ -401,13 +400,13 @@ func reqUsersExploreEnabled() func(ctx *context.APIContext) { func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if ctx.IsSigned && setting.Service.EnableReverseProxyAuthAPI && ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName { + if ctx.IsSigned && setting.Service.EnableReverseProxyAuthAPI && ctx.Authentication.IsReverseProxyAuthentication() { return } // Require basic authorization method to be used and that basic // authorization used password login to verify the user. - if passwordLogin, ok := ctx.Data["IsPasswordLogin"].(bool); !ok || !passwordLogin { + if !ctx.Authentication.IsPasswordAuthentication() { ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth method not allowed") return } diff --git a/routers/common/auth.go b/routers/common/auth.go index d4b3b1fea7..014db93dad 100644 --- a/routers/common/auth.go +++ b/routers/common/auth.go @@ -4,32 +4,30 @@ package common import ( - user_model "forgejo.org/models/user" + "errors" + "forgejo.org/modules/web/middleware" auth_service "forgejo.org/services/auth" "forgejo.org/services/context" ) -type AuthResult struct { - Doer *user_model.User - IsBasicAuth bool -} - -func AuthShared(ctx *context.Base, sessionStore auth_service.SessionStore, authMethod auth_service.Method) (ar AuthResult, err error) { - ar.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, sessionStore) +func AuthShared(ctx *context.Base, sessionStore auth_service.SessionStore, authMethod auth_service.Method) (ar auth_service.AuthenticationResult, err error) { + ar, err = authMethod.Verify(ctx.Req, ctx.Resp, sessionStore) if err != nil { return ar, err } - if ar.Doer != nil { - if ctx.Locale.Language() != ar.Doer.Language { + if ar == nil { + return nil, errors.New("failure to retrieve AuthenticationResult - nil value") + } + doer := ar.User() + if doer != nil { + if ctx.Locale.Language() != doer.Language { ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req) } - ar.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth_service.BasicMethodName - ctx.Data["IsSigned"] = true - ctx.Data[middleware.ContextDataKeySignedUser] = ar.Doer - ctx.Data["SignedUserID"] = ar.Doer.ID - ctx.Data["IsAdmin"] = ar.Doer.IsAdmin + ctx.Data[middleware.ContextDataKeySignedUser] = doer + ctx.Data["SignedUserID"] = doer.ID + ctx.Data["IsAdmin"] = doer.IsAdmin } else { ctx.Data["IsSigned"] = false ctx.Data["SignedUserID"] = int64(0) diff --git a/routers/init.go b/routers/init.go index adea646672..26cf0ace3e 100644 --- a/routers/init.go +++ b/routers/init.go @@ -33,7 +33,7 @@ import ( "forgejo.org/routers/private" web_routers "forgejo.org/routers/web" actions_service "forgejo.org/services/actions" - "forgejo.org/services/auth" + auth_method "forgejo.org/services/auth/method" "forgejo.org/services/auth/source/oauth2" "forgejo.org/services/automerge" "forgejo.org/services/cron" @@ -160,7 +160,7 @@ func InitWebInstalled(ctx context.Context) { mustInitCtx(ctx, ssh.Init) - auth.Init() + auth_method.Init() mustInit(svg.Init) actions_service.Init() diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index e9312b3060..97b5ccbe6b 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -28,6 +28,7 @@ import ( "forgejo.org/modules/web" "forgejo.org/modules/web/middleware" auth_service "forgejo.org/services/auth" + auth_method "forgejo.org/services/auth/method" "forgejo.org/services/auth/source/oauth2" "forgejo.org/services/context" "forgejo.org/services/externalaccount" @@ -213,7 +214,7 @@ func SignInPost(ctx *context.Context) { } } - u, source, err := auth_service.UserSignIn(ctx, form.UserName, form.Password) + u, source, err := auth_method.UserSignIn(ctx, form.UserName, form.Password) if err != nil { if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form) diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index d2cb0d29c7..e212ec58b2 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -16,7 +16,7 @@ import ( "forgejo.org/modules/setting" "forgejo.org/modules/util" "forgejo.org/modules/web" - auth_service "forgejo.org/services/auth" + auth_method "forgejo.org/services/auth/method" "forgejo.org/services/auth/source/oauth2" "forgejo.org/services/context" "forgejo.org/services/externalaccount" @@ -128,7 +128,7 @@ func LinkAccountPostSignIn(ctx *context.Context) { return } - u, _, err := auth_service.UserSignIn(ctx, signInForm.UserName, signInForm.Password) + u, _, err := auth_method.UserSignIn(ctx, signInForm.UserName, signInForm.Password) if err != nil { handleSignInError(ctx, signInForm.UserName, &signInForm, tplLinkAccount, "UserLinkAccount", err) return diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index eba2b441c4..dae71dd377 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -33,7 +33,7 @@ import ( "forgejo.org/modules/util" "forgejo.org/modules/web" "forgejo.org/modules/web/middleware" - auth_service "forgejo.org/services/auth" + auth_method "forgejo.org/services/auth/method" source_service "forgejo.org/services/auth/source" "forgejo.org/services/auth/source/oauth2" "forgejo.org/services/context" @@ -294,7 +294,7 @@ func ifOnlyPublicGroups(scopes string) bool { // InfoOAuth manages request for userinfo endpoint func InfoOAuth(ctx *context.Context) { - if ctx.Doer == nil || ctx.Data["AuthedMethod"] != (&auth_service.OAuth2{}).Name() { + if ctx.Doer == nil || !ctx.Authentication.IsOAuth2JWTAuthentication() { ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`) ctx.PlainText(http.StatusUnauthorized, "no valid authorization") return @@ -316,7 +316,7 @@ func InfoOAuth(ctx *context.Context) { } } - _, grantScopes := auth_service.CheckOAuthAccessToken(ctx, token) + _, grantScopes := auth_method.CheckOAuthAccessToken(ctx, token) onlyPublicGroups := ifOnlyPublicGroups(grantScopes) groups, err := getOAuthGroupsForUser(ctx, ctx.Doer, onlyPublicGroups) diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index b2c4f3b68e..a15bf234c0 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -18,6 +18,7 @@ import ( "forgejo.org/modules/setting" "forgejo.org/modules/web" "forgejo.org/services/auth" + auth_method "forgejo.org/services/auth/method" "forgejo.org/services/context" "forgejo.org/services/forms" ) @@ -266,7 +267,7 @@ func ConnectOpenIDPost(ctx *context.Context) { ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp ctx.Data["OpenID"] = oid - u, source, err := auth.UserSignIn(ctx, form.UserName, form.Password) + u, source, err := auth_method.UserSignIn(ctx, form.UserName, form.Password) if err != nil { handleSignInError(ctx, form.UserName, &form, tplConnectOID, "ConnectOpenIDPost", err) return diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index de05d99a3e..936b108281 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -158,7 +158,7 @@ func httpBase(ctx *context.Context) *serviceHandler { return nil } - if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true { + if ctx.Authentication.IsPasswordAuthentication() { _, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID) if err == nil { // TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented @@ -187,8 +187,7 @@ func httpBase(ctx *context.Context) *serviceHandler { // Because of special ref "refs/for" .. , need delay write permission check accessMode = perm.AccessModeRead - if ctx.Data["IsActionsToken"] == true { - taskID := ctx.Data["ActionsTaskID"].(int64) + if hasTaskID, taskID := ctx.Authentication.ActionsTaskID().Get(); hasTaskID { task, err := actions_model.GetTaskByID(ctx, taskID) if err != nil { ctx.ServerError("GetTaskByID", err) diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 5b0e8b4970..b84ef51940 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -19,7 +19,7 @@ import ( "forgejo.org/modules/timeutil" "forgejo.org/modules/validation" "forgejo.org/modules/web" - "forgejo.org/services/auth" + auth_method "forgejo.org/services/auth/method" "forgejo.org/services/auth/source/db" "forgejo.org/services/auth/source/smtp" "forgejo.org/services/context" @@ -274,7 +274,7 @@ func DeleteAccount(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsAccount"] = true - if _, _, err := auth.UserSignIn(ctx, ctx.Doer.Name, ctx.FormString("password")); err != nil { + if _, _, err := auth_method.UserSignIn(ctx, ctx.Doer.Name, ctx.FormString("password")); err != nil { switch { case user_model.IsErrUserNotExist(err): loadAccountData(ctx) diff --git a/routers/web/web.go b/routers/web/web.go index 766c39fa57..492a578259 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -48,6 +48,7 @@ import ( user_setting "forgejo.org/routers/web/user/setting" "forgejo.org/routers/web/user/setting/security" auth_service "forgejo.org/services/auth" + auth_method "forgejo.org/services/auth/method" "forgejo.org/services/context" "forgejo.org/services/forms" "forgejo.org/services/lfs" @@ -104,15 +105,15 @@ func optionsCorsHandler() func(next http.Handler) http.Handler { // // The Session plugin is expected to be executed second, in order to skip authentication // for users that have already signed in. -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 +func buildAuthGroup() *auth_method.Group { + group := auth_method.NewGroup() + group.Add(&auth_method.OAuth2{}) // FIXME: this should be removed and only applied in download and oauth related routers + group.Add(&auth_method.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 + group.Add(&auth_method.ReverseProxy{}) // reverseproxy should before Session, otherwise the header will be ignored if user has login } - group.Add(&auth_service.Session{}) + group.Add(&auth_method.Session{}) return group } @@ -125,9 +126,13 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) { ctx.Error(http.StatusUnauthorized, ctx.Locale.TrString("auth.unauthorized_credentials", "https://codeberg.org/forgejo/forgejo/issues/2809")) return } - ctx.Doer = ar.Doer - ctx.IsSigned = ar.Doer != nil - ctx.IsBasicAuth = ar.IsBasicAuth + if ar == nil { + ctx.Error(http.StatusInternalServerError, "webAuth nil authentication result") + return + } + ctx.Doer = ar.User() + ctx.IsSigned = ar.User() != nil + ctx.Authentication = ar if ctx.Doer == nil { // ensure the session uid is deleted _ = ctx.Session.Delete("uid") diff --git a/services/auth/interface.go b/services/auth/interface.go index 12b04a7abf..73cbe5403f 100644 --- a/services/auth/interface.go +++ b/services/auth/interface.go @@ -7,9 +7,12 @@ import ( "context" "net/http" + auth_model "forgejo.org/models/auth" user_model "forgejo.org/models/user" + "forgejo.org/modules/optional" "forgejo.org/modules/session" "forgejo.org/modules/web/middleware" + "forgejo.org/services/authz" ) // DataStore represents a data store @@ -20,15 +23,11 @@ type SessionStore session.Store // Method represents an authentication method (plugin) for HTTP requests. type Method interface { - // Verify tries to verify the authentication data contained in the request. - // 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 - // 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) - - Name() string + // Verify tries to verify the authentication data contained in the request. If verification is successful returns an + // AuthenticationResult implementation with details about the authentication, or, may return an + // AnonymousAuthentication if the authentication method doesn't indicate that the request is authenticated. An error + // is only returned if a failure occurred while checking authentication. + Verify(http *http.Request, w http.ResponseWriter, sess SessionStore) (AuthenticationResult, error) } // PasswordAuthenticator represents a source of authentication @@ -45,3 +44,62 @@ type LocalTwoFASkipper interface { type SynchronizableSource interface { Sync(ctx context.Context, updateExisting bool) error } + +type AuthenticationResult interface { + // May return `nil` to represent an anonymous, unauthenticated user. + User() *user_model.User + + // Optional permission scope indicated by the authentication method + Scope() optional.Option[auth_model.AccessTokenScope] + // Optional authorization reducer indicated by the authentication method + Reducer() authz.AuthorizationReducer + + // Identifies if the authentication involved the user's password. If so, and the user has 2FA enabled, some + // restrictions may be applied. + IsPasswordAuthentication() bool + + // Identifies if the authentication was performed by a reverse proxy. + IsReverseProxyAuthentication() bool + + // Identifies specifically that the OAuth2 JWT authentication method was used. If so, some related OAuth2 API + // endpoints may be accessible that otherwise wouldn't be. + IsOAuth2JWTAuthentication() bool + + // If authenticated as an Actions task (using ${{ forgejo.token }}), then indicates the specific task that performed + // the authentication. + ActionsTaskID() optional.Option[int64] +} + +type BaseAuthenticationResult struct{} + +func (*BaseAuthenticationResult) IsOAuth2JWTAuthentication() bool { + return false +} + +func (*BaseAuthenticationResult) IsPasswordAuthentication() bool { + return false +} + +func (*BaseAuthenticationResult) IsReverseProxyAuthentication() bool { + return false +} + +func (*BaseAuthenticationResult) ActionsTaskID() optional.Option[int64] { + return optional.None[int64]() +} + +func (*BaseAuthenticationResult) Reducer() authz.AuthorizationReducer { + return nil +} + +func (*BaseAuthenticationResult) Scope() optional.Option[auth_model.AccessTokenScope] { + return optional.None[auth_model.AccessTokenScope]() +} + +type UnauthenticatedResult struct { + *BaseAuthenticationResult +} + +func (*UnauthenticatedResult) User() *user_model.User { + return nil +} diff --git a/services/auth/additional_scopes_test.go b/services/auth/method/additional_scopes_test.go similarity index 98% rename from services/auth/additional_scopes_test.go rename to services/auth/method/additional_scopes_test.go index 9ab4e6e61f..2ed8af4b03 100644 --- a/services/auth/additional_scopes_test.go +++ b/services/auth/method/additional_scopes_test.go @@ -1,4 +1,4 @@ -package auth +package method import ( "testing" diff --git a/services/auth/auth.go b/services/auth/method/auth.go similarity index 97% rename from services/auth/auth.go rename to services/auth/method/auth.go index 14d78cdbc0..b2e89d51e9 100644 --- a/services/auth/auth.go +++ b/services/auth/method/auth.go @@ -2,7 +2,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package auth +package method import ( "fmt" @@ -17,6 +17,7 @@ import ( "forgejo.org/modules/session" "forgejo.org/modules/setting" "forgejo.org/modules/web/middleware" + "forgejo.org/services/auth" user_service "forgejo.org/services/user" ) @@ -63,7 +64,7 @@ func isArchivePath(req *http.Request) bool { } // handleSignIn clears existing session variables and stores new ones for the specified user object -func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore, user *user_model.User) { +func handleSignIn(resp http.ResponseWriter, req *http.Request, sess auth.SessionStore, user *user_model.User) { // We need to regenerate the session... newSess, err := session.RegenerateSession(resp, req) if err != nil { diff --git a/services/auth/method/auth_result_accesstoken.go b/services/auth/method/auth_result_accesstoken.go new file mode 100644 index 0000000000..74b659c548 --- /dev/null +++ b/services/auth/method/auth_result_accesstoken.go @@ -0,0 +1,33 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package method + +import ( + auth_model "forgejo.org/models/auth" + user_model "forgejo.org/models/user" + "forgejo.org/modules/optional" + "forgejo.org/services/auth" + "forgejo.org/services/authz" +) + +var _ auth.AuthenticationResult = &accessTokenAuthenticationResult{} + +type accessTokenAuthenticationResult struct { + *auth.BaseAuthenticationResult + user *user_model.User + scope auth_model.AccessTokenScope + reducer authz.AuthorizationReducer +} + +func (r *accessTokenAuthenticationResult) User() *user_model.User { + return r.user +} + +func (r *accessTokenAuthenticationResult) Scope() optional.Option[auth_model.AccessTokenScope] { + return optional.Some(r.scope) +} + +func (r *accessTokenAuthenticationResult) Reducer() authz.AuthorizationReducer { + return r.reducer +} diff --git a/services/auth/method/auth_result_actionstask.go b/services/auth/method/auth_result_actionstask.go new file mode 100644 index 0000000000..b6a9dd1287 --- /dev/null +++ b/services/auth/method/auth_result_actionstask.go @@ -0,0 +1,31 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package method + +import ( + auth_model "forgejo.org/models/auth" + user_model "forgejo.org/models/user" + "forgejo.org/modules/optional" + "forgejo.org/services/auth" +) + +var _ auth.AuthenticationResult = &actionsTaskTokenAuthenticationResult{} + +type actionsTaskTokenAuthenticationResult struct { + *auth.BaseAuthenticationResult + user *user_model.User + taskID int64 +} + +func (r *actionsTaskTokenAuthenticationResult) Scope() optional.Option[auth_model.AccessTokenScope] { + return optional.None[auth_model.AccessTokenScope]() +} + +func (r *actionsTaskTokenAuthenticationResult) User() *user_model.User { + return r.user +} + +func (r *actionsTaskTokenAuthenticationResult) ActionsTaskID() optional.Option[int64] { + return optional.Some(r.taskID) +} diff --git a/services/auth/method/auth_result_basicpassword.go b/services/auth/method/auth_result_basicpassword.go new file mode 100644 index 0000000000..3364046cea --- /dev/null +++ b/services/auth/method/auth_result_basicpassword.go @@ -0,0 +1,24 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package method + +import ( + user_model "forgejo.org/models/user" + "forgejo.org/services/auth" +) + +var _ auth.AuthenticationResult = &basicPaswordAuthenticationResult{} + +type basicPaswordAuthenticationResult struct { + *auth.BaseAuthenticationResult + user *user_model.User +} + +func (*basicPaswordAuthenticationResult) IsPasswordAuthentication() bool { + return true +} + +func (r *basicPaswordAuthenticationResult) User() *user_model.User { + return r.user +} diff --git a/services/auth/method/auth_result_httpsign.go b/services/auth/method/auth_result_httpsign.go new file mode 100644 index 0000000000..eb6fef7a14 --- /dev/null +++ b/services/auth/method/auth_result_httpsign.go @@ -0,0 +1,20 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package method + +import ( + user_model "forgejo.org/models/user" + "forgejo.org/services/auth" +) + +var _ auth.AuthenticationResult = &httpSignAuthenticationResult{} + +type httpSignAuthenticationResult struct { + *auth.BaseAuthenticationResult + user *user_model.User +} + +func (r *httpSignAuthenticationResult) User() *user_model.User { + return r.user +} diff --git a/services/auth/method/auth_result_oauth.go b/services/auth/method/auth_result_oauth.go new file mode 100644 index 0000000000..ce451e6459 --- /dev/null +++ b/services/auth/method/auth_result_oauth.go @@ -0,0 +1,31 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package method + +import ( + auth_model "forgejo.org/models/auth" + user_model "forgejo.org/models/user" + "forgejo.org/modules/optional" + "forgejo.org/services/auth" +) + +var _ auth.AuthenticationResult = &oAuth2JWTAuthenticationResult{} + +type oAuth2JWTAuthenticationResult struct { + *auth.BaseAuthenticationResult + user *user_model.User + scope optional.Option[auth_model.AccessTokenScope] +} + +func (*oAuth2JWTAuthenticationResult) IsOAuth2JWTAuthentication() bool { + return true +} + +func (r *oAuth2JWTAuthenticationResult) User() *user_model.User { + return r.user +} + +func (r *oAuth2JWTAuthenticationResult) Scope() optional.Option[auth_model.AccessTokenScope] { + return r.scope +} diff --git a/services/auth/method/auth_result_reverseproxy.go b/services/auth/method/auth_result_reverseproxy.go new file mode 100644 index 0000000000..cf575330e6 --- /dev/null +++ b/services/auth/method/auth_result_reverseproxy.go @@ -0,0 +1,24 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package method + +import ( + user_model "forgejo.org/models/user" + "forgejo.org/services/auth" +) + +var _ auth.AuthenticationResult = &reverseProxyAuthenticationResult{} + +type reverseProxyAuthenticationResult struct { + *auth.BaseAuthenticationResult + user *user_model.User +} + +func (r *reverseProxyAuthenticationResult) User() *user_model.User { + return r.user +} + +func (*reverseProxyAuthenticationResult) IsReverseProxyAuthentication() bool { + return true +} diff --git a/services/auth/method/auth_result_session.go b/services/auth/method/auth_result_session.go new file mode 100644 index 0000000000..71c591dacd --- /dev/null +++ b/services/auth/method/auth_result_session.go @@ -0,0 +1,20 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package method + +import ( + user_model "forgejo.org/models/user" + "forgejo.org/services/auth" +) + +var _ auth.AuthenticationResult = &sessionAuthenticationResult{} + +type sessionAuthenticationResult struct { + *auth.BaseAuthenticationResult + user *user_model.User +} + +func (r *sessionAuthenticationResult) User() *user_model.User { + return r.user +} diff --git a/services/auth/auth_test.go b/services/auth/method/auth_test.go similarity index 99% rename from services/auth/auth_test.go rename to services/auth/method/auth_test.go index 82308402d2..6e686feab8 100644 --- a/services/auth/auth_test.go +++ b/services/auth/method/auth_test.go @@ -2,7 +2,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package auth +package method import ( "net/http" diff --git a/services/auth/basic.go b/services/auth/method/basic.go similarity index 81% rename from services/auth/basic.go rename to services/auth/method/basic.go index 502d806a12..5b551135be 100644 --- a/services/auth/basic.go +++ b/services/auth/method/basic.go @@ -2,7 +2,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package auth +package method import ( "errors" @@ -14,48 +14,42 @@ import ( user_model "forgejo.org/models/user" "forgejo.org/modules/base" "forgejo.org/modules/log" + "forgejo.org/modules/optional" "forgejo.org/modules/setting" "forgejo.org/modules/util" "forgejo.org/modules/web/middleware" + "forgejo.org/services/auth" "forgejo.org/services/authz" ) // Ensure the struct implements the interface. var ( - _ Method = &Basic{} + _ auth.Method = &Basic{} ) -// BasicMethodName is the constant name of the basic authentication method -const BasicMethodName = "basic" - // Basic implements the Auth interface and authenticates requests (API requests // only) by looking for Basic authentication data or "x-oauth-basic" token in the "Authorization" // header. type Basic struct{} -// Name represents the name of auth method -func (b *Basic) Name() string { - return BasicMethodName -} - // 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 *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { +func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, _ auth.SessionStore) (auth.AuthenticationResult, 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 + return &auth.UnauthenticatedResult{}, nil } baHead := req.Header.Get("Authorization") if len(baHead) == 0 { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } auths := strings.SplitN(baHead, " ", 2) if len(auths) != 2 || (strings.ToLower(auths[0]) != "basic") { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } uname, passwd, _ := base.BasicAuthDecode(auths[1]) @@ -83,13 +77,13 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore return nil, err } - store.GetData()["IsApiToken"] = true + var scope auth_model.AccessTokenScope if grantScopes != "" { - store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScope(grantScopes) + scope = auth_model.AccessTokenScope(grantScopes) } else { - store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all + scope = auth_model.AccessTokenScopeAll // fallback to all } - return u, nil + return &oAuth2JWTAuthenticationResult{user: u, scope: optional.Some(scope)}, nil } // check personal access token @@ -106,17 +100,13 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore log.Error("UpdateLastUsed: %v", err) } - store.GetData()["IsApiToken"] = true - store.GetData()["ApiTokenScope"] = token.Scope - reducer, err := authz.GetAuthorizationReducerForAccessToken(req.Context(), token) if err != nil { log.Error("authz.GetAuthorizationReducerForAccessToken: %v", err) return nil, err } - store.GetData()["ApiTokenReducer"] = reducer - return u, nil + return &accessTokenAuthenticationResult{user: u, scope: token.Scope, reducer: reducer}, nil } else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) { log.Error("GetAccessTokenBySha: %v", err) } @@ -125,15 +115,11 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken) if err == nil && task != nil { log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID) - - store.GetData()["IsActionsToken"] = true - store.GetData()["ActionsTaskID"] = task.ID - - return user_model.NewActionsUser(), nil + return &actionsTaskTokenAuthenticationResult{user: user_model.NewActionsUser(), taskID: task.ID}, nil } if !setting.Service.EnableBasicAuth { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } log.Trace("Basic Authorization: Attempting SignIn for %s", uname) @@ -155,7 +141,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore return nil, errors.New("Basic authorization is not allowed while having security keys enrolled") } - if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() { + if skipper, ok := source.Cfg.(auth.LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() { if err := validateTOTP(req, u); err != nil { return nil, err } @@ -163,8 +149,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore log.Trace("Basic Authorization: Logged in user %-v", u) - store.GetData()["IsPasswordLogin"] = true - return u, nil + return &basicPaswordAuthenticationResult{user: u}, nil } func getOtpHeader(header http.Header) string { diff --git a/services/auth/group.go b/services/auth/method/group.go similarity index 52% rename from services/auth/group.go rename to services/auth/method/group.go index b713301b50..a87f5d6a75 100644 --- a/services/auth/group.go +++ b/services/auth/method/group.go @@ -1,51 +1,41 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package auth +package method import ( "net/http" - "strings" - user_model "forgejo.org/models/user" + "forgejo.org/services/auth" ) // Ensure the struct implements the interface. var ( - _ Method = &Group{} + _ auth.Method = &Group{} ) // Group implements the Auth interface with serval Auth. type Group struct { - methods []Method + methods []auth.Method } // NewGroup creates a new auth group -func NewGroup(methods ...Method) *Group { +func NewGroup(methods ...auth.Method) *Group { return &Group{ methods: methods, } } // Add adds a new method to group -func (b *Group) Add(method Method) { +func (b *Group) Add(method auth.Method) { b.methods = append(b.methods, method) } -// Name returns group's methods name -func (b *Group) Name() string { - names := make([]string, 0, len(b.methods)) - for _, m := range b.methods { - names = append(names, m.Name()) - } - return strings.Join(names, ",") -} - -func (b *Group) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { +func (b *Group) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) (auth.AuthenticationResult, error) { // Try to sign in with each of the enabled plugins var retErr error for _, m := range b.methods { - user, err := m.Verify(req, w, store, sess) + authResult, err := m.Verify(req, w, sess) if err != nil { if retErr == nil { retErr = err @@ -57,16 +47,17 @@ func (b *Group) Verify(req *http.Request, w http.ResponseWriter, store DataStore continue } - // If any method returns a user, we can stop trying. - // Return the user and ignore any error returned by previous methods. - if user != nil { - if store.GetData()["AuthedMethod"] == nil { - store.GetData()["AuthedMethod"] = m.Name() - } - return user, nil + // If any method returns an authenticated result, we can stop trying. Return and ignore any error returned by + // previous methods. + if authResult.User() != nil { + return authResult, nil } } - // If no method returns a user, return the error returned by the first method. - return nil, retErr + if retErr != nil { + // If no method returns a user, return the error returned by the first method. + return nil, retErr + } + + return &auth.UnauthenticatedResult{}, nil } diff --git a/services/auth/httpsign.go b/services/auth/method/httpsign.go similarity index 94% rename from services/auth/httpsign.go rename to services/auth/method/httpsign.go index e776ccbbed..a5328bddcd 100644 --- a/services/auth/httpsign.go +++ b/services/auth/method/httpsign.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package auth +package method import ( "bytes" @@ -16,6 +16,7 @@ import ( user_model "forgejo.org/models/user" "forgejo.org/modules/log" "forgejo.org/modules/setting" + "forgejo.org/services/auth" "github.com/42wim/httpsig" "golang.org/x/crypto/ssh" @@ -23,7 +24,7 @@ import ( // Ensure the struct implements the interface. var ( - _ Method = &HTTPSign{} + _ auth.Method = &HTTPSign{} ) // HTTPSign implements the Auth interface and authenticates requests (API requests @@ -31,18 +32,13 @@ var ( // more information can be found on https://github.com/go-fed/httpsig type HTTPSign struct{} -// Name represents the name of auth method -func (h *HTTPSign) Name() string { - return "httpsign" -} - // 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. -func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { +func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, _ auth.SessionStore) (auth.AuthenticationResult, error) { sigHead := req.Header.Get("Signature") if len(sigHead) == 0 { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } var ( @@ -53,14 +49,14 @@ func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataSt if len(req.Header.Get("X-Ssh-Certificate")) != 0 { // Handle Signature signed by SSH certificates if len(setting.SSH.TrustedUserCAKeys) == 0 { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } publicKey, err = VerifyCert(req) if err != nil { log.Debug("VerifyCert on request from %s: failed: %v", req.RemoteAddr, err) log.Warn("Failed authentication attempt from %s", req.RemoteAddr) - return nil, nil + return &auth.UnauthenticatedResult{}, nil } } else { // Handle Signature signed by Public Key @@ -68,7 +64,7 @@ func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataSt if err != nil { log.Debug("VerifyPubKey on request from %s: failed: %v", req.RemoteAddr, err) log.Warn("Failed authentication attempt from %s", req.RemoteAddr) - return nil, nil + return &auth.UnauthenticatedResult{}, nil } } @@ -78,11 +74,8 @@ func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, store DataSt return nil, err } - store.GetData()["IsApiToken"] = true - log.Trace("HTTP Sign: Logged in user %-v", u) - - return u, nil + return &httpSignAuthenticationResult{user: u}, nil } func VerifyPubKey(r *http.Request) (*asymkey_model.PublicKey, error) { diff --git a/services/auth/method/main_test.go b/services/auth/method/main_test.go new file mode 100644 index 0000000000..b5334990c3 --- /dev/null +++ b/services/auth/method/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package method + +import ( + "testing" + + "forgejo.org/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/services/auth/oauth2.go b/services/auth/method/oauth2.go similarity index 76% rename from services/auth/oauth2.go rename to services/auth/method/oauth2.go index a23f586ffd..e80c4bc35c 100644 --- a/services/auth/oauth2.go +++ b/services/auth/method/oauth2.go @@ -2,10 +2,11 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package auth +package method import ( "context" + "errors" "net/http" "slices" "strings" @@ -15,17 +16,19 @@ import ( auth_model "forgejo.org/models/auth" user_model "forgejo.org/models/user" "forgejo.org/modules/log" + "forgejo.org/modules/optional" "forgejo.org/modules/setting" "forgejo.org/modules/util" "forgejo.org/modules/web/middleware" "forgejo.org/services/actions" + "forgejo.org/services/auth" "forgejo.org/services/auth/source/oauth2" "forgejo.org/services/authz" ) // Ensure the struct implements the interface. var ( - _ Method = &OAuth2{} + _ auth.Method = &OAuth2{} ) // grantAdditionalScopes returns valid scopes coming from grant @@ -113,11 +116,6 @@ func CheckTaskIsRunning(ctx context.Context, taskID int64) bool { // "Authorization" header. type OAuth2 struct{} -// Name represents the name of auth method -func (o *OAuth2) Name() string { - return "oauth2" -} - // parseToken returns the token from request, and a boolean value // representing whether the token exists or not func parseToken(req *http.Request) (string, bool) { @@ -148,33 +146,39 @@ func parseToken(req *http.Request) (string, bool) { // userIDFromToken returns the user id corresponding to the OAuth token. // It will set 'IsApiToken' to true if the token is an API token and // set 'ApiTokenScope' to the scope of the access token -func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) (int64, error) { +func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string) (auth.AuthenticationResult, error) { if tokenSHA == "" { - return 0, auth_model.ErrAccessTokenEmpty{} + return nil, auth_model.ErrAccessTokenEmpty{} } // Let's see if token is valid. if strings.Contains(tokenSHA, ".") { // First attempt to decode an actions JWT, returning the actions user if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil { if CheckTaskIsRunning(ctx, taskID) { - store.GetData()["IsActionsToken"] = true - store.GetData()["ActionsTaskID"] = taskID - return user_model.ActionsUserID, nil + return &actionsTaskTokenAuthenticationResult{user: user_model.NewActionsUser(), taskID: taskID}, nil } } // Otherwise, check if this is an OAuth access token uid, grantScopes := CheckOAuthAccessToken(ctx, tokenSHA) + var accessTokenScope optional.Option[auth_model.AccessTokenScope] if uid != 0 { - store.GetData()["IsApiToken"] = true if grantScopes != "" { - store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScope(grantScopes) + accessTokenScope = optional.Some(auth_model.AccessTokenScope(grantScopes)) } else { - store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScopeAll // fallback to all + accessTokenScope = optional.Some(auth_model.AccessTokenScopeAll) // fallback to all } } - return uid, nil + user, err := user_model.GetPossibleUserByID(ctx, uid) + if err != nil { + if !user_model.IsErrUserNotExist(err) { + log.Error("GetUserByName: %v", err) + } + return nil, err + } + return &oAuth2JWTAuthenticationResult{user: user, scope: accessTokenScope}, nil } + t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA) if err != nil { if auth_model.IsErrAccessTokenNotExist(err) { @@ -182,68 +186,63 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA) if err == nil && task != nil { log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID) - - store.GetData()["IsActionsToken"] = true - store.GetData()["ActionsTaskID"] = task.ID - - return user_model.ActionsUserID, nil + return &actionsTaskTokenAuthenticationResult{user: user_model.NewActionsUser(), taskID: task.ID}, nil } } else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) { log.Error("GetAccessTokenBySHA: %v", err) + return nil, err } - return 0, err + return nil, err } if err := t.UpdateLastUsed(ctx); err != nil { log.Error("UpdateLastUsed: %v", err) } if t.UID == 0 { - return 0, auth_model.ErrAccessTokenNotExist{} + return nil, auth_model.ErrAccessTokenNotExist{} } - store.GetData()["IsApiToken"] = true - store.GetData()["ApiTokenScope"] = t.Scope reducer, err := authz.GetAuthorizationReducerForAccessToken(ctx, t) if err != nil { log.Error("authz.GetAuthorizationReducerForAccessToken: %v", err) - return 0, err + return nil, err } - store.GetData()["ApiTokenReducer"] = reducer - return t.UID, nil + u, err := user_model.GetUserByID(ctx, t.UID) + if err != nil { + log.Error("GetUserByID: %v", err) + return nil, err + } + + return &accessTokenAuthenticationResult{user: u, scope: t.Scope, reducer: reducer}, nil } // 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. // Returns nil if verification fails. -func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { +func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, _ auth.SessionStore) (auth.AuthenticationResult, error) { // These paths are not API paths, but we still want to check for tokens because they maybe in the API returned URLs if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) && !isGitRawOrAttachPath(req) && !isArchivePath(req) { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } token, ok := parseToken(req) if !ok { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } - id, err := o.userIDFromToken(req.Context(), token, store) + auth, err := o.userIDFromToken(req.Context(), token) if err != nil { return nil, err + } else if auth.User() == nil { + // Having successfully found a token, it's now expected that the only valid outcomes are either errors that + // result in 401s (if the token wasn't valid), or valid authentication as a user. If we reach here, we've missed + // those expected outcomes and somehow returned an unauthenticated response even though a token was provided. + return nil, errors.New("unexpected unauthenticated result from userIDFromToken") } - log.Trace("OAuth2 Authorization: Found token for user[%d]", id) - - user, err := user_model.GetPossibleUserByID(req.Context(), id) - if err != nil { - if !user_model.IsErrUserNotExist(err) { - log.Error("GetUserByName: %v", err) - } - return nil, err - } - - log.Trace("OAuth2 Authorization: Logged in user %-v", user) - return user, nil + log.Trace("OAuth2 Authorization: Logged in user %-v", auth.User()) + return auth, nil } func isAuthenticatedTokenRequest(req *http.Request) bool { diff --git a/services/auth/oauth2_test.go b/services/auth/method/oauth2_test.go similarity index 87% rename from services/auth/oauth2_test.go rename to services/auth/method/oauth2_test.go index 67e42c3c56..c08ec57151 100644 --- a/services/auth/oauth2_test.go +++ b/services/auth/method/oauth2_test.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package auth +package method import ( "net/http" @@ -11,7 +11,6 @@ import ( "forgejo.org/models/auth" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" - "forgejo.org/modules/web/middleware" "forgejo.org/services/actions" "github.com/stretchr/testify/assert" @@ -33,14 +32,13 @@ func TestUserIDFromToken(t *testing.T) { token, err := actions.CreateAuthorizationToken(task, map[string]any{}, false) require.NoError(t, err) - ds := make(middleware.ContextData) - o := OAuth2{} - uid, err := o.userIDFromToken(t.Context(), token, ds) + authResult, err := o.userIDFromToken(t.Context(), token) require.NoError(t, err) - assert.Equal(t, int64(user_model.ActionsUserID), uid) - assert.Equal(t, true, ds["IsActionsToken"]) - assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID)) + assert.Equal(t, int64(user_model.ActionsUserID), authResult.User().ID) + isActionsToken, authTaskID := authResult.ActionsTaskID().Get() + assert.True(t, isActionsToken) + assert.Equal(t, int64(RunningTaskID), authTaskID) }) t.Run("Actions error-JWT", func(t *testing.T) { @@ -52,13 +50,12 @@ func TestUserIDFromToken(t *testing.T) { "To short": {"abc", auth.ErrAccessTokenNotExist{Token: "abc"}}, } - ds := make(middleware.ContextData) o := OAuth2{} for name, c := range cases { t.Run(name, func(t *testing.T) { - uid, err := o.userIDFromToken(t.Context(), c.Token, ds) + authResult, err := o.userIDFromToken(t.Context(), c.Token) require.ErrorIs(t, err, c.Error) - assert.Equal(t, int64(0), uid) + assert.Nil(t, authResult) }) } }) diff --git a/services/auth/reverseproxy.go b/services/auth/method/reverseproxy.go similarity index 93% rename from services/auth/reverseproxy.go rename to services/auth/method/reverseproxy.go index eb9ceb8cf2..df6e626bcb 100644 --- a/services/auth/reverseproxy.go +++ b/services/auth/method/reverseproxy.go @@ -2,9 +2,10 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package auth +package method import ( + "errors" "net/http" "strings" @@ -12,14 +13,16 @@ import ( "forgejo.org/modules/log" "forgejo.org/modules/optional" "forgejo.org/modules/setting" + "forgejo.org/modules/util" "forgejo.org/modules/web/middleware" + "forgejo.org/services/auth" gouuid "github.com/google/uuid" ) // Ensure the struct implements the interface. var ( - _ Method = &ReverseProxy{} + _ auth.Method = &ReverseProxy{} ) // ReverseProxyMethodName is the constant name of the ReverseProxy authentication method @@ -37,11 +40,6 @@ func (r *ReverseProxy) getUserName(req *http.Request) string { return strings.TrimSpace(req.Header.Get(setting.ReverseProxyAuthUser)) } -// Name represents the name of auth method -func (r *ReverseProxy) Name() string { - return ReverseProxyMethodName -} - // getUserFromAuthUser extracts the username from the "setting.ReverseProxyAuthUser" header // of the request and returns the corresponding user object for that name. // Verification of header data is not performed as it should have already been done by @@ -52,7 +50,7 @@ func (r *ReverseProxy) Name() string { func (r *ReverseProxy) getUserFromAuthUser(req *http.Request) (*user_model.User, error) { username := r.getUserName(req) if len(username) == 0 { - return nil, nil + return nil, util.ErrNotExist } log.Trace("ReverseProxy Authorization: Found username: %s", username) @@ -104,15 +102,15 @@ func (r *ReverseProxy) getUserFromAuthEmail(req *http.Request) *user_model.User // 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). // Returns nil if the headers are empty or the user is not found. -func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { +func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) (auth.AuthenticationResult, error) { user, err := r.getUserFromAuthUser(req) - if err != nil { + if err != nil && !errors.Is(err, util.ErrNotExist) { return nil, err } if user == nil { user = r.getUserFromAuthEmail(req) if user == nil { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } } @@ -122,10 +120,9 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, store Da handleSignIn(w, req, sess, user) } } - store.GetData()["IsReverseProxy"] = true log.Trace("ReverseProxy Authorization: Logged in user %-v", user) - return user, nil + return &reverseProxyAuthenticationResult{user: user}, nil } // isAutoRegisterAllowed checks if EnableReverseProxyAutoRegister setting is true diff --git a/services/auth/reverseproxy_test.go b/services/auth/method/reverseproxy_test.go similarity index 99% rename from services/auth/reverseproxy_test.go rename to services/auth/method/reverseproxy_test.go index f6740a0134..fbcd412da5 100644 --- a/services/auth/reverseproxy_test.go +++ b/services/auth/method/reverseproxy_test.go @@ -1,7 +1,7 @@ // Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT -package auth +package method import ( "net/http" diff --git a/services/auth/session.go b/services/auth/method/session.go similarity index 77% rename from services/auth/session.go rename to services/auth/method/session.go index a15c24c940..540d544aa0 100644 --- a/services/auth/session.go +++ b/services/auth/method/session.go @@ -1,47 +1,43 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package auth +package method import ( "net/http" user_model "forgejo.org/models/user" "forgejo.org/modules/log" + "forgejo.org/services/auth" ) // Ensure the struct implements the interface. var ( - _ Method = &Session{} + _ auth.Method = &Session{} ) // Session checks if there is a user uid stored in the session and returns the user // object for that uid. type Session struct{} -// Name represents the name of auth method -func (s *Session) Name() string { - return "session" -} - // 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. -func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { +func (s *Session) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) (auth.AuthenticationResult, error) { if sess == nil { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } // Get user ID uid := sess.Get("uid") if uid == nil { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } log.Trace("Session Authorization: Found user[%d]", uid) id, ok := uid.(int64) if !ok { - return nil, nil + return &auth.UnauthenticatedResult{}, nil } // Get user object @@ -52,9 +48,9 @@ func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataSto // Return the err as-is to keep current signed-in session, in case the err is something like context.Canceled. Otherwise non-existing user (nil, nil) will make the caller clear the signed-in session. return nil, err } - return nil, nil + return &auth.UnauthenticatedResult{}, nil } log.Trace("Session Authorization: Logged in user %-v", user) - return user, nil + return &sessionAuthenticationResult{user: user}, nil } diff --git a/services/auth/signin.go b/services/auth/method/signin.go similarity index 94% rename from services/auth/signin.go rename to services/auth/method/signin.go index 495b3d387e..d664b16a8c 100644 --- a/services/auth/signin.go +++ b/services/auth/method/signin.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package auth +package method import ( "context" @@ -12,6 +12,7 @@ import ( user_model "forgejo.org/models/user" "forgejo.org/modules/log" "forgejo.org/modules/optional" + auth_service "forgejo.org/services/auth" "forgejo.org/services/auth/source/oauth2" "forgejo.org/services/auth/source/smtp" @@ -65,7 +66,7 @@ func UserSignIn(ctx context.Context, username, password string) (*user_model.Use return nil, nil, oauth2.ErrAuthSourceNotActivated } - authenticator, ok := source.Cfg.(PasswordAuthenticator) + authenticator, ok := source.Cfg.(auth_service.PasswordAuthenticator) if !ok { return nil, nil, smtp.ErrUnsupportedLoginType } @@ -98,7 +99,7 @@ func UserSignIn(ctx context.Context, username, password string) (*user_model.Use continue } - authenticator, ok := source.Cfg.(PasswordAuthenticator) + authenticator, ok := source.Cfg.(auth_service.PasswordAuthenticator) if !ok { continue } diff --git a/services/context/api.go b/services/context/api.go index 1064b1ab4a..a6af94dfde 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -25,6 +25,7 @@ import ( "forgejo.org/modules/setting" "forgejo.org/modules/web" web_types "forgejo.org/modules/web/types" + "forgejo.org/services/auth" "forgejo.org/services/authz" "code.forgejo.org/go-chi/cache" @@ -36,9 +37,9 @@ type APIContext struct { Cache cache.Cache - Doer *user_model.User // current signed-in user - IsSigned bool - IsBasicAuth bool + Doer *user_model.User // current signed-in user + IsSigned bool + Authentication auth.AuthenticationResult ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer diff --git a/services/context/context.go b/services/context/context.go index 8ced686aac..af5ec26143 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -25,6 +25,7 @@ import ( "forgejo.org/modules/web" "forgejo.org/modules/web/middleware" web_types "forgejo.org/modules/web/types" + "forgejo.org/services/auth" "code.forgejo.org/go-chi/cache" "code.forgejo.org/go-chi/session" @@ -51,9 +52,9 @@ type Context struct { Link string // current request URL (without query string) - Doer *user_model.User // current signed-in user - IsSigned bool - IsBasicAuth bool + Doer *user_model.User // current signed-in user + IsSigned bool + Authentication auth.AuthenticationResult ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer @@ -112,6 +113,8 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context { Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"), Repo: &Repository{PullRequest: &PullRequest{}}, Org: &Organization{}, + + Authentication: &auth.UnauthenticatedResult{}, } ctx.TemplateContext = NewTemplateContextForWeb(ctx) ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}} diff --git a/services/context/permission.go b/services/context/permission.go index f898bd98ae..44adbbd6d9 100644 --- a/services/context/permission.go +++ b/services/context/permission.go @@ -12,7 +12,6 @@ import ( repo_model "forgejo.org/models/repo" "forgejo.org/models/unit" "forgejo.org/modules/log" - "forgejo.org/services/authz" ) // RequireRepoAdmin returns a middleware for requiring repository admin permission @@ -125,12 +124,7 @@ func CheckRepoDelegateActionTrust(ctx *Context) bool { // CheckRepoScopedToken check whether personal access token has repo scope func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) { - if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true { - return - } - - scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) - if ok { // it's a personal access token but not oauth2 token + if hasScope, scope := ctx.Authentication.Scope().Get(); hasScope { var scopeMatched bool requiredScopes := auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository) @@ -159,8 +153,7 @@ func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_ } } - reducer, ok := ctx.Data["ApiTokenReducer"].(authz.AuthorizationReducer) - if ok { + if reducer := ctx.Authentication.Reducer(); reducer != nil { var accessMode perm.AccessMode switch level { case auth_model.Read: @@ -184,8 +177,7 @@ func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_ } func CheckRuntimeDeterminedScope(ctx *APIContext, scopeCategory auth_model.AccessTokenScopeCategory, level auth_model.AccessTokenScopeLevel, msg string) { - scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) - if ok { + if hasScope, scope := ctx.Authentication.Scope().Get(); hasScope { var scopeMatched bool requiredScopes := auth_model.GetRequiredScopes(level, scopeCategory) diff --git a/services/lfs/server.go b/services/lfs/server.go index ef0df4a0ab..457602243e 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -539,8 +539,7 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho accessMode = perm.AccessModeWrite } - if ctx.Data["IsActionsToken"] == true { - taskID := ctx.Data["ActionsTaskID"].(int64) + if hasTaskID, taskID := ctx.Authentication.ActionsTaskID().Get(); hasTaskID { task, err := actions_model.GetTaskByID(ctx, taskID) if err != nil { log.Error("Unable to GetTaskByID for task[%d] Error: %v", taskID, err) diff --git a/tests/integration/api_packages_chef_test.go b/tests/integration/api_packages_chef_test.go index cadf46b03f..b467f18e13 100644 --- a/tests/integration/api_packages_chef_test.go +++ b/tests/integration/api_packages_chef_test.go @@ -94,9 +94,10 @@ nwIDAQAB defer tests.PrintCurrentTest(t)() req := NewRequest(t, "POST", "/dummy") - u, err := auth.Verify(req.Request, nil, nil, nil) - assert.Nil(t, u) + u, err := auth.Verify(req.Request, nil, nil) require.NoError(t, err) + require.NotNil(t, u) + assert.Nil(t, u.User()) }) t.Run("NotExistingUser", func(t *testing.T) { @@ -104,7 +105,7 @@ nwIDAQAB req := NewRequest(t, "POST", "/dummy"). SetHeader("X-Ops-Userid", "not-existing-user") - u, err := auth.Verify(req.Request, nil, nil, nil) + u, err := auth.Verify(req.Request, nil, nil) assert.Nil(t, u) require.Error(t, err) }) @@ -114,12 +115,12 @@ nwIDAQAB req := NewRequest(t, "POST", "/dummy"). SetHeader("X-Ops-Userid", user.Name) - u, err := auth.Verify(req.Request, nil, nil, nil) + u, err := auth.Verify(req.Request, nil, nil) assert.Nil(t, u) require.Error(t, err) req.SetHeader("X-Ops-Timestamp", "2023-01-01T00:00:00Z") - u, err = auth.Verify(req.Request, nil, nil, nil) + u, err = auth.Verify(req.Request, nil, nil) assert.Nil(t, u) require.Error(t, err) }) @@ -130,27 +131,27 @@ nwIDAQAB req := NewRequest(t, "POST", "/dummy"). SetHeader("X-Ops-Userid", user.Name). SetHeader("X-Ops-Timestamp", time.Now().UTC().Format(time.RFC3339)) - u, err := auth.Verify(req.Request, nil, nil, nil) + u, err := auth.Verify(req.Request, nil, nil) assert.Nil(t, u) require.Error(t, err) req.SetHeader("X-Ops-Sign", "version=none") - u, err = auth.Verify(req.Request, nil, nil, nil) + u, err = auth.Verify(req.Request, nil, nil) assert.Nil(t, u) require.Error(t, err) req.SetHeader("X-Ops-Sign", "version=1.4") - u, err = auth.Verify(req.Request, nil, nil, nil) + u, err = auth.Verify(req.Request, nil, nil) assert.Nil(t, u) require.Error(t, err) req.SetHeader("X-Ops-Sign", "version=1.0;algorithm=sha2") - u, err = auth.Verify(req.Request, nil, nil, nil) + u, err = auth.Verify(req.Request, nil, nil) assert.Nil(t, u) require.Error(t, err) req.SetHeader("X-Ops-Sign", "version=1.0;algorithm=sha256") - u, err = auth.Verify(req.Request, nil, nil, nil) + u, err = auth.Verify(req.Request, nil, nil) assert.Nil(t, u) require.Error(t, err) }) @@ -166,7 +167,7 @@ nwIDAQAB SetHeader("X-Ops-Sign", "version=1.0;algorithm=sha1"). SetHeader("X-Ops-Content-Hash", "unused"). SetHeader("X-Ops-Authorization-4", "dummy") - u, err := auth.Verify(req.Request, nil, nil, nil) + u, err := auth.Verify(req.Request, nil, nil) assert.Nil(t, u) require.Error(t, err) @@ -257,7 +258,7 @@ nwIDAQAB defer tests.PrintCurrentTest(t)() signRequest(req, v) - u, err = auth.Verify(req.Request, nil, nil, nil) + u, err = auth.Verify(req.Request, nil, nil) assert.NotNil(t, u) require.NoError(t, err) })