refactor: change authentication to return structured data (#12202)

Currently authentication methods return information in two forms: they return who was authenticated as a `*user_model.User`, and then they insert key-values into `ctx.Data` which has critical impact on how the authenticated request is treated.  This PR changes the authentication methods to return structured data in the form of an `AuthenticationResult`, with all the key-value information in `ctx.Data` being moved into methods on the `AuthenticationResult` interface.

Authentication workflows in Forgejo are a real mess.  This is the first step in trying to clean it up and make the code predictable and reasonable, and is both follow-up work that was identified from the repo-specific access tokens (where the `"ApiTokenReducer"` key-value was added), and is pre-requisite work to future JWT enhancements that are [being discussed](https://codeberg.org/forgejo/forgejo/issues/3571#issuecomment-13268004).

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests for Go changes

- I added test coverage for Go changes...
  - [ ] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
  - All changes, at least in theory, are refactors of existing logic and are not expected to have functional deviations -- existing regression tests are the only planned testing.
- I ran...
  - [x] `make pr-go` before pushing

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12202
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
This commit is contained in:
Mathieu Fenniak 2026-04-22 21:00:26 +02:00 committed by Mathieu Fenniak
parent 2ed98ac848
commit 1ddd5faa5c
45 changed files with 620 additions and 348 deletions

View file

@ -347,21 +347,6 @@ linters:
- path: services/actions/trust.go - path: services/actions/trust.go
linters: linters:
- nilnil - 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 - path: services/contexttest/context_tests.go
linters: linters:
- nilnil - nilnil

View file

@ -38,14 +38,13 @@ import (
"forgejo.org/routers/api/packages/swift" "forgejo.org/routers/api/packages/swift"
"forgejo.org/routers/api/packages/vagrant" "forgejo.org/routers/api/packages/vagrant"
"forgejo.org/services/auth" "forgejo.org/services/auth"
auth_method "forgejo.org/services/auth/method"
"forgejo.org/services/context" "forgejo.org/services/context"
) )
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
return func(ctx *context.Context) { return func(ctx *context.Context) {
if ctx.Data["IsApiToken"] == true { if hasScope, scope := ctx.Authentication.Scope().Get(); hasScope {
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
if ok { // it's a personal access token but not oauth2 token
scopeMatched := false scopeMatched := false
var err error var err error
switch accessMode { switch accessMode {
@ -82,7 +81,6 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
} }
} }
} }
}
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`) ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`)
@ -116,38 +114,48 @@ func enforcePackagesQuota() func(ctx *context.Context) {
func verifyAuth(r *web.Route, authMethods []auth.Method) { func verifyAuth(r *web.Route, authMethods []auth.Method) {
if setting.Service.EnableReverseProxyAuth { 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) { r.Use(func(ctx *context.Context) {
var err error authResult, err := authGroup.Verify(ctx.Req, ctx.Resp, ctx.Session)
ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
if err != nil { if err != nil {
log.Info("Failed to verify user: %v", err) log.Info("Failed to verify user: %v", err)
ctx.Error(http.StatusUnauthorized, "authGroup.Verify") ctx.Error(http.StatusUnauthorized, "authGroup.Verify")
return return
} }
if authResult == nil {
ctx.Error(http.StatusInternalServerError, "verifyAuth nil authentication result")
return
}
ctx.Doer = authResult.User()
ctx.IsSigned = ctx.Doer != nil ctx.IsSigned = ctx.Doer != nil
ctx.Authentication = authResult
}) })
} }
func verifyContainerAuth(r *web.Route, authMethods []auth.Method) { func verifyContainerAuth(r *web.Route, authMethods []auth.Method) {
if setting.Service.EnableReverseProxyAuth { 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) { r.Use(func(ctx *context.Context) {
var err error authResult, err := authGroup.Verify(ctx.Req, ctx.Resp, ctx.Session)
ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
if err != nil { if err != nil {
log.Info("Failed to verify user: %v", err) log.Info("Failed to verify user: %v", err)
container.APIUnauthorizedError(ctx) container.APIUnauthorizedError(ctx)
ctx.Error(http.StatusUnauthorized, "authGroup.Verify") ctx.Error(http.StatusUnauthorized, "authGroup.Verify")
return return
} }
if authResult == nil {
ctx.Error(http.StatusInternalServerError, "verifyContainerAuth nil authentication result")
return
}
ctx.Doer = authResult.User()
ctx.IsSigned = ctx.Doer != nil ctx.IsSigned = ctx.Doer != nil
ctx.Authentication = authResult
}) })
} }
@ -159,8 +167,8 @@ func CommonRoutes() *web.Route {
r.Use(context.PackageContexter()) r.Use(context.PackageContexter())
verifyAuth(r, []auth.Method{ verifyAuth(r, []auth.Method{
&auth.OAuth2{}, &auth_method.OAuth2{},
&auth.Basic{}, &auth_method.Basic{},
&nuget.Auth{}, &nuget.Auth{},
&conan.Auth{}, &conan.Auth{},
&chef.Auth{}, &chef.Auth{},
@ -804,7 +812,7 @@ func ContainerRoutes() *web.Route {
r.Use(context.PackageContexter()) 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.Get("", container.ReqContainerAccess, container.DetermineSupport)
r.Group("/token", func() { r.Group("/token", func() {

View file

@ -40,8 +40,18 @@ var (
authorizationPattern = regexp.MustCompile(`\AX-Ops-Authorization-(\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: // Documentation:
// https://docs.chef.io/server/api_chef_server/#required-headers // https://docs.chef.io/server/api_chef_server/#required-headers
// https://github.com/chef-boneyard/chef-rfc/blob/master/rfc065-sign-v1.3.md // 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 // Verify extracts the user from the signed request
// If the request is signed with the user private key the user is verified. // 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) u, err := getUserFromRequest(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if u == nil { if u == nil {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
pub, err := getUserPublicKey(req.Context(), u) 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 nil, err
} }
return u, nil return &chefAuthenticationResult{user: u}, nil
} }
func getUserFromRequest(req *http.Request) (*user_model.User, error) { func getUserFromRequest(req *http.Request) (*user_model.User, error) {

View file

@ -6,13 +6,32 @@ package conan
import ( import (
"net/http" "net/http"
auth_model "forgejo.org/models/auth"
user_model "forgejo.org/models/user" user_model "forgejo.org/models/user"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/optional"
"forgejo.org/services/auth" "forgejo.org/services/auth"
"forgejo.org/services/packages" "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{} type Auth struct{}
@ -21,7 +40,7 @@ func (a *Auth) Name() string {
} }
// Verify extracts the user from the Bearer token // 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) uid, scope, err := packages.ParseAuthorizationToken(req)
if err != nil { if err != nil {
log.Trace("ParseAuthorizationToken: %v", err) 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 { if uid == 0 {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
// Propagate scope of the authorization token. // Propagate scope of the authorization token.
authScope := optional.None[auth_model.AccessTokenScope]()
if scope != "" { if scope != "" {
store.GetData()["IsApiToken"] = true authScope = optional.Some(scope)
store.GetData()["ApiTokenScope"] = scope
} }
u, err := user_model.GetUserByID(req.Context(), uid) 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 nil, err
} }
return u, nil return &conanAuthenticationResult{user: u, scope: authScope}, nil
} }

View file

@ -11,7 +11,6 @@ import (
"strings" "strings"
"time" "time"
auth_model "forgejo.org/models/auth"
"forgejo.org/models/db" "forgejo.org/models/db"
packages_model "forgejo.org/models/packages" packages_model "forgejo.org/models/packages"
conan_model "forgejo.org/models/packages/conan" 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. // 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) token, err := packages_service.CreateAuthorizationToken(ctx.Doer, scope)
if err != nil { if err != nil {

View file

@ -6,13 +6,32 @@ package container
import ( import (
"net/http" "net/http"
auth_model "forgejo.org/models/auth"
user_model "forgejo.org/models/user" user_model "forgejo.org/models/user"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/optional"
"forgejo.org/services/auth" "forgejo.org/services/auth"
"forgejo.org/services/packages" "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{} type Auth struct{}
@ -22,7 +41,7 @@ func (a *Auth) Name() string {
// Verify extracts the user from the Bearer token // Verify extracts the user from the Bearer token
// If it's an anonymous session a ghost user is returned // 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) uid, scope, err := packages.ParseAuthorizationToken(req)
if err != nil { if err != nil {
log.Trace("ParseAuthorizationToken: %v", err) 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 { if uid == 0 {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
// Propagate scope of the authorization token. // Propagate scope of the authorization token.
authScope := optional.None[auth_model.AccessTokenScope]()
if scope != "" { if scope != "" {
store.GetData()["IsApiToken"] = true authScope = optional.Some(scope)
store.GetData()["ApiTokenScope"] = scope
} }
u, err := user_model.GetPossibleUserByID(req.Context(), uid) 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 nil, err
} }
return u, nil return &containerAuthenticationResult{user: u, scope: authScope}, nil
} }

View file

@ -13,7 +13,6 @@ import (
"regexp" "regexp"
"strconv" "strconv"
auth_model "forgejo.org/models/auth"
packages_model "forgejo.org/models/packages" packages_model "forgejo.org/models/packages"
container_model "forgejo.org/models/packages/container" container_model "forgejo.org/models/packages/container"
user_model "forgejo.org/models/user" user_model "forgejo.org/models/user"
@ -158,7 +157,7 @@ func Authenticate(ctx *context.Context) {
} }
// If there's an API scope, ensure it propagates. // 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) token, err := packages_service.CreateAuthorizationToken(u, scope)
if err != nil { if err != nil {

View file

@ -12,6 +12,20 @@ import (
"forgejo.org/services/auth" "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{} var _ auth.Method = &Auth{}
type Auth struct{} 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 // 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")) token, err := auth_model.GetAccessTokenBySHA(req.Context(), req.Header.Get("X-NuGet-ApiKey"))
if err != nil { if err != nil {
if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) { if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
log.Error("GetAccessTokenBySHA: %v", err) log.Error("GetAccessTokenBySHA: %v", err)
return nil, err return nil, err
} }
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
u, err := user_model.GetUserByID(req.Context(), token.UID) 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) log.Error("UpdateLastUsed: %v", err)
} }
return u, nil return &nugetAuthenticationResult{user: u}, nil
} }

View file

@ -4,6 +4,7 @@
package shared package shared
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
@ -12,6 +13,7 @@ import (
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/routers/common" "forgejo.org/routers/common"
"forgejo.org/services/auth" "forgejo.org/services/auth"
auth_method "forgejo.org/services/auth/method"
"forgejo.org/services/authz" "forgejo.org/services/authz"
"forgejo.org/services/context" "forgejo.org/services/context"
@ -43,14 +45,14 @@ func Middlewares() (stack []any) {
) )
} }
func buildAuthGroup() *auth.Group { func buildAuthGroup() *auth_method.Group {
group := auth.NewGroup( group := auth_method.NewGroup(
&auth.OAuth2{}, &auth_method.OAuth2{},
&auth.HTTPSign{}, &auth_method.HTTPSign{},
&auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API &auth_method.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API
) )
if setting.Service.EnableReverseProxyAuthAPI { if setting.Service.EnableReverseProxyAuthAPI {
group.Add(&auth.ReverseProxy{}) group.Add(&auth_method.ReverseProxy{})
} }
return group return group
@ -63,15 +65,18 @@ func apiAuthentication(authMethod auth.Method) func(*context.APIContext) {
ctx.Error(http.StatusUnauthorized, "APIAuth", err) ctx.Error(http.StatusUnauthorized, "APIAuth", err)
return return
} }
ctx.Doer = ar.Doer if ar == nil {
ctx.IsSigned = ar.Doer != nil ctx.Error(http.StatusInternalServerError, "apiAuthentication nil authentication result", errors.New("nil authentication result"))
ctx.IsBasicAuth = ar.IsBasicAuth return
}
ctx.Doer = ar.User()
ctx.IsSigned = ctx.Doer != nil
ctx.Authentication = ar
} }
} }
func apiAuthorization(ctx *context.APIContext) { func apiAuthorization(ctx *context.APIContext) {
scope, scopeExists := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) if hasScope, scope := ctx.Authentication.Scope().Get(); hasScope {
if scopeExists {
publicOnly, err := scope.PublicOnly() publicOnly, err := scope.PublicOnly()
if err != nil { if err != nil {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error())
@ -80,13 +85,13 @@ func apiAuthorization(ctx *context.APIContext) {
ctx.PublicOnly = publicOnly ctx.PublicOnly = publicOnly
} }
reducer, reducerExists := ctx.Data["ApiTokenReducer"].(authz.AuthorizationReducer) reducer := ctx.Authentication.Reducer()
if reducerExists { if reducer != nil {
ctx.Reducer = reducer ctx.Reducer = reducer
} else { } else {
// No "ApiTokenReducer" will be populated if the auth method wasn't an PAT. In this case, we populate // No Reducer will be populated if the auth method wasn't an PAT. In this case, we populate `ctx.Reducer` so no
// `ctx.Reducer` so no nil checks are needed, and we respect the scope `PublicOnly()` so that it it's safe to // nil checks are needed, and we respect the scope `PublicOnly()` so that it it's safe to just rely on
// just rely on `ctx.Reducer` to account for public-only access: // `ctx.Reducer` to account for public-only access:
if ctx.PublicOnly { if ctx.PublicOnly {
ctx.Reducer = &authz.PublicReposAuthorizationReducer{} ctx.Reducer = &authz.PublicReposAuthorizationReducer{}
} else { } else {

View file

@ -85,7 +85,6 @@ import (
"forgejo.org/routers/api/v1/settings" "forgejo.org/routers/api/v1/settings"
"forgejo.org/routers/api/v1/user" "forgejo.org/routers/api/v1/user"
"forgejo.org/services/actions" "forgejo.org/services/actions"
"forgejo.org/services/auth"
"forgejo.org/services/context" "forgejo.org/services/context"
"forgejo.org/services/forms" "forgejo.org/services/forms"
redirect_service "forgejo.org/services/redirect" redirect_service "forgejo.org/services/redirect"
@ -180,8 +179,8 @@ func repoAssignment() func(ctx *context.APIContext) {
repo.Owner = owner repo.Owner = owner
ctx.Repo.Repository = repo ctx.Repo.Repository = repo
if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID { if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID && ctx.Authentication.ActionsTaskID().Has() {
taskID := ctx.Data["ActionsTaskID"].(int64) _, taskID := ctx.Authentication.ActionsTaskID().Get()
task, err := actions_model.GetTaskByID(ctx, taskID) task, err := actions_model.GetTaskByID(ctx, taskID)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "actions_model.GetTaskByID", err) ctx.Error(http.StatusInternalServerError, "actions_model.GetTaskByID", err)
@ -330,8 +329,8 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
} }
// Need OAuth2 token to be present. // Need OAuth2 token to be present.
scope, scopeExists := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) hasScope, scope := ctx.Authentication.Scope().Get()
if ctx.Data["IsApiToken"] != true || !scopeExists { if !hasScope {
return return
} }
@ -372,7 +371,7 @@ func tokenRequiresRepoOwnerScope(ctx *context.APIContext) {
func reqToken() func(ctx *context.APIContext) { func reqToken() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
// If actions token is present // If actions token is present
if true == ctx.Data["IsActionsToken"] { if ctx.Authentication.ActionsTaskID().Has() {
return return
} }
@ -401,13 +400,13 @@ func reqUsersExploreEnabled() func(ctx *context.APIContext) {
func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) { func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) {
return 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 return
} }
// Require basic authorization method to be used and that basic // Require basic authorization method to be used and that basic
// authorization used password login to verify the user. // 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") ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth method not allowed")
return return
} }

View file

@ -4,32 +4,30 @@
package common package common
import ( import (
user_model "forgejo.org/models/user" "errors"
"forgejo.org/modules/web/middleware" "forgejo.org/modules/web/middleware"
auth_service "forgejo.org/services/auth" auth_service "forgejo.org/services/auth"
"forgejo.org/services/context" "forgejo.org/services/context"
) )
type AuthResult struct { func AuthShared(ctx *context.Base, sessionStore auth_service.SessionStore, authMethod auth_service.Method) (ar auth_service.AuthenticationResult, err error) {
Doer *user_model.User ar, err = authMethod.Verify(ctx.Req, ctx.Resp, sessionStore)
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)
if err != nil { if err != nil {
return ar, err return ar, err
} }
if ar.Doer != nil { if ar == nil {
if ctx.Locale.Language() != ar.Doer.Language { 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) ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
} }
ar.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth_service.BasicMethodName
ctx.Data["IsSigned"] = true ctx.Data["IsSigned"] = true
ctx.Data[middleware.ContextDataKeySignedUser] = ar.Doer ctx.Data[middleware.ContextDataKeySignedUser] = doer
ctx.Data["SignedUserID"] = ar.Doer.ID ctx.Data["SignedUserID"] = doer.ID
ctx.Data["IsAdmin"] = ar.Doer.IsAdmin ctx.Data["IsAdmin"] = doer.IsAdmin
} else { } else {
ctx.Data["IsSigned"] = false ctx.Data["IsSigned"] = false
ctx.Data["SignedUserID"] = int64(0) ctx.Data["SignedUserID"] = int64(0)

View file

@ -33,7 +33,7 @@ import (
"forgejo.org/routers/private" "forgejo.org/routers/private"
web_routers "forgejo.org/routers/web" web_routers "forgejo.org/routers/web"
actions_service "forgejo.org/services/actions" 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/auth/source/oauth2"
"forgejo.org/services/automerge" "forgejo.org/services/automerge"
"forgejo.org/services/cron" "forgejo.org/services/cron"
@ -160,7 +160,7 @@ func InitWebInstalled(ctx context.Context) {
mustInitCtx(ctx, ssh.Init) mustInitCtx(ctx, ssh.Init)
auth.Init() auth_method.Init()
mustInit(svg.Init) mustInit(svg.Init)
actions_service.Init() actions_service.Init()

View file

@ -28,6 +28,7 @@ import (
"forgejo.org/modules/web" "forgejo.org/modules/web"
"forgejo.org/modules/web/middleware" "forgejo.org/modules/web/middleware"
auth_service "forgejo.org/services/auth" auth_service "forgejo.org/services/auth"
auth_method "forgejo.org/services/auth/method"
"forgejo.org/services/auth/source/oauth2" "forgejo.org/services/auth/source/oauth2"
"forgejo.org/services/context" "forgejo.org/services/context"
"forgejo.org/services/externalaccount" "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 err != nil {
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form) ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)

View file

@ -16,7 +16,7 @@ import (
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/util" "forgejo.org/modules/util"
"forgejo.org/modules/web" "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/auth/source/oauth2"
"forgejo.org/services/context" "forgejo.org/services/context"
"forgejo.org/services/externalaccount" "forgejo.org/services/externalaccount"
@ -128,7 +128,7 @@ func LinkAccountPostSignIn(ctx *context.Context) {
return return
} }
u, _, err := auth_service.UserSignIn(ctx, signInForm.UserName, signInForm.Password) u, _, err := auth_method.UserSignIn(ctx, signInForm.UserName, signInForm.Password)
if err != nil { if err != nil {
handleSignInError(ctx, signInForm.UserName, &signInForm, tplLinkAccount, "UserLinkAccount", err) handleSignInError(ctx, signInForm.UserName, &signInForm, tplLinkAccount, "UserLinkAccount", err)
return return

View file

@ -33,7 +33,7 @@ import (
"forgejo.org/modules/util" "forgejo.org/modules/util"
"forgejo.org/modules/web" "forgejo.org/modules/web"
"forgejo.org/modules/web/middleware" "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" source_service "forgejo.org/services/auth/source"
"forgejo.org/services/auth/source/oauth2" "forgejo.org/services/auth/source/oauth2"
"forgejo.org/services/context" "forgejo.org/services/context"
@ -294,7 +294,7 @@ func ifOnlyPublicGroups(scopes string) bool {
// InfoOAuth manages request for userinfo endpoint // InfoOAuth manages request for userinfo endpoint
func InfoOAuth(ctx *context.Context) { 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.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
ctx.PlainText(http.StatusUnauthorized, "no valid authorization") ctx.PlainText(http.StatusUnauthorized, "no valid authorization")
return 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) onlyPublicGroups := ifOnlyPublicGroups(grantScopes)
groups, err := getOAuthGroupsForUser(ctx, ctx.Doer, onlyPublicGroups) groups, err := getOAuthGroupsForUser(ctx, ctx.Doer, onlyPublicGroups)

View file

@ -18,6 +18,7 @@ import (
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/web" "forgejo.org/modules/web"
"forgejo.org/services/auth" "forgejo.org/services/auth"
auth_method "forgejo.org/services/auth/method"
"forgejo.org/services/context" "forgejo.org/services/context"
"forgejo.org/services/forms" "forgejo.org/services/forms"
) )
@ -266,7 +267,7 @@ func ConnectOpenIDPost(ctx *context.Context) {
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["OpenID"] = oid 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 { if err != nil {
handleSignInError(ctx, form.UserName, &form, tplConnectOID, "ConnectOpenIDPost", err) handleSignInError(ctx, form.UserName, &form, tplConnectOID, "ConnectOpenIDPost", err)
return return

View file

@ -158,7 +158,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
return nil 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) _, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err == nil { 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 // 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 // Because of special ref "refs/for" .. , need delay write permission check
accessMode = perm.AccessModeRead accessMode = perm.AccessModeRead
if ctx.Data["IsActionsToken"] == true { if hasTaskID, taskID := ctx.Authentication.ActionsTaskID().Get(); hasTaskID {
taskID := ctx.Data["ActionsTaskID"].(int64)
task, err := actions_model.GetTaskByID(ctx, taskID) task, err := actions_model.GetTaskByID(ctx, taskID)
if err != nil { if err != nil {
ctx.ServerError("GetTaskByID", err) ctx.ServerError("GetTaskByID", err)

View file

@ -19,7 +19,7 @@ import (
"forgejo.org/modules/timeutil" "forgejo.org/modules/timeutil"
"forgejo.org/modules/validation" "forgejo.org/modules/validation"
"forgejo.org/modules/web" "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/db"
"forgejo.org/services/auth/source/smtp" "forgejo.org/services/auth/source/smtp"
"forgejo.org/services/context" "forgejo.org/services/context"
@ -274,7 +274,7 @@ func DeleteAccount(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAccount"] = true 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 { switch {
case user_model.IsErrUserNotExist(err): case user_model.IsErrUserNotExist(err):
loadAccountData(ctx) loadAccountData(ctx)

View file

@ -48,6 +48,7 @@ import (
user_setting "forgejo.org/routers/web/user/setting" user_setting "forgejo.org/routers/web/user/setting"
"forgejo.org/routers/web/user/setting/security" "forgejo.org/routers/web/user/setting/security"
auth_service "forgejo.org/services/auth" auth_service "forgejo.org/services/auth"
auth_method "forgejo.org/services/auth/method"
"forgejo.org/services/context" "forgejo.org/services/context"
"forgejo.org/services/forms" "forgejo.org/services/forms"
"forgejo.org/services/lfs" "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 // The Session plugin is expected to be executed second, in order to skip authentication
// for users that have already signed in. // for users that have already signed in.
func buildAuthGroup() *auth_service.Group { func buildAuthGroup() *auth_method.Group {
group := auth_service.NewGroup() group := auth_method.NewGroup()
group.Add(&auth_service.OAuth2{}) // FIXME: this should be removed and only applied in download and oauth related routers group.Add(&auth_method.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_method.Basic{}) // FIXME: this should be removed and only applied in download and git/lfs routers
if setting.Service.EnableReverseProxyAuth { if setting.Service.EnableReverseProxyAuth {
group.Add(&auth_service.ReverseProxy{}) // reverseproxy should before Session, otherwise the header will be ignored if user has login group.Add(&auth_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 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")) ctx.Error(http.StatusUnauthorized, ctx.Locale.TrString("auth.unauthorized_credentials", "https://codeberg.org/forgejo/forgejo/issues/2809"))
return return
} }
ctx.Doer = ar.Doer if ar == nil {
ctx.IsSigned = ar.Doer != nil ctx.Error(http.StatusInternalServerError, "webAuth nil authentication result")
ctx.IsBasicAuth = ar.IsBasicAuth return
}
ctx.Doer = ar.User()
ctx.IsSigned = ar.User() != nil
ctx.Authentication = ar
if ctx.Doer == nil { if ctx.Doer == nil {
// ensure the session uid is deleted // ensure the session uid is deleted
_ = ctx.Session.Delete("uid") _ = ctx.Session.Delete("uid")

View file

@ -7,9 +7,12 @@ import (
"context" "context"
"net/http" "net/http"
auth_model "forgejo.org/models/auth"
user_model "forgejo.org/models/user" user_model "forgejo.org/models/user"
"forgejo.org/modules/optional"
"forgejo.org/modules/session" "forgejo.org/modules/session"
"forgejo.org/modules/web/middleware" "forgejo.org/modules/web/middleware"
"forgejo.org/services/authz"
) )
// DataStore represents a data store // DataStore represents a data store
@ -20,15 +23,11 @@ type SessionStore session.Store
// Method represents an authentication method (plugin) for HTTP requests. // Method represents an authentication method (plugin) for HTTP requests.
type Method interface { type Method interface {
// Verify tries to verify the authentication data contained in the request. // Verify tries to verify the authentication data contained in the request. If verification is successful returns an
// If verification is successful returns either an existing user object (with id > 0) // AuthenticationResult implementation with details about the authentication, or, may return an
// or a new user object (with id = 0) populated with the information that was found // AnonymousAuthentication if the authentication method doesn't indicate that the request is authenticated. An error
// in the authentication data (username or email). // is only returned if a failure occurred while checking authentication.
// Second argument returns err if verification fails, otherwise Verify(http *http.Request, w http.ResponseWriter, sess SessionStore) (AuthenticationResult, error)
// 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
} }
// PasswordAuthenticator represents a source of authentication // PasswordAuthenticator represents a source of authentication
@ -45,3 +44,62 @@ type LocalTwoFASkipper interface {
type SynchronizableSource interface { type SynchronizableSource interface {
Sync(ctx context.Context, updateExisting bool) error 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
}

View file

@ -1,4 +1,4 @@
package auth package method
import ( import (
"testing" "testing"

View file

@ -2,7 +2,7 @@
// Copyright 2019 The Gitea Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package auth package method
import ( import (
"fmt" "fmt"
@ -17,6 +17,7 @@ import (
"forgejo.org/modules/session" "forgejo.org/modules/session"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/web/middleware" "forgejo.org/modules/web/middleware"
"forgejo.org/services/auth"
user_service "forgejo.org/services/user" 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 // 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... // We need to regenerate the session...
newSess, err := session.RegenerateSession(resp, req) newSess, err := session.RegenerateSession(resp, req)
if err != nil { if err != nil {

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -2,7 +2,7 @@
// Copyright 2019 The Gitea Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package auth package method
import ( import (
"net/http" "net/http"

View file

@ -2,7 +2,7 @@
// Copyright 2019 The Gitea Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package auth package method
import ( import (
"errors" "errors"
@ -14,48 +14,42 @@ import (
user_model "forgejo.org/models/user" user_model "forgejo.org/models/user"
"forgejo.org/modules/base" "forgejo.org/modules/base"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/optional"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/util" "forgejo.org/modules/util"
"forgejo.org/modules/web/middleware" "forgejo.org/modules/web/middleware"
"forgejo.org/services/auth"
"forgejo.org/services/authz" "forgejo.org/services/authz"
) )
// Ensure the struct implements the interface. // Ensure the struct implements the interface.
var ( 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 // 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" // only) by looking for Basic authentication data or "x-oauth-basic" token in the "Authorization"
// header. // header.
type Basic struct{} 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 // 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 // "Authorization" header of the request and returns the corresponding user object for that
// name/token on successful validation. // name/token on successful validation.
// Returns nil if header is empty or validation fails. // 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 // Basic authentication should only fire on API, Download or on Git or LFSPaths
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) { if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
baHead := req.Header.Get("Authorization") baHead := req.Header.Get("Authorization")
if len(baHead) == 0 { if len(baHead) == 0 {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
auths := strings.SplitN(baHead, " ", 2) auths := strings.SplitN(baHead, " ", 2)
if len(auths) != 2 || (strings.ToLower(auths[0]) != "basic") { if len(auths) != 2 || (strings.ToLower(auths[0]) != "basic") {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
uname, passwd, _ := base.BasicAuthDecode(auths[1]) 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 return nil, err
} }
store.GetData()["IsApiToken"] = true var scope auth_model.AccessTokenScope
if grantScopes != "" { if grantScopes != "" {
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScope(grantScopes) scope = auth_model.AccessTokenScope(grantScopes)
} else { } 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 // 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) log.Error("UpdateLastUsed: %v", err)
} }
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = token.Scope
reducer, err := authz.GetAuthorizationReducerForAccessToken(req.Context(), token) reducer, err := authz.GetAuthorizationReducerForAccessToken(req.Context(), token)
if err != nil { if err != nil {
log.Error("authz.GetAuthorizationReducerForAccessToken: %v", err) log.Error("authz.GetAuthorizationReducerForAccessToken: %v", err)
return nil, 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) { } else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
log.Error("GetAccessTokenBySha: %v", 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) task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken)
if err == nil && task != nil { if err == nil && task != nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID) log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
return &actionsTaskTokenAuthenticationResult{user: user_model.NewActionsUser(), taskID: task.ID}, nil
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = task.ID
return user_model.NewActionsUser(), nil
} }
if !setting.Service.EnableBasicAuth { if !setting.Service.EnableBasicAuth {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
log.Trace("Basic Authorization: Attempting SignIn for %s", uname) 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") 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 { if err := validateTOTP(req, u); err != nil {
return nil, err 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) log.Trace("Basic Authorization: Logged in user %-v", u)
store.GetData()["IsPasswordLogin"] = true return &basicPaswordAuthenticationResult{user: u}, nil
return u, nil
} }
func getOtpHeader(header http.Header) string { func getOtpHeader(header http.Header) string {

View file

@ -1,51 +1,41 @@
// Copyright 2021 The Gitea Authors. All rights reserved. // Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package auth package method
import ( import (
"net/http" "net/http"
"strings"
user_model "forgejo.org/models/user" "forgejo.org/services/auth"
) )
// Ensure the struct implements the interface. // Ensure the struct implements the interface.
var ( var (
_ Method = &Group{} _ auth.Method = &Group{}
) )
// Group implements the Auth interface with serval Auth. // Group implements the Auth interface with serval Auth.
type Group struct { type Group struct {
methods []Method methods []auth.Method
} }
// NewGroup creates a new auth group // NewGroup creates a new auth group
func NewGroup(methods ...Method) *Group { func NewGroup(methods ...auth.Method) *Group {
return &Group{ return &Group{
methods: methods, methods: methods,
} }
} }
// Add adds a new method to group // 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) b.methods = append(b.methods, method)
} }
// Name returns group's methods name func (b *Group) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) (auth.AuthenticationResult, error) {
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) {
// Try to sign in with each of the enabled plugins // Try to sign in with each of the enabled plugins
var retErr error var retErr error
for _, m := range b.methods { for _, m := range b.methods {
user, err := m.Verify(req, w, store, sess) authResult, err := m.Verify(req, w, sess)
if err != nil { if err != nil {
if retErr == nil { if retErr == nil {
retErr = err retErr = err
@ -57,16 +47,17 @@ func (b *Group) Verify(req *http.Request, w http.ResponseWriter, store DataStore
continue continue
} }
// If any method returns a user, we can stop trying. // If any method returns an authenticated result, we can stop trying. Return and ignore any error returned by
// Return the user and ignore any error returned by previous methods. // previous methods.
if user != nil { if authResult.User() != nil {
if store.GetData()["AuthedMethod"] == nil { return authResult, nil
store.GetData()["AuthedMethod"] = m.Name()
}
return user, nil
} }
} }
if retErr != nil {
// If no method returns a user, return the error returned by the first method. // If no method returns a user, return the error returned by the first method.
return nil, retErr return nil, retErr
} }
return &auth.UnauthenticatedResult{}, nil
}

View file

@ -1,7 +1,7 @@
// Copyright 2021 The Gitea Authors. All rights reserved. // Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package auth package method
import ( import (
"bytes" "bytes"
@ -16,6 +16,7 @@ import (
user_model "forgejo.org/models/user" user_model "forgejo.org/models/user"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/services/auth"
"github.com/42wim/httpsig" "github.com/42wim/httpsig"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
@ -23,7 +24,7 @@ import (
// Ensure the struct implements the interface. // Ensure the struct implements the interface.
var ( var (
_ Method = &HTTPSign{} _ auth.Method = &HTTPSign{}
) )
// HTTPSign implements the Auth interface and authenticates requests (API requests // 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 // more information can be found on https://github.com/go-fed/httpsig
type HTTPSign struct{} 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 // Verify extracts and validates HTTPsign from the Signature header of the request and returns
// the corresponding user object on successful validation. // the corresponding user object on successful validation.
// Returns nil if header is empty or validation fails. // 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") sigHead := req.Header.Get("Signature")
if len(sigHead) == 0 { if len(sigHead) == 0 {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
var ( 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 { if len(req.Header.Get("X-Ssh-Certificate")) != 0 {
// Handle Signature signed by SSH certificates // Handle Signature signed by SSH certificates
if len(setting.SSH.TrustedUserCAKeys) == 0 { if len(setting.SSH.TrustedUserCAKeys) == 0 {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
publicKey, err = VerifyCert(req) publicKey, err = VerifyCert(req)
if err != nil { if err != nil {
log.Debug("VerifyCert on request from %s: failed: %v", req.RemoteAddr, err) log.Debug("VerifyCert on request from %s: failed: %v", req.RemoteAddr, err)
log.Warn("Failed authentication attempt from %s", req.RemoteAddr) log.Warn("Failed authentication attempt from %s", req.RemoteAddr)
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
} else { } else {
// Handle Signature signed by Public Key // 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 { if err != nil {
log.Debug("VerifyPubKey on request from %s: failed: %v", req.RemoteAddr, err) log.Debug("VerifyPubKey on request from %s: failed: %v", req.RemoteAddr, err)
log.Warn("Failed authentication attempt from %s", req.RemoteAddr) 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 return nil, err
} }
store.GetData()["IsApiToken"] = true
log.Trace("HTTP Sign: Logged in user %-v", u) log.Trace("HTTP Sign: Logged in user %-v", u)
return &httpSignAuthenticationResult{user: u}, nil
return u, nil
} }
func VerifyPubKey(r *http.Request) (*asymkey_model.PublicKey, error) { func VerifyPubKey(r *http.Request) (*asymkey_model.PublicKey, error) {

View file

@ -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)
}

View file

@ -2,10 +2,11 @@
// Copyright 2019 The Gitea Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package auth package method
import ( import (
"context" "context"
"errors"
"net/http" "net/http"
"slices" "slices"
"strings" "strings"
@ -15,17 +16,19 @@ import (
auth_model "forgejo.org/models/auth" auth_model "forgejo.org/models/auth"
user_model "forgejo.org/models/user" user_model "forgejo.org/models/user"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/optional"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/util" "forgejo.org/modules/util"
"forgejo.org/modules/web/middleware" "forgejo.org/modules/web/middleware"
"forgejo.org/services/actions" "forgejo.org/services/actions"
"forgejo.org/services/auth"
"forgejo.org/services/auth/source/oauth2" "forgejo.org/services/auth/source/oauth2"
"forgejo.org/services/authz" "forgejo.org/services/authz"
) )
// Ensure the struct implements the interface. // Ensure the struct implements the interface.
var ( var (
_ Method = &OAuth2{} _ auth.Method = &OAuth2{}
) )
// grantAdditionalScopes returns valid scopes coming from grant // grantAdditionalScopes returns valid scopes coming from grant
@ -113,11 +116,6 @@ func CheckTaskIsRunning(ctx context.Context, taskID int64) bool {
// "Authorization" header. // "Authorization" header.
type OAuth2 struct{} 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 // parseToken returns the token from request, and a boolean value
// representing whether the token exists or not // representing whether the token exists or not
func parseToken(req *http.Request) (string, bool) { 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. // userIDFromToken returns the user id corresponding to the OAuth token.
// It will set 'IsApiToken' to true if the token is an API token and // It will set 'IsApiToken' to true if the token is an API token and
// set 'ApiTokenScope' to the scope of the access token // 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 == "" { if tokenSHA == "" {
return 0, auth_model.ErrAccessTokenEmpty{} return nil, auth_model.ErrAccessTokenEmpty{}
} }
// Let's see if token is valid. // Let's see if token is valid.
if strings.Contains(tokenSHA, ".") { if strings.Contains(tokenSHA, ".") {
// First attempt to decode an actions JWT, returning the actions user // First attempt to decode an actions JWT, returning the actions user
if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil { if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil {
if CheckTaskIsRunning(ctx, taskID) { if CheckTaskIsRunning(ctx, taskID) {
store.GetData()["IsActionsToken"] = true return &actionsTaskTokenAuthenticationResult{user: user_model.NewActionsUser(), taskID: taskID}, nil
store.GetData()["ActionsTaskID"] = taskID
return user_model.ActionsUserID, nil
} }
} }
// Otherwise, check if this is an OAuth access token // Otherwise, check if this is an OAuth access token
uid, grantScopes := CheckOAuthAccessToken(ctx, tokenSHA) uid, grantScopes := CheckOAuthAccessToken(ctx, tokenSHA)
var accessTokenScope optional.Option[auth_model.AccessTokenScope]
if uid != 0 { if uid != 0 {
store.GetData()["IsApiToken"] = true
if grantScopes != "" { if grantScopes != "" {
store.GetData()["ApiTokenScope"] = auth_model.AccessTokenScope(grantScopes) accessTokenScope = optional.Some(auth_model.AccessTokenScope(grantScopes))
} else { } 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) t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA)
if err != nil { if err != nil {
if auth_model.IsErrAccessTokenNotExist(err) { 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) task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA)
if err == nil && task != nil { if err == nil && task != nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID) log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
return &actionsTaskTokenAuthenticationResult{user: user_model.NewActionsUser(), taskID: task.ID}, nil
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = task.ID
return user_model.ActionsUserID, nil
} }
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) { } else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
log.Error("GetAccessTokenBySHA: %v", err) log.Error("GetAccessTokenBySHA: %v", err)
return nil, err
} }
return 0, err return nil, err
} }
if err := t.UpdateLastUsed(ctx); err != nil { if err := t.UpdateLastUsed(ctx); err != nil {
log.Error("UpdateLastUsed: %v", err) log.Error("UpdateLastUsed: %v", err)
} }
if t.UID == 0 { 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) reducer, err := authz.GetAuthorizationReducerForAccessToken(ctx, t)
if err != nil { if err != nil {
log.Error("authz.GetAuthorizationReducerForAccessToken: %v", err) 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 // 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. // or the "Authorization" header and returns the corresponding user object for that ID.
// If verification is successful returns an existing user object. // If verification is successful returns an existing user object.
// Returns nil if verification fails. // 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 // 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) && if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) && !isAuthenticatedTokenRequest(req) &&
!isGitRawOrAttachPath(req) && !isArchivePath(req) { !isGitRawOrAttachPath(req) && !isArchivePath(req) {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
token, ok := parseToken(req) token, ok := parseToken(req)
if !ok { 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 { if err != nil {
return nil, err 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) log.Trace("OAuth2 Authorization: Logged in user %-v", auth.User())
return auth, nil
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
} }
func isAuthenticatedTokenRequest(req *http.Request) bool { func isAuthenticatedTokenRequest(req *http.Request) bool {

View file

@ -1,7 +1,7 @@
// Copyright 2024 The Gitea Authors. All rights reserved. // Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package auth package method
import ( import (
"net/http" "net/http"
@ -11,7 +11,6 @@ import (
"forgejo.org/models/auth" "forgejo.org/models/auth"
"forgejo.org/models/unittest" "forgejo.org/models/unittest"
user_model "forgejo.org/models/user" user_model "forgejo.org/models/user"
"forgejo.org/modules/web/middleware"
"forgejo.org/services/actions" "forgejo.org/services/actions"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -33,14 +32,13 @@ func TestUserIDFromToken(t *testing.T) {
token, err := actions.CreateAuthorizationToken(task, map[string]any{}, false) token, err := actions.CreateAuthorizationToken(task, map[string]any{}, false)
require.NoError(t, err) require.NoError(t, err)
ds := make(middleware.ContextData)
o := OAuth2{} o := OAuth2{}
uid, err := o.userIDFromToken(t.Context(), token, ds) authResult, err := o.userIDFromToken(t.Context(), token)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, int64(user_model.ActionsUserID), uid) assert.Equal(t, int64(user_model.ActionsUserID), authResult.User().ID)
assert.Equal(t, true, ds["IsActionsToken"]) isActionsToken, authTaskID := authResult.ActionsTaskID().Get()
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID)) assert.True(t, isActionsToken)
assert.Equal(t, int64(RunningTaskID), authTaskID)
}) })
t.Run("Actions error-JWT", func(t *testing.T) { 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"}}, "To short": {"abc", auth.ErrAccessTokenNotExist{Token: "abc"}},
} }
ds := make(middleware.ContextData)
o := OAuth2{} o := OAuth2{}
for name, c := range cases { for name, c := range cases {
t.Run(name, func(t *testing.T) { 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) require.ErrorIs(t, err, c.Error)
assert.Equal(t, int64(0), uid) assert.Nil(t, authResult)
}) })
} }
}) })

View file

@ -2,9 +2,10 @@
// Copyright 2019 The Gitea Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package auth package method
import ( import (
"errors"
"net/http" "net/http"
"strings" "strings"
@ -12,14 +13,16 @@ import (
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/optional" "forgejo.org/modules/optional"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/util"
"forgejo.org/modules/web/middleware" "forgejo.org/modules/web/middleware"
"forgejo.org/services/auth"
gouuid "github.com/google/uuid" gouuid "github.com/google/uuid"
) )
// Ensure the struct implements the interface. // Ensure the struct implements the interface.
var ( var (
_ Method = &ReverseProxy{} _ auth.Method = &ReverseProxy{}
) )
// ReverseProxyMethodName is the constant name of the ReverseProxy authentication method // 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)) 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 // getUserFromAuthUser extracts the username from the "setting.ReverseProxyAuthUser" header
// of the request and returns the corresponding user object for that name. // 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 // 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) { func (r *ReverseProxy) getUserFromAuthUser(req *http.Request) (*user_model.User, error) {
username := r.getUserName(req) username := r.getUserName(req)
if len(username) == 0 { if len(username) == 0 {
return nil, nil return nil, util.ErrNotExist
} }
log.Trace("ReverseProxy Authorization: Found username: %s", username) 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), // 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). // 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. // 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) user, err := r.getUserFromAuthUser(req)
if err != nil { if err != nil && !errors.Is(err, util.ErrNotExist) {
return nil, err return nil, err
} }
if user == nil { if user == nil {
user = r.getUserFromAuthEmail(req) user = r.getUserFromAuthEmail(req)
if user == nil { 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) handleSignIn(w, req, sess, user)
} }
} }
store.GetData()["IsReverseProxy"] = true
log.Trace("ReverseProxy Authorization: Logged in user %-v", user) log.Trace("ReverseProxy Authorization: Logged in user %-v", user)
return user, nil return &reverseProxyAuthenticationResult{user: user}, nil
} }
// isAutoRegisterAllowed checks if EnableReverseProxyAutoRegister setting is true // isAutoRegisterAllowed checks if EnableReverseProxyAutoRegister setting is true

View file

@ -1,7 +1,7 @@
// Copyright 2024 The Forgejo Authors. All rights reserved. // Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package auth package method
import ( import (
"net/http" "net/http"

View file

@ -1,47 +1,43 @@
// Copyright 2019 The Gitea Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package auth package method
import ( import (
"net/http" "net/http"
user_model "forgejo.org/models/user" user_model "forgejo.org/models/user"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/services/auth"
) )
// Ensure the struct implements the interface. // Ensure the struct implements the interface.
var ( var (
_ Method = &Session{} _ auth.Method = &Session{}
) )
// Session checks if there is a user uid stored in the session and returns the user // Session checks if there is a user uid stored in the session and returns the user
// object for that uid. // object for that uid.
type Session struct{} 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 // Verify checks if there is a user uid stored in the session and returns the user
// object for that uid. // object for that uid.
// Returns nil if there is no user uid stored in the session. // 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 { if sess == nil {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
// Get user ID // Get user ID
uid := sess.Get("uid") uid := sess.Get("uid")
if uid == nil { if uid == nil {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
log.Trace("Session Authorization: Found user[%d]", uid) log.Trace("Session Authorization: Found user[%d]", uid)
id, ok := uid.(int64) id, ok := uid.(int64)
if !ok { if !ok {
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
// Get user object // 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 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, err
} }
return nil, nil return &auth.UnauthenticatedResult{}, nil
} }
log.Trace("Session Authorization: Logged in user %-v", user) log.Trace("Session Authorization: Logged in user %-v", user)
return user, nil return &sessionAuthenticationResult{user: user}, nil
} }

View file

@ -1,7 +1,7 @@
// Copyright 2021 The Gitea Authors. All rights reserved. // Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package auth package method
import ( import (
"context" "context"
@ -12,6 +12,7 @@ import (
user_model "forgejo.org/models/user" user_model "forgejo.org/models/user"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/optional" "forgejo.org/modules/optional"
auth_service "forgejo.org/services/auth"
"forgejo.org/services/auth/source/oauth2" "forgejo.org/services/auth/source/oauth2"
"forgejo.org/services/auth/source/smtp" "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 return nil, nil, oauth2.ErrAuthSourceNotActivated
} }
authenticator, ok := source.Cfg.(PasswordAuthenticator) authenticator, ok := source.Cfg.(auth_service.PasswordAuthenticator)
if !ok { if !ok {
return nil, nil, smtp.ErrUnsupportedLoginType return nil, nil, smtp.ErrUnsupportedLoginType
} }
@ -98,7 +99,7 @@ func UserSignIn(ctx context.Context, username, password string) (*user_model.Use
continue continue
} }
authenticator, ok := source.Cfg.(PasswordAuthenticator) authenticator, ok := source.Cfg.(auth_service.PasswordAuthenticator)
if !ok { if !ok {
continue continue
} }

View file

@ -25,6 +25,7 @@ import (
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/web" "forgejo.org/modules/web"
web_types "forgejo.org/modules/web/types" web_types "forgejo.org/modules/web/types"
"forgejo.org/services/auth"
"forgejo.org/services/authz" "forgejo.org/services/authz"
"code.forgejo.org/go-chi/cache" "code.forgejo.org/go-chi/cache"
@ -38,7 +39,7 @@ type APIContext struct {
Doer *user_model.User // current signed-in user Doer *user_model.User // current signed-in user
IsSigned bool IsSigned bool
IsBasicAuth bool Authentication auth.AuthenticationResult
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer

View file

@ -25,6 +25,7 @@ import (
"forgejo.org/modules/web" "forgejo.org/modules/web"
"forgejo.org/modules/web/middleware" "forgejo.org/modules/web/middleware"
web_types "forgejo.org/modules/web/types" web_types "forgejo.org/modules/web/types"
"forgejo.org/services/auth"
"code.forgejo.org/go-chi/cache" "code.forgejo.org/go-chi/cache"
"code.forgejo.org/go-chi/session" "code.forgejo.org/go-chi/session"
@ -53,7 +54,7 @@ type Context struct {
Doer *user_model.User // current signed-in user Doer *user_model.User // current signed-in user
IsSigned bool IsSigned bool
IsBasicAuth bool Authentication auth.AuthenticationResult
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer 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(), "/"), Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"),
Repo: &Repository{PullRequest: &PullRequest{}}, Repo: &Repository{PullRequest: &PullRequest{}},
Org: &Organization{}, Org: &Organization{},
Authentication: &auth.UnauthenticatedResult{},
} }
ctx.TemplateContext = NewTemplateContextForWeb(ctx) ctx.TemplateContext = NewTemplateContextForWeb(ctx)
ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}} ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}}

View file

@ -12,7 +12,6 @@ import (
repo_model "forgejo.org/models/repo" repo_model "forgejo.org/models/repo"
"forgejo.org/models/unit" "forgejo.org/models/unit"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/services/authz"
) )
// RequireRepoAdmin returns a middleware for requiring repository admin permission // 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 // CheckRepoScopedToken check whether personal access token has repo scope
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) { func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true { if hasScope, scope := ctx.Authentication.Scope().Get(); hasScope {
return
}
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
if ok { // it's a personal access token but not oauth2 token
var scopeMatched bool var scopeMatched bool
requiredScopes := auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository) 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 reducer := ctx.Authentication.Reducer(); reducer != nil {
if ok {
var accessMode perm.AccessMode var accessMode perm.AccessMode
switch level { switch level {
case auth_model.Read: 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) { func CheckRuntimeDeterminedScope(ctx *APIContext, scopeCategory auth_model.AccessTokenScopeCategory, level auth_model.AccessTokenScopeLevel, msg string) {
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope) if hasScope, scope := ctx.Authentication.Scope().Get(); hasScope {
if ok {
var scopeMatched bool var scopeMatched bool
requiredScopes := auth_model.GetRequiredScopes(level, scopeCategory) requiredScopes := auth_model.GetRequiredScopes(level, scopeCategory)

View file

@ -539,8 +539,7 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
accessMode = perm.AccessModeWrite accessMode = perm.AccessModeWrite
} }
if ctx.Data["IsActionsToken"] == true { if hasTaskID, taskID := ctx.Authentication.ActionsTaskID().Get(); hasTaskID {
taskID := ctx.Data["ActionsTaskID"].(int64)
task, err := actions_model.GetTaskByID(ctx, taskID) task, err := actions_model.GetTaskByID(ctx, taskID)
if err != nil { if err != nil {
log.Error("Unable to GetTaskByID for task[%d] Error: %v", taskID, err) log.Error("Unable to GetTaskByID for task[%d] Error: %v", taskID, err)

View file

@ -94,9 +94,10 @@ nwIDAQAB
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "POST", "/dummy") req := NewRequest(t, "POST", "/dummy")
u, err := auth.Verify(req.Request, nil, nil, nil) u, err := auth.Verify(req.Request, nil, nil)
assert.Nil(t, u)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, u)
assert.Nil(t, u.User())
}) })
t.Run("NotExistingUser", func(t *testing.T) { t.Run("NotExistingUser", func(t *testing.T) {
@ -104,7 +105,7 @@ nwIDAQAB
req := NewRequest(t, "POST", "/dummy"). req := NewRequest(t, "POST", "/dummy").
SetHeader("X-Ops-Userid", "not-existing-user") 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) assert.Nil(t, u)
require.Error(t, err) require.Error(t, err)
}) })
@ -114,12 +115,12 @@ nwIDAQAB
req := NewRequest(t, "POST", "/dummy"). req := NewRequest(t, "POST", "/dummy").
SetHeader("X-Ops-Userid", user.Name) 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) assert.Nil(t, u)
require.Error(t, err) require.Error(t, err)
req.SetHeader("X-Ops-Timestamp", "2023-01-01T00:00:00Z") 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) assert.Nil(t, u)
require.Error(t, err) require.Error(t, err)
}) })
@ -130,27 +131,27 @@ nwIDAQAB
req := NewRequest(t, "POST", "/dummy"). req := NewRequest(t, "POST", "/dummy").
SetHeader("X-Ops-Userid", user.Name). SetHeader("X-Ops-Userid", user.Name).
SetHeader("X-Ops-Timestamp", time.Now().UTC().Format(time.RFC3339)) 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) assert.Nil(t, u)
require.Error(t, err) require.Error(t, err)
req.SetHeader("X-Ops-Sign", "version=none") 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) assert.Nil(t, u)
require.Error(t, err) require.Error(t, err)
req.SetHeader("X-Ops-Sign", "version=1.4") 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) assert.Nil(t, u)
require.Error(t, err) require.Error(t, err)
req.SetHeader("X-Ops-Sign", "version=1.0;algorithm=sha2") 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) assert.Nil(t, u)
require.Error(t, err) require.Error(t, err)
req.SetHeader("X-Ops-Sign", "version=1.0;algorithm=sha256") 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) assert.Nil(t, u)
require.Error(t, err) require.Error(t, err)
}) })
@ -166,7 +167,7 @@ nwIDAQAB
SetHeader("X-Ops-Sign", "version=1.0;algorithm=sha1"). SetHeader("X-Ops-Sign", "version=1.0;algorithm=sha1").
SetHeader("X-Ops-Content-Hash", "unused"). SetHeader("X-Ops-Content-Hash", "unused").
SetHeader("X-Ops-Authorization-4", "dummy") 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) assert.Nil(t, u)
require.Error(t, err) require.Error(t, err)
@ -257,7 +258,7 @@ nwIDAQAB
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
signRequest(req, v) signRequest(req, v)
u, err = auth.Verify(req.Request, nil, nil, nil) u, err = auth.Verify(req.Request, nil, nil)
assert.NotNil(t, u) assert.NotNil(t, u)
require.NoError(t, err) require.NoError(t, err)
}) })