mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-18 15:47:37 +02:00
When the API returns 401 Unauthorized, the log output at info/debug level gave no indication of why the authentication failed. All detail was buried at trace level. Add log.Warn calls at the key auth failure points so operators can see the reason for authentication failures without enabling trace logging: - routers/api/v1/api.go: log method, path, remote IP and reason before returning 401 - services/auth/oauth2.go: log JWT parse failures, grant-not-found and token expiry at Warn level - services/auth/basic.go: log when no token type matched instead of returning nil silently Assisted-by: claude-sonnet-4-6
203 lines
6.4 KiB
Go
203 lines
6.4 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"
|
|
|
|
actions_model "gitea.dev/models/actions"
|
|
auth_model "gitea.dev/models/auth"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/auth/httpauth"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/setting"
|
|
"gitea.dev/modules/timeutil"
|
|
"gitea.dev/modules/util"
|
|
)
|
|
|
|
// Ensure the struct implements the interface.
|
|
var (
|
|
_ Method = &Basic{}
|
|
)
|
|
|
|
// BasicMethodName is the constant name of the basic authentication method
|
|
const (
|
|
BasicMethodName = "basic"
|
|
AccessTokenMethodName = "access_token"
|
|
OAuth2TokenMethodName = "oauth2_token"
|
|
ActionTokenMethodName = "action_token"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
func (b *Basic) parseAuthBasic(req *http.Request) (ret struct{ authToken, uname, passwd string }) {
|
|
authHeader := req.Header.Get("Authorization")
|
|
if authHeader == "" {
|
|
return ret
|
|
}
|
|
parsed, ok := httpauth.ParseAuthorizationHeader(authHeader)
|
|
if !ok || parsed.BasicAuth == nil {
|
|
return ret
|
|
}
|
|
uname, passwd := parsed.BasicAuth.Username, parsed.BasicAuth.Password
|
|
|
|
// 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")
|
|
}
|
|
ret.authToken, ret.uname, ret.passwd = authToken, uname, passwd
|
|
return ret
|
|
}
|
|
|
|
// VerifyAuthToken only the access token provided as parameter, used by other auth methods that want to reuse access token verification logic
|
|
func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore, authToken string) (*user_model.User, error) {
|
|
// get oauth2 token's user's ID
|
|
accessTokenScope, uid := GetOAuthAccessTokenScopeAndUserID(req.Context(), 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()["LoginMethod"] = OAuth2TokenMethodName
|
|
store.GetData()["IsApiToken"] = true
|
|
store.GetData()["ApiTokenScope"] = accessTokenScope
|
|
return u, nil
|
|
}
|
|
|
|
// check personal access token
|
|
token, err := auth_model.GetAccessTokenBySHA(req.Context(), authToken)
|
|
if err == nil {
|
|
log.Trace("Basic Authorization: Valid AccessToken for user[%d]", uid)
|
|
u, err := user_model.GetUserByID(req.Context(), token.UID)
|
|
if err != nil {
|
|
log.Error("GetUserByID: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
token.UpdatedUnix = timeutil.TimeStampNow()
|
|
if err = auth_model.UpdateAccessToken(req.Context(), token); err != nil {
|
|
log.Error("UpdateAccessToken: %v", err)
|
|
}
|
|
|
|
store.GetData()["LoginMethod"] = AccessTokenMethodName
|
|
store.GetData()["IsApiToken"] = true
|
|
store.GetData()["ApiTokenScope"] = token.Scope
|
|
return u, nil
|
|
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
|
|
log.Error("GetAccessTokenBySha: %v", err)
|
|
}
|
|
|
|
// check task token
|
|
task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken)
|
|
if err == nil && task != nil {
|
|
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
|
|
store.GetData()["LoginMethod"] = ActionTokenMethodName
|
|
return user_model.NewActionsUserWithTaskID(task.ID), nil
|
|
}
|
|
log.Warn("Basic Authorization: token not found for any known token type")
|
|
return nil, nil //nolint:nilnil // the auth method is not applicable
|
|
}
|
|
|
|
// 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) {
|
|
parseBasicRet := b.parseAuthBasic(req)
|
|
authToken, uname, passwd := parseBasicRet.authToken, parseBasicRet.uname, parseBasicRet.passwd
|
|
if authToken == "" && uname == "" {
|
|
return nil, nil //nolint:nilnil // the auth method is not applicable
|
|
}
|
|
u, err := b.VerifyAuthToken(req, w, store, sess, authToken)
|
|
if u != nil || err != nil {
|
|
return u, err
|
|
}
|
|
|
|
if !setting.Service.EnableBasicAuth {
|
|
return nil, nil //nolint:nilnil // the auth method is not applicable
|
|
}
|
|
|
|
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)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if !source.TwoFactorShouldSkip() {
|
|
// Check if the user has WebAuthn registration
|
|
hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hasWebAuthn {
|
|
return nil, ErrUserAuthMessage("basic authorization is not allowed while WebAuthn enrolled")
|
|
}
|
|
|
|
if err := validateTOTP(req, u); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
store.GetData()["LoginMethod"] = BasicMethodName
|
|
log.Trace("Basic Authorization: Logged in user %-v", u)
|
|
|
|
return u, nil
|
|
}
|
|
|
|
func validateTOTP(req *http.Request, u *user_model.User) error {
|
|
twofa, err := auth_model.GetTwoFactorByUID(req.Context(), 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(req.Header.Get("X-Gitea-OTP")); err != nil {
|
|
return err
|
|
} else if !ok {
|
|
return util.NewInvalidArgumentErrorf("invalid provided OTP")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GetAccessScope(store DataStore) auth_model.AccessTokenScope {
|
|
if v, ok := store.GetData()["ApiTokenScope"]; ok {
|
|
return v.(auth_model.AccessTokenScope)
|
|
}
|
|
switch store.GetData()["LoginMethod"] {
|
|
case OAuth2TokenMethodName:
|
|
fallthrough
|
|
case BasicMethodName, AccessTokenMethodName:
|
|
return auth_model.AccessTokenScopeAll
|
|
case ActionTokenMethodName:
|
|
fallthrough
|
|
default:
|
|
return ""
|
|
}
|
|
}
|