forgejo/services/auth/basic.go
Loïc Dachary 5d18f4b19f
[BRANDING] X-Forgejo-OTP can be used instead of X-Gitea-OTP
(cherry picked from commit 7b0549cd70)
(cherry picked from commit 13e10a65d9)
(cherry picked from commit 65bdd73cf2)
(cherry picked from commit 64eba8bb92)
(cherry picked from commit 4c49b1a759)
(cherry picked from commit 93b4d06406)
(cherry picked from commit e2bc5f36d9)
(cherry picked from commit 2bee76f9df)
(cherry picked from commit 3d8a1b4a9f)
(cherry picked from commit 99dd092cd0)
(cherry picked from commit 0fdbd02204)
(cherry picked from commit 70b277a183)
(cherry picked from commit 3eece7fbb4)
(cherry picked from commit 4838fc9e11)
(cherry picked from commit b76ed541cf)
(cherry picked from commit dcdfb5b65c)
(cherry picked from commit 377dc48cdc)
(cherry picked from commit acc862f411)
(cherry picked from commit ac75ef101f)
(cherry picked from commit 08f2d9f7c5)
(cherry picked from commit e4096f0b64)
(cherry picked from commit bf5876f062)
(cherry picked from commit 7dc60637e5)
(cherry picked from commit ef3101774b)
(cherry picked from commit ecb9e8867c)
(cherry picked from commit 64f0ae72fe)
(cherry picked from commit 8dd6ec7862)
(cherry picked from commit b36723e52b)

Conflicts:
	modules/context/api.go
	https://codeberg.org/forgejo/forgejo/pulls/1466
(cherry picked from commit 5c378e0cb8)
(cherry picked from commit 1d87602819)
(cherry picked from commit 0f72002d66)
(cherry picked from commit da2556eb13)
(cherry picked from commit c01688cd90)
(cherry picked from commit af4bba8329)
(cherry picked from commit 33ca322c2e)

Conflicts:
	modules/context/api.go
	https://codeberg.org/forgejo/forgejo/pulls/1739
(cherry picked from commit c18e374d44)
(cherry picked from commit 27c4797c9f)
2023-11-14 13:17:12 +01:00

170 lines
4.9 KiB
Go

// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"net/http"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
)
// Ensure the struct implements the interface.
var (
_ Method = &Basic{}
_ Named = &Basic{}
)
// BasicMethodName is the constant name of the basic authentication method
const BasicMethodName = "basic"
// Basic implements the Auth interface and authenticates requests (API requests
// only) by looking for Basic authentication data or "x-oauth-basic" token in the "Authorization"
// header.
type Basic struct{}
// Name represents the name of auth method
func (b *Basic) Name() string {
return BasicMethodName
}
// Verify extracts and validates Basic data (username and password/token) from the
// "Authorization" header of the request and returns the corresponding user object for that
// name/token on successful validation.
// Returns nil if header is empty or validation fails.
func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) {
// Basic authentication should only fire on API, Download or on Git or LFSPaths
if !middleware.IsAPIPath(req) && !isContainerPath(req) && !isAttachmentDownload(req) && !isGitRawOrAttachOrLFSPath(req) {
return nil, nil
}
baHead := req.Header.Get("Authorization")
if len(baHead) == 0 {
return nil, nil
}
auths := strings.SplitN(baHead, " ", 2)
if len(auths) != 2 || (strings.ToLower(auths[0]) != "basic") {
return nil, nil
}
uname, passwd, _ := base.BasicAuthDecode(auths[1])
// Check if username or password is a token
isUsernameToken := len(passwd) == 0 || passwd == "x-oauth-basic"
// Assume username is token
authToken := uname
if !isUsernameToken {
log.Trace("Basic Authorization: Attempting login for: %s", uname)
// Assume password is token
authToken = passwd
} else {
log.Trace("Basic Authorization: Attempting login with username as token")
}
// check oauth2 token
uid := CheckOAuthAccessToken(authToken)
if uid != 0 {
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)
u, err := user_model.GetUserByID(req.Context(), uid)
if err != nil {
log.Error("GetUserByID: %v", err)
return nil, err
}
store.GetData()["IsApiToken"] = true
return u, nil
}
// check personal access token
token, err := auth_model.GetAccessTokenBySHA(authToken)
if err == nil {
log.Trace("Basic Authorization: Valid AccessToken for user[%d]", uid)
u, err := user_model.GetUserByID(req.Context(), token.UID)
if err != nil {
log.Error("GetUserByID: %v", err)
return nil, err
}
token.UpdatedUnix = timeutil.TimeStampNow()
if err = auth_model.UpdateAccessToken(token); err != nil {
log.Error("UpdateAccessToken: %v", err)
}
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = token.Scope
return u, nil
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
log.Error("GetAccessTokenBySha: %v", err)
}
// check task token
task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken)
if err == nil && task != nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = task.ID
return user_model.NewActionsUser(), nil
}
if !setting.Service.EnableBasicAuth {
return nil, nil
}
log.Trace("Basic Authorization: Attempting SignIn for %s", uname)
u, source, err := UserSignIn(uname, passwd)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
log.Error("UserSignIn: %v", err)
}
return nil, err
}
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
if err := validateTOTP(req, u); err != nil {
return nil, err
}
}
log.Trace("Basic Authorization: Logged in user %-v", u)
return u, nil
}
func getOtpHeader(header http.Header) string {
otpHeader := header.Get("X-Gitea-OTP")
if forgejoHeader := header.Get("X-Forgejo-OTP"); forgejoHeader != "" {
otpHeader = forgejoHeader
}
return otpHeader
}
func validateTOTP(req *http.Request, u *user_model.User) error {
twofa, err := auth_model.GetTwoFactorByUID(u.ID)
if err != nil {
if auth_model.IsErrTwoFactorNotEnrolled(err) {
// No 2FA enrollment for this user
return nil
}
return err
}
if ok, err := twofa.ValidateTOTP(getOtpHeader(req.Header)); err != nil {
return err
} else if !ok {
return util.NewInvalidArgumentErrorf("invalid provided OTP")
}
return nil
}