mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
[v15.0/forgejo] refactor: clarify four different outputs that authentication methods provide (#12468)
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/12231 #12202 began a refactor of Forgejo's authentication implementations by providing structured data on an authentication success. However, error cases were maintained as-is in that refactor, leaving a complex situation: what does returning an error from an authentication method mean?; does it mean that the authentication failed, or that a server error occurred? Can another authentication still be tried? This PR changes authentication methods so that they can return one of four things: - `AuthenticationSuccess` with an authentication result. - `AuthenticationNotAttempted` which indicates that no credentials relevant for this authentication method were presented. If every method returned `AuthenticationNotAttempted`, then you would have an unauthenticated access. - `AuthenticationAttemptedIncorrectCredential` which indicates that credentials were present and failed validation -- a situation indicating a `401 Unauthorized`. - `AuthenticationError` which indicates that an internal server error occurred and failed authentication -- indicating a `500 Internal Server Error`. This paves the way for one more refactor coming next: `basic.go` and `oauth2.go` perform 3-4 different authentications each (access tokens, oauth JWTs, actions tokens, actions JWTs, and username/password). With the capability to return these more precise responses, these authentication methods can be split up into separate logic that isn't intertwined together. Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12468 Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
parent
0aa1b45956
commit
a1222ebb5b
17 changed files with 340 additions and 229 deletions
|
|
@ -4,6 +4,7 @@
|
|||
package packages
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
|
@ -119,19 +120,30 @@ func verifyAuth(r *web.Route, authMethods []auth.Method) {
|
|||
authGroup := auth_method.NewGroup(authMethods...)
|
||||
|
||||
r.Use(func(ctx *context.Context) {
|
||||
authResult, err := authGroup.Verify(ctx.Req, ctx.Resp, ctx.Session)
|
||||
if err != nil {
|
||||
log.Info("Failed to verify user: %v", err)
|
||||
output := authGroup.Verify(ctx.Req, ctx.Resp, ctx.Session)
|
||||
var ar auth.AuthenticationResult
|
||||
switch v := output.(type) {
|
||||
case *auth.AuthenticationSuccess:
|
||||
ar = v.Result
|
||||
case *auth.AuthenticationNotAttempted:
|
||||
ar = &auth.UnauthenticatedResult{}
|
||||
case *auth.AuthenticationError:
|
||||
ctx.ServerError("authentication error", v.Error)
|
||||
return
|
||||
case *auth.AuthenticationAttemptedIncorrectCredential:
|
||||
ctx.Error(http.StatusUnauthorized, "authGroup.Verify")
|
||||
return
|
||||
}
|
||||
if authResult == nil {
|
||||
ctx.Error(http.StatusInternalServerError, "verifyAuth nil authentication result")
|
||||
default:
|
||||
ctx.ServerError("Verify failed", errors.New("unexpected result from Method.Verify"))
|
||||
return
|
||||
}
|
||||
ctx.Doer = authResult.User()
|
||||
if ar == nil {
|
||||
ctx.ServerError("nil authentication result", errors.New("nil authentication result"))
|
||||
return
|
||||
}
|
||||
ctx.Doer = ar.User()
|
||||
ctx.IsSigned = ctx.Doer != nil
|
||||
ctx.Authentication = authResult
|
||||
ctx.Authentication = ar
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -142,20 +154,32 @@ func verifyContainerAuth(r *web.Route, authMethods []auth.Method) {
|
|||
authGroup := auth_method.NewGroup(authMethods...)
|
||||
|
||||
r.Use(func(ctx *context.Context) {
|
||||
authResult, err := authGroup.Verify(ctx.Req, ctx.Resp, ctx.Session)
|
||||
if err != nil {
|
||||
log.Info("Failed to verify user: %v", err)
|
||||
output := authGroup.Verify(ctx.Req, ctx.Resp, ctx.Session)
|
||||
var ar auth.AuthenticationResult
|
||||
switch v := output.(type) {
|
||||
case *auth.AuthenticationSuccess:
|
||||
ar = v.Result
|
||||
case *auth.AuthenticationNotAttempted:
|
||||
ar = &auth.UnauthenticatedResult{}
|
||||
case *auth.AuthenticationError:
|
||||
ctx.ServerError("authentication error", v.Error)
|
||||
return
|
||||
case *auth.AuthenticationAttemptedIncorrectCredential:
|
||||
log.Info("Failed to verify user: %v", v.Error)
|
||||
container.APIUnauthorizedError(ctx)
|
||||
ctx.Error(http.StatusUnauthorized, "authGroup.Verify")
|
||||
return
|
||||
}
|
||||
if authResult == nil {
|
||||
ctx.Error(http.StatusInternalServerError, "verifyContainerAuth nil authentication result")
|
||||
default:
|
||||
ctx.ServerError("Verify failed", errors.New("unexpected result from Method.Verify"))
|
||||
return
|
||||
}
|
||||
ctx.Doer = authResult.User()
|
||||
if ar == nil {
|
||||
ctx.ServerError("nil authentication result", errors.New("nil authentication result"))
|
||||
return
|
||||
}
|
||||
ctx.Doer = ar.User()
|
||||
ctx.IsSigned = ctx.Doer != nil
|
||||
ctx.Authentication = authResult
|
||||
ctx.Authentication = ar
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,34 +65,37 @@ 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, sess auth.SessionStore) (auth.AuthenticationResult, error) {
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) auth.MethodOutput {
|
||||
u, err := getUserFromRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("chef auth getUserFromRequest: %w", err)}
|
||||
}
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: err}
|
||||
}
|
||||
if u == nil {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
||||
pub, err := getUserPublicKey(req.Context(), u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("chef auth getUserPublicKey: %w", err)}
|
||||
}
|
||||
|
||||
if err := verifyTimestamp(req); err != nil {
|
||||
return nil, err
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: err}
|
||||
}
|
||||
|
||||
version, err := getSignVersion(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: err}
|
||||
}
|
||||
|
||||
if err := verifySignedHeaders(req, version, pub.(*rsa.PublicKey)); err != nil {
|
||||
return nil, err
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: err}
|
||||
}
|
||||
|
||||
return &chefAuthenticationResult{user: u}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &chefAuthenticationResult{user: u}}
|
||||
}
|
||||
|
||||
func getUserFromRequest(req *http.Request) (*user_model.User, error) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
package conan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
|
|
@ -40,15 +41,18 @@ func (a *Auth) Name() string {
|
|||
}
|
||||
|
||||
// Verify extracts the user from the Bearer token
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) (auth.AuthenticationResult, error) {
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) auth.MethodOutput {
|
||||
uid, scope, err := packages.ParseAuthorizationToken(req)
|
||||
if err != nil {
|
||||
log.Trace("ParseAuthorizationToken: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if uid == 0 {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
// Errors from ParseAuthorizationToken are almost all from malformed incoming input, which we'll consider an
|
||||
// auth failure:
|
||||
// - `Authorization` header was present for all cases, so it's not `AuthenticationNotAttempted`
|
||||
// - it's not `AuthenticationError` because malformed headers would cause errors, and this is intended for
|
||||
// server errors which should cause 500s
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: fmt.Errorf("conan auth JWT error: %w", err)}
|
||||
} else if uid == 0 {
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
||||
// Propagate scope of the authorization token.
|
||||
|
|
@ -60,8 +64,8 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, sess auth.Sessio
|
|||
u, err := user_model.GetUserByID(req.Context(), uid)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("conan auth GetUserByID failed: %w", err)}
|
||||
}
|
||||
|
||||
return &conanAuthenticationResult{user: u, scope: authScope}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &conanAuthenticationResult{user: u, scope: authScope}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
|
|
@ -41,15 +42,18 @@ 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, sess auth.SessionStore) (auth.AuthenticationResult, error) {
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) auth.MethodOutput {
|
||||
uid, scope, err := packages.ParseAuthorizationToken(req)
|
||||
if err != nil {
|
||||
log.Trace("ParseAuthorizationToken: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if uid == 0 {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
// Errors from ParseAuthorizationToken are almost all from malformed incoming input, which we'll consider an
|
||||
// auth failure:
|
||||
// - `Authorization` header was present for all cases, so it's not `AuthenticationNotAttempted`
|
||||
// - it's not `AuthenticationError` because malformed headers would cause errors, and this is intended for
|
||||
// server errors which should cause 500s
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: err}
|
||||
} else if uid == 0 {
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
||||
// Propagate scope of the authorization token.
|
||||
|
|
@ -60,9 +64,8 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, sess auth.Sessio
|
|||
|
||||
u, err := user_model.GetPossibleUserByID(req.Context(), uid)
|
||||
if err != nil {
|
||||
log.Error("GetPossibleUserByID: %v", err)
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("container auth GetPossibleUserByID: %w", err)}
|
||||
}
|
||||
|
||||
return &containerAuthenticationResult{user: u, scope: authScope}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &containerAuthenticationResult{user: u, scope: authScope}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
package nuget
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
|
|
@ -26,34 +27,30 @@ func (r *nugetAuthenticationResult) User() *user_model.User {
|
|||
return r.user
|
||||
}
|
||||
|
||||
var _ auth.Method = &Auth{}
|
||||
|
||||
type Auth struct{}
|
||||
|
||||
func (a *Auth) Name() string {
|
||||
return "nuget"
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#request-parameters
|
||||
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"))
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) auth.MethodOutput {
|
||||
apiKey := req.Header.Get("X-NuGet-ApiKey")
|
||||
if apiKey == "" {
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
token, err := auth_model.GetAccessTokenBySHA(req.Context(), apiKey)
|
||||
if err != nil {
|
||||
if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
|
||||
log.Error("GetAccessTokenBySHA: %v", err)
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("nuget auth GetAccessTokenBySHA: %w", err)}
|
||||
}
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: err}
|
||||
}
|
||||
|
||||
u, err := user_model.GetUserByID(req.Context(), token.UID)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("nuget auth GetUserByID: %w", err)}
|
||||
}
|
||||
|
||||
if err := token.UpdateLastUsed(req.Context()); err != nil {
|
||||
log.Error("UpdateLastUsed: %v", err)
|
||||
}
|
||||
|
||||
return &nugetAuthenticationResult{user: u}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &nugetAuthenticationResult{user: u}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,13 +60,25 @@ func buildAuthGroup() *auth_method.Group {
|
|||
|
||||
func apiAuthentication(authMethod auth.Method) func(*context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
ar, err := common.AuthShared(ctx.Base, nil, authMethod)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnauthorized, "APIAuth", err)
|
||||
output := common.AuthShared(ctx.Base, nil, authMethod)
|
||||
var ar auth.AuthenticationResult
|
||||
switch v := output.(type) {
|
||||
case *auth.AuthenticationSuccess:
|
||||
ar = v.Result
|
||||
case *auth.AuthenticationNotAttempted:
|
||||
ar = &auth.UnauthenticatedResult{}
|
||||
case *auth.AuthenticationAttemptedIncorrectCredential:
|
||||
ctx.Error(http.StatusUnauthorized, "APIAuth", v.Error)
|
||||
return
|
||||
case *auth.AuthenticationError:
|
||||
ctx.ServerError("authentication error", v.Error)
|
||||
return
|
||||
default:
|
||||
ctx.ServerError("authentication error", errors.New("unexpected result from common.AuthShared"))
|
||||
return
|
||||
}
|
||||
if ar == nil {
|
||||
ctx.Error(http.StatusInternalServerError, "apiAuthentication nil authentication result", errors.New("nil authentication result"))
|
||||
ctx.ServerError("nil authentication result", errors.New("nil authentication result"))
|
||||
return
|
||||
}
|
||||
ctx.Doer = ar.User()
|
||||
|
|
|
|||
|
|
@ -4,35 +4,37 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"forgejo.org/modules/web/middleware"
|
||||
auth_service "forgejo.org/services/auth"
|
||||
"forgejo.org/services/context"
|
||||
)
|
||||
|
||||
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
|
||||
func AuthShared(ctx *context.Base, sessionStore auth_service.SessionStore, authMethod auth_service.Method) auth_service.MethodOutput {
|
||||
output := authMethod.Verify(ctx.Req, ctx.Resp, sessionStore)
|
||||
|
||||
var ar auth_service.AuthenticationResult
|
||||
switch v := output.(type) {
|
||||
case *auth_service.AuthenticationSuccess:
|
||||
ar = v.Result
|
||||
case *auth_service.AuthenticationNotAttempted:
|
||||
ar = &auth_service.UnauthenticatedResult{}
|
||||
}
|
||||
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)
|
||||
if ar != nil {
|
||||
doer := ar.User()
|
||||
if doer != nil {
|
||||
if ctx.Locale.Language() != doer.Language {
|
||||
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
|
||||
}
|
||||
ctx.Data["IsSigned"] = true
|
||||
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)
|
||||
}
|
||||
ctx.Data["IsSigned"] = true
|
||||
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)
|
||||
}
|
||||
return ar, nil
|
||||
return output
|
||||
}
|
||||
|
||||
// VerifyOptions contains required or check options
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package web
|
|||
|
||||
import (
|
||||
gocontext "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
|
@ -120,14 +121,28 @@ func buildAuthGroup() *auth_method.Group {
|
|||
|
||||
func webAuth(authMethod auth_service.Method) func(*context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
ar, err := common.AuthShared(ctx.Base, ctx.Session, authMethod)
|
||||
if err != nil {
|
||||
log.Info("Failed to verify user: %v", err)
|
||||
output := common.AuthShared(ctx.Base, ctx.Session, authMethod)
|
||||
var ar auth_service.AuthenticationResult
|
||||
switch v := output.(type) {
|
||||
case *auth_service.AuthenticationSuccess:
|
||||
ar = v.Result
|
||||
case *auth_service.AuthenticationNotAttempted:
|
||||
ar = &auth_service.UnauthenticatedResult{}
|
||||
case *auth_service.AuthenticationAttemptedIncorrectCredential:
|
||||
ctx.Error(http.StatusUnauthorized, ctx.Locale.TrString("auth.unauthorized_credentials", "https://codeberg.org/forgejo/forgejo/issues/2809"))
|
||||
return
|
||||
case *auth_service.AuthenticationError:
|
||||
// Don't reveal the internal server error details to the user as they may contain sensitive details -- log
|
||||
// the details, return a generic error.
|
||||
log.Error("internal error during authentication: %v", v.Error)
|
||||
ctx.ServerError("authentication error", errors.New("internal server error in authentication"))
|
||||
return
|
||||
default:
|
||||
ctx.ServerError("authentication error", errors.New("unexpected result from common.AuthShared"))
|
||||
return
|
||||
}
|
||||
if ar == nil {
|
||||
ctx.Error(http.StatusInternalServerError, "webAuth nil authentication result")
|
||||
ctx.ServerError("nil authentication result", errors.New("nil authentication result"))
|
||||
return
|
||||
}
|
||||
ctx.Doer = ar.User()
|
||||
|
|
|
|||
|
|
@ -23,13 +23,49 @@ 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 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)
|
||||
// Verify tries to validate credentials provided in the request, and returns one of the [MethodOutput] results
|
||||
// indicating the result of its validation.
|
||||
Verify(http *http.Request, w http.ResponseWriter, sess SessionStore) MethodOutput
|
||||
}
|
||||
|
||||
// When attempting to authenticate with an authentication [Method], one of the MethodOutput implementations must be
|
||||
// returned. This interface serves as a enum of supported outputs, plus related values for each enum. Outputs are
|
||||
// [AuthenticationSuccess], [AuthenticationNotAttempted], [AuthenticationAttemptedWithFailure], and
|
||||
// [AuthenticationError].
|
||||
type MethodOutput interface {
|
||||
isMethodOutput()
|
||||
}
|
||||
|
||||
// Authentication positively identified the incoming request as successfully authenticated with the result in the
|
||||
// attached [AuthenticationResult]. For example, if a username and password were provided and we confirmed that those
|
||||
// credentials matched an existing user in the database, then AuthenticationSuccess would be returned.
|
||||
type AuthenticationSuccess struct {
|
||||
Result AuthenticationResult
|
||||
}
|
||||
|
||||
// Authentication method was not found to be applicable for the given request. For example, if a request did not contain
|
||||
// `Authorization: Basic ...`, then a basic authentication method would return AuthenticationNotAttempted to indicate
|
||||
// that this method didn't apply to the incoming request.
|
||||
type AuthenticationNotAttempted struct{}
|
||||
|
||||
// Authentication method was attempted against the request and positively identified to be an incorrect credential. For
|
||||
// example, if a request contained `Authorization: Basic ...`, and the username and password that were provided were
|
||||
// found to not match any user, AuthenticationAttemptedIncorrectCredential would be returned. This is typically an
|
||||
// indicator of a `401 Unauthorized ...` response.
|
||||
type AuthenticationAttemptedIncorrectCredential struct {
|
||||
Error error
|
||||
}
|
||||
|
||||
// Authentication was attempted and an unexpected internal error occurred.
|
||||
type AuthenticationError struct {
|
||||
Error error
|
||||
}
|
||||
|
||||
func (*AuthenticationSuccess) isMethodOutput() {}
|
||||
func (*AuthenticationNotAttempted) isMethodOutput() {}
|
||||
func (*AuthenticationAttemptedIncorrectCredential) isMethodOutput() {}
|
||||
func (*AuthenticationError) isMethodOutput() {}
|
||||
|
||||
// PasswordAuthenticator represents a source of authentication
|
||||
type PasswordAuthenticator interface {
|
||||
Authenticate(ctx context.Context, user *user_model.User, login, password string) (*user_model.User, error)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package method
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
|
|
@ -19,6 +20,7 @@ import (
|
|||
"forgejo.org/modules/util"
|
||||
"forgejo.org/modules/web/middleware"
|
||||
"forgejo.org/services/auth"
|
||||
"forgejo.org/services/auth/source/db"
|
||||
"forgejo.org/services/authz"
|
||||
)
|
||||
|
||||
|
|
@ -36,20 +38,20 @@ type Basic struct{}
|
|||
// "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, _ auth.SessionStore) (auth.AuthenticationResult, error) {
|
||||
func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, _ auth.SessionStore) auth.MethodOutput {
|
||||
// Basic authentication should only fire on API, Download or on Git or LFSPaths
|
||||
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
||||
baHead := req.Header.Get("Authorization")
|
||||
if len(baHead) == 0 {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
||||
auths := strings.SplitN(baHead, " ", 2)
|
||||
if len(auths) != 2 || (strings.ToLower(auths[0]) != "basic") {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
||||
uname, passwd, _ := base.BasicAuthDecode(auths[1])
|
||||
|
|
@ -73,8 +75,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, _ auth.SessionS
|
|||
|
||||
u, err := user_model.GetUserByID(req.Context(), uid)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("basic auth GetUserByID: %w", err)}
|
||||
}
|
||||
|
||||
var scope auth_model.AccessTokenScope
|
||||
|
|
@ -83,17 +84,16 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, _ auth.SessionS
|
|||
} else {
|
||||
scope = auth_model.AccessTokenScopeAll // fallback to all
|
||||
}
|
||||
return &oAuth2JWTAuthenticationResult{user: u, scope: optional.Some(scope)}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &oAuth2JWTAuthenticationResult{user: u, scope: optional.Some(scope)}}
|
||||
}
|
||||
|
||||
// check personal access token
|
||||
token, err := auth_model.GetAccessTokenBySHA(req.Context(), authToken)
|
||||
if err == nil {
|
||||
log.Trace("Basic Authorization: Valid AccessToken for user[%d]", uid)
|
||||
log.Trace("Basic Authorization: Valid AccessToken for user[%d]", token.UID)
|
||||
u, err := user_model.GetUserByID(req.Context(), token.UID)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("basic auth GetUserByID for access token: %w", err)}
|
||||
}
|
||||
|
||||
if err = token.UpdateLastUsed(req.Context()); err != nil {
|
||||
|
|
@ -102,11 +102,10 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, _ auth.SessionS
|
|||
|
||||
reducer, err := authz.GetAuthorizationReducerForAccessToken(req.Context(), token)
|
||||
if err != nil {
|
||||
log.Error("authz.GetAuthorizationReducerForAccessToken: %v", err)
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("basic auth GetAuthorizationReducerForAccessToken: %w", err)}
|
||||
}
|
||||
|
||||
return &accessTokenAuthenticationResult{user: u, scope: token.Scope, reducer: reducer}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &accessTokenAuthenticationResult{user: u, scope: token.Scope, reducer: reducer}}
|
||||
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
|
||||
log.Error("GetAccessTokenBySha: %v", err)
|
||||
}
|
||||
|
|
@ -115,41 +114,41 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, _ auth.SessionS
|
|||
task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken)
|
||||
if err == nil && task != nil {
|
||||
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
|
||||
return &actionsTaskTokenAuthenticationResult{user: user_model.NewActionsUser(), taskID: task.ID}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &actionsTaskTokenAuthenticationResult{user: user_model.NewActionsUser(), taskID: task.ID}}
|
||||
}
|
||||
|
||||
if !setting.Service.EnableBasicAuth {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: errors.New("basic authentication by username & password is disabled")}
|
||||
}
|
||||
|
||||
log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
|
||||
u, source, err := UserSignIn(req.Context(), uname, passwd)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
log.Error("UserSignIn: %v", err)
|
||||
if user_model.IsErrUserNotExist(err) || user_model.IsErrUserProhibitLogin(err) ||
|
||||
errors.As(err, &db.ErrUserPasswordInvalid{}) || errors.As(err, &db.ErrUserPasswordNotSet{}) {
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: err}
|
||||
}
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("basic auth UserSignIn: %w", err)}
|
||||
}
|
||||
|
||||
hashWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
|
||||
if err != nil {
|
||||
log.Error("HasWebAuthnRegistrationsByUID: %v", err)
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("basic auth HasWebAuthnRegistrationsByUID: %w", err)}
|
||||
}
|
||||
|
||||
if hashWebAuthn {
|
||||
return nil, errors.New("Basic authorization is not allowed while having security keys enrolled")
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: errors.New("Basic authorization is not allowed while having security keys enrolled")}
|
||||
}
|
||||
|
||||
if skipper, ok := source.Cfg.(auth.LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
|
||||
if err := validateTOTP(req, u); err != nil {
|
||||
return nil, err
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: err}
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace("Basic Authorization: Logged in user %-v", u)
|
||||
|
||||
return &basicPaswordAuthenticationResult{user: u}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &basicPaswordAuthenticationResult{user: u}}
|
||||
}
|
||||
|
||||
func getOtpHeader(header http.Header) string {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
package method
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/services/auth"
|
||||
|
|
@ -31,33 +33,34 @@ func (b *Group) Add(method auth.Method) {
|
|||
b.methods = append(b.methods, method)
|
||||
}
|
||||
|
||||
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
|
||||
func (b *Group) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) auth.MethodOutput {
|
||||
var incorrectCredentials []error
|
||||
|
||||
for _, m := range b.methods {
|
||||
authResult, err := m.Verify(req, w, sess)
|
||||
if err != nil {
|
||||
if retErr == nil {
|
||||
retErr = err
|
||||
}
|
||||
// Try other methods if this one failed.
|
||||
// Some methods may share the same protocol to detect if they are matched.
|
||||
// For example, OAuth2 and conan.Auth both read token from "Authorization: Bearer <token>" header,
|
||||
// If OAuth2 returns error, we should give conan.Auth a chance to try.
|
||||
output := m.Verify(req, w, sess)
|
||||
|
||||
switch v := output.(type) {
|
||||
case *auth.AuthenticationSuccess, *auth.AuthenticationError:
|
||||
return v
|
||||
|
||||
case *auth.AuthenticationNotAttempted:
|
||||
// Move on to the next supported authentication method.
|
||||
continue
|
||||
}
|
||||
|
||||
// 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
|
||||
case *auth.AuthenticationAttemptedIncorrectCredential:
|
||||
// Move on to the next supported authentication method, but keep a record of this error. If none of the
|
||||
// other methods are able to authenticate the user, we'll report this as an incorrect credential (401) case.
|
||||
incorrectCredentials = append(incorrectCredentials, v.Error)
|
||||
continue
|
||||
|
||||
default:
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("unexpected result from Method.Verify on method %v: %v", m, v)}
|
||||
}
|
||||
}
|
||||
|
||||
if retErr != nil {
|
||||
// If no method returns a user, return the error returned by the first method.
|
||||
return nil, retErr
|
||||
if len(incorrectCredentials) != 0 {
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: errors.Join(incorrectCredentials...)}
|
||||
}
|
||||
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ type HTTPSign struct{}
|
|||
// 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, _ auth.SessionStore) (auth.AuthenticationResult, error) {
|
||||
func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, _ auth.SessionStore) auth.MethodOutput {
|
||||
sigHead := req.Header.Get("Signature")
|
||||
if len(sigHead) == 0 {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -49,14 +49,14 @@ func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, _ auth.Sessi
|
|||
if len(req.Header.Get("X-Ssh-Certificate")) != 0 {
|
||||
// Handle Signature signed by SSH certificates
|
||||
if len(setting.SSH.TrustedUserCAKeys) == 0 {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
||||
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 &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{} // 401 is not expected on signature validation miss; return not attempted
|
||||
}
|
||||
} else {
|
||||
// Handle Signature signed by Public Key
|
||||
|
|
@ -64,18 +64,17 @@ func (h *HTTPSign) Verify(req *http.Request, w http.ResponseWriter, _ auth.Sessi
|
|||
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 &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{} // 401 is not expected on signature validation miss; return not attempted
|
||||
}
|
||||
}
|
||||
|
||||
u, err := user_model.GetUserByID(req.Context(), publicKey.OwnerID)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("httpsign GetUserByID: %w", err)}
|
||||
}
|
||||
|
||||
log.Trace("HTTP Sign: Logged in user %-v", u)
|
||||
return &httpSignAuthenticationResult{user: u}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &httpSignAuthenticationResult{user: u}}
|
||||
}
|
||||
|
||||
func VerifyPubKey(r *http.Request) (*asymkey_model.PublicKey, error) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ package method
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
|
@ -146,16 +146,16 @@ 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) (auth.AuthenticationResult, error) {
|
||||
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string) auth.MethodOutput {
|
||||
if tokenSHA == "" {
|
||||
return nil, auth_model.ErrAccessTokenEmpty{}
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: 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) {
|
||||
return &actionsTaskTokenAuthenticationResult{user: user_model.NewActionsUser(), taskID: taskID}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &actionsTaskTokenAuthenticationResult{user: user_model.NewActionsUser(), taskID: taskID}}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -171,12 +171,12 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string) (auth.Aut
|
|||
}
|
||||
user, err := user_model.GetPossibleUserByID(ctx, uid)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
log.Error("GetUserByName: %v", err)
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: err}
|
||||
}
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("oauth2 GetPossibleUserByID: %w", err)}
|
||||
}
|
||||
return &oAuth2JWTAuthenticationResult{user: user, scope: accessTokenScope}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &oAuth2JWTAuthenticationResult{user: user, scope: accessTokenScope}}
|
||||
}
|
||||
|
||||
t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA)
|
||||
|
|
@ -186,63 +186,50 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string) (auth.Aut
|
|||
task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA)
|
||||
if err == nil && task != nil {
|
||||
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
|
||||
return &actionsTaskTokenAuthenticationResult{user: user_model.NewActionsUser(), taskID: task.ID}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &actionsTaskTokenAuthenticationResult{user: user_model.NewActionsUser(), taskID: task.ID}}
|
||||
}
|
||||
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
|
||||
log.Error("GetAccessTokenBySHA: %v", err)
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("oauth2 GetAccessTokenBySHA: %w", err)}
|
||||
}
|
||||
return nil, err
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: err}
|
||||
}
|
||||
if err := t.UpdateLastUsed(ctx); err != nil {
|
||||
log.Error("UpdateLastUsed: %v", err)
|
||||
}
|
||||
if t.UID == 0 {
|
||||
return nil, auth_model.ErrAccessTokenNotExist{}
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: err}
|
||||
}
|
||||
|
||||
reducer, err := authz.GetAuthorizationReducerForAccessToken(ctx, t)
|
||||
if err != nil {
|
||||
log.Error("authz.GetAuthorizationReducerForAccessToken: %v", err)
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("oauth2 GetAuthorizationReducerForAccessToken: %w", err)}
|
||||
}
|
||||
|
||||
u, err := user_model.GetUserByID(ctx, t.UID)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("oauth2 GetUserByID: %w", err)}
|
||||
}
|
||||
|
||||
return &accessTokenAuthenticationResult{user: u, scope: t.Scope, reducer: reducer}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &accessTokenAuthenticationResult{user: u, scope: t.Scope, reducer: reducer}}
|
||||
}
|
||||
|
||||
// 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, _ auth.SessionStore) (auth.AuthenticationResult, error) {
|
||||
func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, _ auth.SessionStore) auth.MethodOutput {
|
||||
// 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 &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
||||
token, ok := parseToken(req)
|
||||
if !ok {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
||||
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: Logged in user %-v", auth.User())
|
||||
return auth, nil
|
||||
return o.userIDFromToken(req.Context(), token)
|
||||
}
|
||||
|
||||
func isAuthenticatedTokenRequest(req *http.Request) bool {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/services/actions"
|
||||
auth_service "forgejo.org/services/auth"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -33,8 +34,10 @@ func TestUserIDFromToken(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
o := OAuth2{}
|
||||
authResult, err := o.userIDFromToken(t.Context(), token)
|
||||
require.NoError(t, err)
|
||||
output := o.userIDFromToken(t.Context(), token)
|
||||
ar, authSuccess := output.(*auth_service.AuthenticationSuccess)
|
||||
require.True(t, authSuccess, "expected type AuthenticationSuccess, but was: %#v", output)
|
||||
authResult := ar.Result
|
||||
assert.Equal(t, int64(user_model.ActionsUserID), authResult.User().ID)
|
||||
isActionsToken, authTaskID := authResult.ActionsTaskID().Get()
|
||||
assert.True(t, isActionsToken)
|
||||
|
|
@ -53,9 +56,13 @@ func TestUserIDFromToken(t *testing.T) {
|
|||
o := OAuth2{}
|
||||
for name, c := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
authResult, err := o.userIDFromToken(t.Context(), c.Token)
|
||||
output := o.userIDFromToken(t.Context(), c.Token)
|
||||
|
||||
ar, authFailure := output.(*auth_service.AuthenticationAttemptedIncorrectCredential)
|
||||
require.True(t, authFailure, "expected type AuthenticationAttemptedIncorrectCredential, but was: %#v", output)
|
||||
err := ar.Error
|
||||
|
||||
require.ErrorIs(t, err, c.Error)
|
||||
assert.Nil(t, authResult)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package method
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
|
|
@ -77,13 +78,13 @@ func (r *ReverseProxy) getEmail(req *http.Request) string {
|
|||
// If an email is available in the "setting.ReverseProxyAuthEmail" header an existing
|
||||
// user object is returned (populated with the email found in header).
|
||||
// Returns nil if header is empty or if "setting.EnableReverseProxyEmail" is disabled.
|
||||
func (r *ReverseProxy) getUserFromAuthEmail(req *http.Request) *user_model.User {
|
||||
func (r *ReverseProxy) getUserFromAuthEmail(req *http.Request) (*user_model.User, error) {
|
||||
if !setting.Service.EnableReverseProxyEmail {
|
||||
return nil
|
||||
return nil, util.ErrNotExist
|
||||
}
|
||||
email := r.getEmail(req)
|
||||
if len(email) == 0 {
|
||||
return nil
|
||||
return nil, util.ErrNotExist
|
||||
}
|
||||
log.Trace("ReverseProxy Authorization: Found email: %s", email)
|
||||
|
||||
|
|
@ -93,24 +94,29 @@ func (r *ReverseProxy) getUserFromAuthEmail(req *http.Request) *user_model.User
|
|||
if !user_model.IsErrUserNotExist(err) {
|
||||
log.Error("GetUserByEmail: %v", err)
|
||||
}
|
||||
return nil
|
||||
return nil, err
|
||||
}
|
||||
return user
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Verify attempts to load a user object based on headers sent by the reverse proxy.
|
||||
// First it will attempt to load it based on the username (see docs for getUserFromAuthUser),
|
||||
// and failing that it will attempt to load it based on the email (see docs for getUserFromAuthEmail).
|
||||
// Returns nil if the headers are empty or the user is not found.
|
||||
func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) (auth.AuthenticationResult, error) {
|
||||
func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) auth.MethodOutput {
|
||||
user, err := r.getUserFromAuthUser(req)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return nil, err
|
||||
return &auth.AuthenticationError{Error: fmt.Errorf("reverse proxy getUserFromAuthUser: %w", err)}
|
||||
}
|
||||
if user == nil {
|
||||
user = r.getUserFromAuthEmail(req)
|
||||
user, err = r.getUserFromAuthEmail(req)
|
||||
if user == nil {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
// Not attempted is returned when no HTTP headers were provided, which is the cases that ErrNotExist
|
||||
// represents:
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: errors.New("user not found")}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +128,7 @@ func (r *ReverseProxy) Verify(req *http.Request, w http.ResponseWriter, sess aut
|
|||
}
|
||||
|
||||
log.Trace("ReverseProxy Authorization: Logged in user %-v", user)
|
||||
return &reverseProxyAuthenticationResult{user: user}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &reverseProxyAuthenticationResult{user: user}}
|
||||
}
|
||||
|
||||
// isAutoRegisterAllowed checks if EnableReverseProxyAutoRegister setting is true
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
package method
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
user_model "forgejo.org/models/user"
|
||||
|
|
@ -23,34 +24,33 @@ type Session struct{}
|
|||
// 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, sess auth.SessionStore) (auth.AuthenticationResult, error) {
|
||||
func (s *Session) Verify(req *http.Request, w http.ResponseWriter, sess auth.SessionStore) auth.MethodOutput {
|
||||
if sess == nil {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
||||
// Get user ID
|
||||
uid := sess.Get("uid")
|
||||
if uid == nil {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
log.Trace("Session Authorization: Found user[%d]", uid)
|
||||
|
||||
id, ok := uid.(int64)
|
||||
if !ok {
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationNotAttempted{}
|
||||
}
|
||||
|
||||
// Get user object
|
||||
user, err := user_model.GetUserByID(req.Context(), id)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
// 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 &auth.AuthenticationError{Error: fmt.Errorf("session auth GetUserByID: %w", err)}
|
||||
}
|
||||
return &auth.UnauthenticatedResult{}, nil
|
||||
return &auth.AuthenticationAttemptedIncorrectCredential{Error: err}
|
||||
}
|
||||
|
||||
log.Trace("Session Authorization: Logged in user %-v", user)
|
||||
return &sessionAuthenticationResult{user: user}, nil
|
||||
return &auth.AuthenticationSuccess{Result: &sessionAuthenticationResult{user: user}}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import (
|
|||
chef_module "forgejo.org/modules/packages/chef"
|
||||
"forgejo.org/modules/setting"
|
||||
chef_router "forgejo.org/routers/api/packages/chef"
|
||||
auth_service "forgejo.org/services/auth"
|
||||
"forgejo.org/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -94,10 +95,9 @@ nwIDAQAB
|
|||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "POST", "/dummy")
|
||||
u, err := auth.Verify(req.Request, nil, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, u)
|
||||
assert.Nil(t, u.User())
|
||||
output := auth.Verify(req.Request, nil, nil)
|
||||
_, authNotAttempted := output.(*auth_service.AuthenticationNotAttempted)
|
||||
assert.True(t, authNotAttempted, "expected type AuthenticationNotAttempted, but was: %#v", output)
|
||||
})
|
||||
|
||||
t.Run("NotExistingUser", func(t *testing.T) {
|
||||
|
|
@ -105,9 +105,11 @@ nwIDAQAB
|
|||
|
||||
req := NewRequest(t, "POST", "/dummy").
|
||||
SetHeader("X-Ops-Userid", "not-existing-user")
|
||||
u, err := auth.Verify(req.Request, nil, nil)
|
||||
assert.Nil(t, u)
|
||||
require.Error(t, err)
|
||||
|
||||
output := auth.Verify(req.Request, nil, nil)
|
||||
ar, authError := output.(*auth_service.AuthenticationAttemptedIncorrectCredential)
|
||||
require.True(t, authError, "expected type AuthenticationAttemptedIncorrectCredential, but was: %#v", output)
|
||||
require.ErrorContains(t, ar.Error, "user does not exist")
|
||||
})
|
||||
|
||||
t.Run("Timestamp", func(t *testing.T) {
|
||||
|
|
@ -115,14 +117,16 @@ nwIDAQAB
|
|||
|
||||
req := NewRequest(t, "POST", "/dummy").
|
||||
SetHeader("X-Ops-Userid", user.Name)
|
||||
u, err := auth.Verify(req.Request, nil, nil)
|
||||
assert.Nil(t, u)
|
||||
require.Error(t, err)
|
||||
output := auth.Verify(req.Request, nil, nil)
|
||||
ar, authError := output.(*auth_service.AuthenticationAttemptedIncorrectCredential)
|
||||
require.True(t, authError, "expected type AuthenticationAttemptedIncorrectCredential, but was: %#v", output)
|
||||
require.ErrorContains(t, ar.Error, "X-Ops-Timestamp header missing")
|
||||
|
||||
req.SetHeader("X-Ops-Timestamp", "2023-01-01T00:00:00Z")
|
||||
u, err = auth.Verify(req.Request, nil, nil)
|
||||
assert.Nil(t, u)
|
||||
require.Error(t, err)
|
||||
output = auth.Verify(req.Request, nil, nil)
|
||||
ar, authError = output.(*auth_service.AuthenticationAttemptedIncorrectCredential)
|
||||
require.True(t, authError, "expected type AuthenticationAttemptedIncorrectCredential, but was: %#v", output)
|
||||
require.ErrorContains(t, ar.Error, "time difference")
|
||||
})
|
||||
|
||||
t.Run("SigningVersion", func(t *testing.T) {
|
||||
|
|
@ -131,29 +135,35 @@ 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)
|
||||
assert.Nil(t, u)
|
||||
require.Error(t, err)
|
||||
|
||||
output := auth.Verify(req.Request, nil, nil)
|
||||
ar, authError := output.(*auth_service.AuthenticationAttemptedIncorrectCredential)
|
||||
require.True(t, authError, "expected type AuthenticationAttemptedIncorrectCredential, but was: %#v", output)
|
||||
require.ErrorContains(t, ar.Error, "X-Ops-Sign header missing")
|
||||
|
||||
req.SetHeader("X-Ops-Sign", "version=none")
|
||||
u, err = auth.Verify(req.Request, nil, nil)
|
||||
assert.Nil(t, u)
|
||||
require.Error(t, err)
|
||||
output = auth.Verify(req.Request, nil, nil)
|
||||
ar, authError = output.(*auth_service.AuthenticationAttemptedIncorrectCredential)
|
||||
require.True(t, authError, "expected type AuthenticationAttemptedIncorrectCredential, but was: %#v", output)
|
||||
require.ErrorContains(t, ar.Error, "invalid X-Ops-Sign header")
|
||||
|
||||
req.SetHeader("X-Ops-Sign", "version=1.4")
|
||||
u, err = auth.Verify(req.Request, nil, nil)
|
||||
assert.Nil(t, u)
|
||||
require.Error(t, err)
|
||||
output = auth.Verify(req.Request, nil, nil)
|
||||
ar, authError = output.(*auth_service.AuthenticationAttemptedIncorrectCredential)
|
||||
require.True(t, authError, "expected type AuthenticationAttemptedIncorrectCredential, but was: %#v", output)
|
||||
require.ErrorContains(t, ar.Error, "unsupported version")
|
||||
|
||||
req.SetHeader("X-Ops-Sign", "version=1.0;algorithm=sha2")
|
||||
u, err = auth.Verify(req.Request, nil, nil)
|
||||
assert.Nil(t, u)
|
||||
require.Error(t, err)
|
||||
output = auth.Verify(req.Request, nil, nil)
|
||||
ar, authError = output.(*auth_service.AuthenticationAttemptedIncorrectCredential)
|
||||
require.True(t, authError, "expected type AuthenticationAttemptedIncorrectCredential, but was: %#v", output)
|
||||
require.ErrorContains(t, ar.Error, "unsupported algorithm")
|
||||
|
||||
req.SetHeader("X-Ops-Sign", "version=1.0;algorithm=sha256")
|
||||
u, err = auth.Verify(req.Request, nil, nil)
|
||||
assert.Nil(t, u)
|
||||
require.Error(t, err)
|
||||
output = auth.Verify(req.Request, nil, nil)
|
||||
ar, authError = output.(*auth_service.AuthenticationAttemptedIncorrectCredential)
|
||||
require.True(t, authError, "expected type AuthenticationAttemptedIncorrectCredential, but was: %#v", output)
|
||||
require.ErrorContains(t, ar.Error, "unsupported algorithm")
|
||||
})
|
||||
|
||||
t.Run("SignedHeaders", func(t *testing.T) {
|
||||
|
|
@ -167,9 +177,10 @@ 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)
|
||||
assert.Nil(t, u)
|
||||
require.Error(t, err)
|
||||
output := auth.Verify(req.Request, nil, nil)
|
||||
ar, authError := output.(*auth_service.AuthenticationAttemptedIncorrectCredential)
|
||||
require.True(t, authError, "expected type AuthenticationAttemptedIncorrectCredential, but was: %#v", output)
|
||||
require.ErrorContains(t, ar.Error, "invalid X-Ops-Authorization headers")
|
||||
|
||||
signRequest := func(rw *RequestWrapper, version string) {
|
||||
req := rw.Request
|
||||
|
|
@ -258,9 +269,12 @@ nwIDAQAB
|
|||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
signRequest(req, v)
|
||||
u, err = auth.Verify(req.Request, nil, nil)
|
||||
assert.NotNil(t, u)
|
||||
require.NoError(t, err)
|
||||
output := auth.Verify(req.Request, nil, nil)
|
||||
ar, authSuccess := output.(*auth_service.AuthenticationSuccess)
|
||||
require.True(t, authSuccess, "expected type AuthenticationSuccess, but was: %#v", output)
|
||||
assert.NotNil(t, ar)
|
||||
assert.NotNil(t, ar.Result)
|
||||
assert.EqualValues(t, 2, ar.Result.User().ID)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue