feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
+5 -5
View File
@@ -26,7 +26,7 @@ var (
func TwoFactor(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("twofa")
if CheckAutoLogin(ctx) {
if performAutoLogin(ctx) {
return
}
@@ -92,14 +92,14 @@ func TwoFactorPost(ctx *context.Context) {
return
}
ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{})
ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{})
}
// TwoFactorScratch shows the scratch code form for two-factor authentication.
func TwoFactorScratch(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("twofa_scratch")
if CheckAutoLogin(ctx) {
if performAutoLogin(ctx) {
return
}
@@ -151,7 +151,7 @@ func TwoFactorScratchPost(ctx *context.Context) {
return
}
handleSignInFull(ctx, u, remember, false)
handleSignInFull(ctx, u, remember)
if ctx.Written() {
return
}
@@ -160,5 +160,5 @@ func TwoFactorScratchPost(ctx *context.Context) {
return
}
ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{})
ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{})
}
+180 -102
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"html/template"
"net/http"
"net/url"
"strings"
"code.gitea.io/gitea/models/auth"
@@ -44,6 +45,33 @@ const (
TplActivatePrompt templates.TplName = "user/auth/activate_prompt" // for showing a message for user activation
)
type CommonAuthOptions struct {
EnableCaptcha bool
}
func prepareCommonAuthPageData(ctx *context.Context, opt CommonAuthOptions) {
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
// for OpenID Connect
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
if opt.EnableCaptcha {
ctx.Data["EnableCaptcha"] = true
ctx.Data["RecaptchaAPIScriptURL"] = strings.TrimSuffix(setting.Service.RecaptchaURL, "/") + "/api.js"
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
ctx.Data["McaptchaURL"] = strings.TrimSuffix(setting.Service.McaptchaURL, "/")
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
if setting.Service.CaptchaType == setting.ImageCaptcha {
ctx.Data["Captcha"] = context.GetImageCaptcha()
}
}
}
// autoSignIn reads cookie and try to auto-login.
func autoSignIn(ctx *context.Context) (bool, error) {
isSucceed := false
@@ -102,7 +130,6 @@ func autoSignIn(ctx *context.Context) (bool, error) {
return false, err
}
ctx.Csrf.PrepareForSessionUser(ctx)
return true, nil
}
@@ -127,20 +154,52 @@ func resetLocale(ctx *context.Context, u *user_model.User) error {
return nil
}
func RedirectAfterLogin(ctx *context.Context) {
func rememberAuthRedirectLink(ctx *context.Context) {
redirectTo := ctx.FormString("redirect_to")
if redirectTo == "" {
redirectTo = ctx.GetSiteCookie("redirect_to")
if ref, err := url.Parse(ctx.Req.Referer()); err == nil && httplib.IsCurrentGiteaSiteURL(ctx, ctx.Req.Referer()) {
// the request paths starting with "/user/" are either:
// * auth related pages: don't redirect back to them
// * user settings pages: they have "require sign-in" protection already, no "referer redirect" would happen
skipRefererRedirect := strings.HasPrefix(ref.Path, setting.AppSubURL+"/user/")
if !skipRefererRedirect {
redirectTo = ref.RequestURI()
}
}
}
middleware.DeleteRedirectToCookie(ctx.Resp)
nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL)
if setting.LandingPageURL == setting.LandingPageLogin {
nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page
if redirectTo != "" {
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
}
ctx.RedirectToCurrentSite(redirectTo, nextRedirectTo)
}
func CheckAutoLogin(ctx *context.Context) bool {
func consumeAuthRedirectLink(ctx *context.Context) string {
redirects := []string{ctx.FormString("redirect_to"), middleware.GetRedirectToCookie(ctx.Req)}
middleware.DeleteRedirectToCookie(ctx.Resp)
if setting.LandingPageURL == setting.LandingPageLogin {
redirects = append(redirects, setting.AppSubURL+"/") // do not cycle-redirect to the login page
} else {
redirects = append(redirects, setting.AppSubURL+string(setting.LandingPageURL))
}
for _, link := range redirects {
if link != "" && httplib.IsCurrentGiteaSiteURL(ctx, link) {
return link
}
}
return setting.AppSubURL + "/"
}
func redirectAfterAuth(ctx *context.Context) {
if setting.Config().Instance.MaintenanceMode.Value(ctx).IsActive() {
// in maintenance mode, redirect to admin dashboard, it is the only accessible page
ctx.Redirect(setting.AppSubURL + "/-/admin")
return
}
ctx.RedirectToCurrentSite(consumeAuthRedirectLink(ctx))
}
func performAutoLogin(ctx *context.Context) bool {
rememberAuthRedirectLink(ctx)
isSucceed, err := autoSignIn(ctx) // try to auto-login
if err != nil {
if errors.Is(err, auth_service.ErrAuthTokenInvalidHash) {
@@ -151,45 +210,73 @@ func CheckAutoLogin(ctx *context.Context) bool {
return true
}
redirectTo := ctx.FormString("redirect_to")
if len(redirectTo) > 0 {
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
}
if isSucceed {
RedirectAfterLogin(ctx)
redirectAfterAuth(ctx)
return true
}
return false
}
func prepareSignInPageData(ctx *context.Context) {
func performAutoLoginOAuth2(ctx *context.Context, data *preparedSignInData) bool {
// If only 1 OAuth provider is present and other login methods are disabled, redirect to the OAuth provider.
onlySingleOAuth2 := len(data.oauth2Providers) == 1 &&
!setting.Service.EnablePasswordSignInForm &&
!setting.Service.EnableOpenIDSignIn &&
!setting.Service.EnablePasskeyAuth &&
!data.enableSSPI
if !onlySingleOAuth2 {
return false
}
skipToOAuthURL := setting.AppSubURL + "/user/oauth2/" + url.PathEscape(data.oauth2Providers[0].DisplayName())
if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" {
skipToOAuthURL += "?redirect_to=" + url.QueryEscape(redirectTo)
}
ctx.Redirect(skipToOAuthURL)
return true
}
type preparedSignInData struct {
oauth2Providers []oauth2.Provider
enableSSPI bool
}
func prepareSignInPageData(ctx *context.Context) (ret preparedSignInData) {
var err error
ret.enableSSPI = auth.IsSSPIEnabled(ctx)
ret.oauth2Providers, err = oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
log.Error("Failed to get OAuth2 providers: %v", err)
}
ctx.Data["Title"] = ctx.Tr("sign_in")
ctx.Data["OAuth2Providers"], _ = oauth2.GetOAuth2Providers(ctx, optional.Some(true))
ctx.Data["OAuth2Providers"] = ret.oauth2Providers
ctx.Data["Title"] = ctx.Tr("sign_in")
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsLogin"] = true
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
ctx.Data["EnableSSPI"] = ret.enableSSPI
if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
context.SetCaptchaData(ctx)
}
prepareCommonAuthPageData(ctx, CommonAuthOptions{
EnableCaptcha: setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin,
})
return ret
}
// SignIn render sign in page
func SignIn(ctx *context.Context) {
if CheckAutoLogin(ctx) {
if performAutoLogin(ctx) {
return
}
if ctx.IsSigned {
RedirectAfterLogin(ctx)
redirectAfterAuth(ctx)
return
}
data := prepareSignInPageData(ctx)
if performAutoLoginOAuth2(ctx, &data) {
return
}
prepareSignInPageData(ctx)
ctx.HTML(http.StatusOK, tplSignIn)
}
@@ -218,10 +305,10 @@ func SignInPost(ctx *context.Context) {
u, source, err := auth_service.UserSignIn(ctx, form.UserName, form.Password)
if err != nil {
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
} else if user_model.IsErrEmailAlreadyUsed(err) {
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignIn, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplSignIn, &form)
log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
} else if user_model.IsErrUserProhibitLogin(err) {
log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
@@ -296,19 +383,19 @@ func SignInPost(ctx *context.Context) {
// This handles the final part of the sign-in process of the user.
func handleSignIn(ctx *context.Context, u *user_model.User, remember bool) {
redirect := handleSignInFull(ctx, u, remember, true)
handleSignInFull(ctx, u, remember)
if ctx.Written() {
return
}
ctx.Redirect(redirect)
redirectAfterAuth(ctx)
}
func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRedirect bool) string {
func handleSignInFull(ctx *context.Context, u *user_model.User, remember bool) {
if remember {
nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID)
if err != nil {
ctx.ServerError("CreateAuthTokenForUserID", err)
return setting.AppSubURL + "/"
return
}
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
@@ -317,7 +404,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
if err != nil {
ctx.ServerError("HasTwoFactorOrWebAuthn", err)
return setting.AppSubURL + "/"
return
}
if err := updateSession(ctx, []string{
@@ -336,7 +423,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
}); err != nil {
ctx.ServerError("RegenerateSession", err)
return setting.AppSubURL + "/"
return
}
// Language setting of the user overwrites the one previously set
@@ -347,7 +434,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
ctx.ServerError("UpdateUser Language", fmt.Errorf("Error updating user language [user: %d, locale: %s]", u.ID, ctx.Locale.Language()))
return setting.AppSubURL + "/"
return
}
}
@@ -357,27 +444,11 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
}
// force to generate a new CSRF token
ctx.Csrf.PrepareForSessionUser(ctx)
// Register last login
if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
ctx.ServerError("UpdateUser", err)
return setting.AppSubURL + "/"
return
}
if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" && httplib.IsCurrentGiteaSiteURL(ctx, redirectTo) {
middleware.DeleteRedirectToCookie(ctx.Resp)
if obeyRedirect {
ctx.RedirectToCurrentSite(redirectTo)
}
return redirectTo
}
if obeyRedirect {
ctx.Redirect(setting.AppSubURL + "/")
}
return setting.AppSubURL + "/"
}
// extractUserNameFromOAuth2 tries to extract a normalized username from the given OAuth2 user.
@@ -403,7 +474,6 @@ func HandleSignOut(ctx *context.Context) {
_ = ctx.Session.Flush()
_ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
ctx.DeleteSiteCookie(setting.CookieRememberName)
ctx.Csrf.DeleteCookie(ctx)
middleware.DeleteRedirectToCookie(ctx.Resp)
}
@@ -415,57 +485,74 @@ func SignOut(ctx *context.Context) {
Data: ctx.Session.ID(),
})
}
// prepare the sign-out URL before destroying the session
redirectTo := buildSignOutRedirectURL(ctx)
HandleSignOut(ctx)
ctx.JSONRedirect(setting.AppSubURL + "/")
ctx.Redirect(redirectTo)
}
// SignUp render the register page
func SignUp(ctx *context.Context) {
func buildSignOutRedirectURL(ctx *context.Context) string {
if ctx.Doer != nil && ctx.Doer.LoginType == auth.OAuth2 {
if s := buildOIDCEndSessionURL(ctx, ctx.Doer); s != "" {
return s
}
}
// The assumption is: if reverse proxy auth is enabled, then the users should only sign-in via reverse proxy auth.
// TODO: in the future, if we need to distinguish different sign-in methods, we need to save the sign-in method in session and check here
if setting.Service.EnableReverseProxyAuth && setting.ReverseProxyLogoutRedirect != "" {
return setting.ReverseProxyLogoutRedirect
}
return setting.AppSubURL + "/"
}
func prepareSignUpPageData(ctx *context.Context) bool {
ctx.Data["Title"] = ctx.Tr("sign_up")
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
ctx.Data["PageIsSignUp"] = true
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
hasUsers, _ := user_model.HasUsers(ctx)
hasUsers, err := user_model.HasUsers(ctx)
if err != nil {
ctx.ServerError("HasUsers", err)
return false
}
ctx.Data["IsFirstTimeRegistration"] = !hasUsers.HasAnyUser
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignUp", err)
return
ctx.ServerError("GetOAuth2Providers", err)
return false
}
ctx.Data["OAuth2Providers"] = oauth2Providers
context.SetCaptchaData(ctx)
ctx.Data["PageIsSignUp"] = true
prepareCommonAuthPageData(ctx, CommonAuthOptions{
EnableCaptcha: setting.Service.EnableCaptcha,
})
// Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration
redirectTo := ctx.FormString("redirect_to")
if len(redirectTo) > 0 {
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
}
return true
}
// SignUp render the register page
func SignUp(ctx *context.Context) {
if !prepareSignUpPageData(ctx) {
return
}
rememberAuthRedirectLink(ctx)
ctx.HTML(http.StatusOK, tplSignUp)
}
// SignUpPost response for sign up information submission
func SignUpPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RegisterForm)
ctx.Data["Title"] = ctx.Tr("sign_up")
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignUp", err)
if !prepareSignUpPageData(ctx) {
return
}
ctx.Data["OAuth2Providers"] = oauth2Providers
context.SetCaptchaData(ctx)
ctx.Data["PageIsSignUp"] = true
form := web.GetForm(ctx).(*forms.RegisterForm)
// Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true
if setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration {
@@ -484,23 +571,23 @@ func SignUpPost(ctx *context.Context) {
}
if !form.IsEmailDomainAllowed() {
ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form)
return
}
if form.Password != form.Retype {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplSignUp, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplSignUp, &form)
return
}
if len(form.Password) < setting.MinPasswordLength {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form)
return
}
if !password.IsComplexEnough(form.Password) {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form)
ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplSignUp, &form)
return
}
if err := password.IsPwned(ctx, form.Password); err != nil {
@@ -510,7 +597,7 @@ func SignUpPost(ctx *context.Context) {
errMsg = ctx.Tr("auth.password_pwned_err")
}
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(errMsg, tplSignUp, &form)
ctx.RenderWithErrDeprecated(errMsg, tplSignUp, &form)
return
}
@@ -551,16 +638,15 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
case setting.OAuth2AccountLinkingAuto:
var user *user_model.User
user = &user_model.User{Name: u.Name}
hasUser, err := user_model.GetUser(ctx, user)
hasUser, err := user_model.GetIndividualUser(ctx, user)
if !hasUser || err != nil {
user = &user_model.User{Email: u.Email}
hasUser, err = user_model.GetUser(ctx, user)
hasUser, err = user_model.GetIndividualUser(ctx, user)
if !hasUser || err != nil {
ctx.ServerError("UserLinkAccount", err)
return false
}
}
// TODO: probably we should respect 'remember' user's choice...
oauth2LinkAccount(ctx, user, possibleLinkAccountData, true)
return false // user is already created here, all redirects are handled
@@ -580,25 +666,25 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
switch {
case user_model.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tpl, form)
case user_model.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tpl, form)
case user_model.IsErrEmailCharIsNotSupported(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tpl, form)
case user_model.IsErrEmailInvalid(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tpl, form)
case db.IsErrNameReserved(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
case db.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
case db.IsErrNameCharsNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tpl, form)
default:
ctx.ServerError("CreateUser", err)
}
@@ -811,8 +897,6 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
return
}
ctx.Csrf.PrepareForSessionUser(ctx)
if err := resetLocale(ctx, user); err != nil {
ctx.ServerError("resetLocale", err)
return
@@ -824,13 +908,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
}
ctx.Flash.Success(ctx.Tr("auth.account_activated"))
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
middleware.DeleteRedirectToCookie(ctx.Resp)
ctx.RedirectToCurrentSite(redirectTo)
return
}
ctx.Redirect(setting.AppSubURL + "/")
redirectAfterAuth(ctx)
}
// ActivateEmail render the activate email page
@@ -4,11 +4,14 @@
package auth
import (
"html/template"
"net/http"
"net/http/httptest"
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
@@ -19,6 +22,7 @@ import (
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
@@ -29,10 +33,10 @@ func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
IsActive: true,
Cfg: &cfg,
})
assert.NoError(t, err)
require.NoError(t, err)
}
func TestUserLogin(t *testing.T) {
func TestWebAuthUserLogin(t *testing.T) {
ctx, resp := contexttest.MockContext(t, "/user/login")
SignIn(ctx)
assert.Equal(t, http.StatusOK, resp.Code)
@@ -60,19 +64,19 @@ func TestUserLogin(t *testing.T) {
assert.Equal(t, "/", test.RedirectURL(resp))
}
func TestSignUpOAuth2Login(t *testing.T) {
func TestWebAuthOAuth2(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
_ = oauth2.Init(t.Context())
addOAuth2Source(t, "dummy-auth-source", oauth2.Source{})
addOAuth2Source(t, "dummy+auth's source", oauth2.Source{})
t.Run("OAuth2MissingField", func(t *testing.T) {
defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
return goth.User{Provider: "dummy-auth-source", UserID: "dummy-user"}, nil
return goth.User{Provider: "dummy+auth's source", UserID: "dummy-user"}, nil
})()
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt)
ctx.SetPathParam("provider", "dummy-auth-source")
ctx, resp := contexttest.MockContext(t, "/user/oauth2/..../callback?code=dummy-code", mockOpt)
ctx.SetPathParamRaw("provider", "dummy+auth%27s%20source")
SignInOAuthCallback(ctx)
assert.Equal(t, http.StatusSeeOther, resp.Code)
assert.Equal(t, "/user/link_account", test.RedirectURL(resp))
@@ -80,16 +84,86 @@ func TestSignUpOAuth2Login(t *testing.T) {
// then the user will be redirected to the link account page, and see a message about the missing fields
ctx, _ = contexttest.MockContext(t, "/user/link_account", mockOpt)
LinkAccount(ctx)
assert.EqualValues(t, "auth.oauth_callback_unable_auto_reg:dummy-auth-source,email", ctx.Data["AutoRegistrationFailedPrompt"])
assert.Equal(t, template.HTML("auth.oauth_callback_unable_auto_reg:dummy+auth&#39;s source,email"), ctx.Data["AutoRegistrationFailedPrompt"])
})
t.Run("OAuth2CallbackError", func(t *testing.T) {
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback", mockOpt)
ctx.SetPathParam("provider", "dummy-auth-source")
ctx, resp := contexttest.MockContext(t, "/user/oauth2/...../callback", mockOpt)
ctx.SetPathParamRaw("provider", "dummy+auth%27s%20source")
SignInOAuthCallback(ctx)
assert.Equal(t, http.StatusSeeOther, resp.Code)
assert.Equal(t, "/user/login", test.RedirectURL(resp))
assert.Contains(t, ctx.Flash.ErrorMsg, "auth.oauth.signin.error.general")
})
t.Run("RedirectSingleProvider", func(t *testing.T) {
enablePassword := &setting.Service.EnablePasswordSignInForm
enableOpenID := &setting.Service.EnableOpenIDSignIn
enablePasskey := &setting.Service.EnablePasskeyAuth
defer test.MockVariableValue(enablePassword, false)()
defer test.MockVariableValue(enableOpenID, false)()
defer test.MockVariableValue(enablePasskey, false)()
testSignIn := func(t *testing.T, link string, expectedCode int, expectedRedirect string) {
ctx, resp := contexttest.MockContext(t, link)
SignIn(ctx)
assert.Equal(t, expectedCode, resp.Code)
if expectedCode == http.StatusSeeOther {
assert.Equal(t, expectedRedirect, test.RedirectURL(resp))
}
}
testSignIn(t, "/user/login", http.StatusSeeOther, "/user/oauth2/dummy+auth%27s%20source")
testSignIn(t, "/user/login?redirect_to=/", http.StatusSeeOther, "/user/oauth2/dummy+auth%27s%20source?redirect_to=%2F")
*enablePassword, *enableOpenID, *enablePasskey = true, false, false
testSignIn(t, "/user/login", http.StatusOK, "")
*enablePassword, *enableOpenID, *enablePasskey = false, true, false
testSignIn(t, "/user/login", http.StatusOK, "")
*enablePassword, *enableOpenID, *enablePasskey = false, false, true
testSignIn(t, "/user/login", http.StatusOK, "")
*enablePassword, *enableOpenID, *enablePasskey = false, false, false
addOAuth2Source(t, "dummy-auth-source-2", oauth2.Source{})
testSignIn(t, "/user/login", http.StatusOK, "")
})
t.Run("OIDCLogout", func(t *testing.T) {
var mockServer *httptest.Server
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/.well-known/openid-configuration":
_, _ = w.Write([]byte(`{
"issuer": "` + mockServer.URL + `",
"authorization_endpoint": "` + mockServer.URL + `/authorize",
"token_endpoint": "` + mockServer.URL + `/token",
"userinfo_endpoint": "` + mockServer.URL + `/userinfo",
"end_session_endpoint": "https://example.com/oidc-logout?oidc-key=oidc-val"
}`))
default:
http.NotFound(w, r)
}
}))
defer mockServer.Close()
addOAuth2Source(t, "oidc-auth-source", oauth2.Source{
Provider: "openidConnect",
ClientID: "mock-client-id",
OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration",
})
authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(t.Context(), "oidc-auth-source")
require.NoError(t, err)
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
ctx, resp := contexttest.MockContext(t, "/user/logout", mockOpt)
ctx.Doer = &user_model.User{ID: 1, LoginType: auth_model.OAuth2, LoginSource: authSource.ID}
SignOut(ctx)
assert.Equal(t, http.StatusSeeOther, resp.Code)
u, err := url.Parse(test.RedirectURL(resp))
require.NoError(t, err)
expectedValues := url.Values{"oidc-key": []string{"oidc-val"}, "post_logout_redirect_uri": []string{setting.AppURL}, "client_id": []string{"mock-client-id"}}
assert.Equal(t, expectedValues, u.Query())
u.RawQuery = ""
assert.Equal(t, "https://example.com/oidc-logout", u.String())
})
}
@@ -24,30 +24,27 @@ import (
var tplLinkAccount templates.TplName = "user/auth/link_account"
// LinkAccount shows the page where the user can decide to login or create a new account
func LinkAccount(ctx *context.Context) {
// FIXME: these common template variables should be prepared in one common function, but not just copy-paste again and again.
func prepareLinkAccountPageData(ctx *context.Context) {
// TODO Make insecure passwords optional for local accounts also, once email-based Second-Factor Auth is available
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
ctx.Data["Title"] = ctx.Tr("link_account")
ctx.Data["LinkAccountMode"] = true
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
ctx.Data["Captcha"] = context.GetImageCaptcha()
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
// use this to set the right link into the signIn and signUp templates in the link_account template
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
ctx.Data["ShowRegistrationButton"] = false
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
prepareCommonAuthPageData(ctx, CommonAuthOptions{
EnableCaptcha: setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha,
})
}
// LinkAccount shows the page where the user can decide to login or create a new account
func LinkAccount(ctx *context.Context) {
prepareLinkAccountPageData(ctx)
linkAccountData := oauth2GetLinkAccountData(ctx)
@@ -99,10 +96,10 @@ func LinkAccount(ctx *context.Context) {
func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl templates.TplName, invoker string, err error) {
if errors.Is(err, util.ErrNotExist) {
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm)
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm)
} else if errors.Is(err, util.ErrInvalidArgument) {
ctx.Data["user_exists"] = true
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm)
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm)
} else if user_model.IsErrUserProhibitLogin(err) {
ctx.Data["user_exists"] = true
log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err)
@@ -126,28 +123,10 @@ func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl
// LinkAccountPostSignIn handle the coupling of external account with another account using signIn
func LinkAccountPostSignIn(ctx *context.Context) {
signInForm := web.GetForm(ctx).(*forms.SignInForm)
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
ctx.Data["Title"] = ctx.Tr("link_account")
ctx.Data["LinkAccountMode"] = true
ctx.Data["LinkAccountModeSignIn"] = true
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["Captcha"] = context.GetImageCaptcha()
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
// use this to set the right link into the signIn and signUp templates in the link_account template
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
ctx.Data["LinkAccountModeSignIn"] = true
prepareLinkAccountPageData(ctx)
linkAccountData := oauth2GetLinkAccountData(ctx)
if linkAccountData == nil {
@@ -218,30 +197,10 @@ func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData
// LinkAccountPostRegister handle the creation of a new account for an external account using signUp
func LinkAccountPostRegister(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RegisterForm)
// TODO Make insecure passwords optional for local accounts also,
// once email-based Second-Factor Auth is available
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
ctx.Data["Title"] = ctx.Tr("link_account")
ctx.Data["LinkAccountMode"] = true
ctx.Data["LinkAccountModeRegister"] = true
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["Captcha"] = context.GetImageCaptcha()
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
// use this to set the right link into the signIn and signUp templates in the link_account template
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
ctx.Data["LinkAccountModeRegister"] = true
prepareLinkAccountPageData(ctx)
linkAccountData := oauth2GetLinkAccountData(ctx)
if linkAccountData == nil {
@@ -266,7 +225,7 @@ func LinkAccountPostRegister(ctx *context.Context) {
}
if !form.IsEmailDomainAllowed() {
ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form)
return
}
@@ -280,12 +239,12 @@ func LinkAccountPostRegister(ctx *context.Context) {
} else {
if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplLinkAccount, &form)
return
}
if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form)
return
}
}
@@ -304,6 +263,11 @@ func LinkAccountPostRegister(ctx *context.Context) {
return
}
oauth2SignInSync(ctx, linkAccountData.AuthSourceID, u, linkAccountData.GothUser)
if ctx.Written() {
return
}
authSource, err := auth.GetSourceByID(ctx, linkAccountData.AuthSourceID)
if err != nil {
ctx.ServerError("GetSourceByID", err)
+79 -33
View File
@@ -10,18 +10,20 @@ import (
"html"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
auth_module "code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
source_service "code.gitea.io/gitea/services/auth/source"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/context"
@@ -42,10 +44,7 @@ func SignInOAuth(ctx *context.Context) {
return
}
redirectTo := ctx.FormString("redirect_to")
if len(redirectTo) > 0 {
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
}
rememberAuthRedirectLink(ctx)
// try to do a direct callback flow, so we don't authenticate the user again but use the valid accesstoken to get the user
user, gothUser, err := oAuth2UserLoginCallback(ctx, authSource, ctx.Req, ctx.Resp)
@@ -120,7 +119,7 @@ func SignInOAuthCallback(ctx *context.Context) {
return
}
if err, ok := err.(*go_oauth2.RetrieveError); ok {
ctx.Flash.Error("OAuth2 RetrieveError: "+err.Error(), true)
ctx.Flash.Error("OAuth2 RetrieveError: " + err.Error())
ctx.Redirect(setting.AppSubURL + "/user/login")
return
}
@@ -278,7 +277,7 @@ type LinkAccountData struct {
}
func init() {
gob.Register(LinkAccountData{})
gob.Register(LinkAccountData{}) // TODO: CHI-SESSION-GOB-REGISTER
}
func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
@@ -303,21 +302,42 @@ func showLinkingLogin(ctx *context.Context, authSourceID int64, gothUser goth.Us
ctx.Redirect(setting.AppSubURL + "/user/link_account")
}
func oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
resp, err := http.Get(url)
if err == nil {
defer func() {
_ = resp.Body.Close()
}()
}
// ignore any error
if err == nil && resp.StatusCode == http.StatusOK {
data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize {
_ = user_service.UploadAvatar(ctx, u, data)
}
}
var oauth2AvatarHTTPClient = &http.Client{Timeout: 30 * time.Second}
func oauth2UpdateAvatarIfNeed(ctx *context.Context, avatarURL string, u *user_model.User) {
if !setting.OAuth2Client.UpdateAvatar || len(avatarURL) == 0 {
return
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, avatarURL, nil)
if err != nil {
log.Warn("invalid avatar URL %q: %v", avatarURL, err)
return
}
// Some hosts (e.g. Wikimedia) reject Go's default User-Agent.
req.Header.Set("User-Agent", "Gitea "+setting.AppVer)
resp, err := oauth2AvatarHTTPClient.Do(req)
if err != nil {
log.Warn("fetch %q failed: %v", avatarURL, err)
return
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
log.Warn("fetch %q returned status %d", avatarURL, resp.StatusCode)
return
}
data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
if err != nil {
log.Warn("read body from %q failed: %v", avatarURL, err)
return
}
if int64(len(data)) > setting.Avatar.MaxFileSize {
log.Warn("avatar from %q exceeds max size %d", avatarURL, setting.Avatar.MaxFileSize)
return
}
if err := user_service.UploadAvatar(ctx, u, data); err != nil {
log.Warn("UploadAvatar for user %q failed: %v", u.Name, err)
}
}
@@ -393,21 +413,12 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
return
}
// force to generate a new CSRF token
ctx.Csrf.PrepareForSessionUser(ctx)
if err := resetLocale(ctx, u); err != nil {
ctx.ServerError("resetLocale", err)
return
}
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
middleware.DeleteRedirectToCookie(ctx.Resp)
ctx.RedirectToCurrentSite(redirectTo)
return
}
ctx.Redirect(setting.AppSubURL + "/")
redirectAfterAuth(ctx)
return
}
@@ -493,7 +504,7 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ
LoginSource: authSource.ID,
}
hasUser, err := user_model.GetUser(ctx, user)
hasUser, err := user_model.GetIndividualUser(ctx, user)
if err != nil {
return nil, goth.User{}, err
}
@@ -519,3 +530,38 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ
// no user found to login
return nil, gothUser, nil
}
// buildOIDCEndSessionURL constructs an OIDC RP-Initiated Logout URL for the
// given user. Returns "" if the user's auth source is not OIDC or doesn't
// advertise an end_session_endpoint.
func buildOIDCEndSessionURL(ctx *context.Context, doer *user_model.User) string {
authSource, err := auth.GetSourceByID(ctx, doer.LoginSource)
if err != nil {
log.Error("Failed to get auth source for OIDC logout (source=%d): %v", doer.LoginSource, err)
return ""
}
oauth2Cfg, ok := authSource.Cfg.(*oauth2.Source)
if !ok {
return ""
}
endSessionEndpoint := oauth2.GetOIDCEndSessionEndpoint(authSource.Name)
if endSessionEndpoint == "" {
return ""
}
endSessionURL, err := url.Parse(endSessionEndpoint)
if err != nil {
log.Error("Failed to parse end_session_endpoint %q: %v", endSessionEndpoint, err)
return ""
}
// RP-Initiated Logout 1.0: use client_id to identify the client to the IdP.
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
params := endSessionURL.Query()
params.Set("client_id", oauth2Cfg.ClientID)
params.Set("post_logout_redirect_uri", httplib.GuessCurrentAppURL(ctx))
endSessionURL.RawQuery = params.Encode()
return endSessionURL.String()
}
@@ -4,6 +4,7 @@
package auth
import (
"errors"
"fmt"
"html"
"html/template"
@@ -179,11 +180,11 @@ func AuthorizeOAuth(ctx *context.Context) {
errs := binding.Errors{}
errs = form.Validate(ctx.Req, errs)
if len(errs) > 0 {
errstring := ""
var errstring strings.Builder
for _, e := range errs {
errstring += e.Error() + "\n"
errstring.WriteString(e.Error() + "\n")
}
ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring))
ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring.String()))
return
}
@@ -230,8 +231,7 @@ func AuthorizeOAuth(ctx *context.Context) {
// pkce support
switch form.CodeChallengeMethod {
case "S256":
case "plain":
case "S256", "plain":
if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallengeMethod); err != nil {
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeServerError,
@@ -561,6 +561,13 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server
})
return
}
if grant.ApplicationID != app.ID {
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
ErrorDescription: "refresh token belongs to a different client",
})
return
}
// check if token got already used
if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) {
@@ -614,6 +621,14 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
})
return
}
if authorizationCode.IsExpired() {
_ = authorizationCode.Invalidate(ctx)
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
ErrorDescription: "authorization code expired",
})
return
}
// check if code verifier authorizes the client, PKCE support
if !authorizationCode.ValidateCodeChallenge(form.CodeVerifier) {
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
@@ -622,6 +637,13 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
})
return
}
if authorizationCode.RedirectURI != "" && form.RedirectURI != authorizationCode.RedirectURI {
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
ErrorDescription: "redirect_uri differs from the original authorization request",
})
return
}
// check if granted for this application
if authorizationCode.Grant.ApplicationID != app.ID {
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
@@ -632,9 +654,15 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
}
// remove token from database to deny duplicate usage
if err := authorizationCode.Invalidate(ctx); err != nil {
errDescription := "cannot process your request"
errCode := oauth2_provider.AccessTokenErrorCodeInvalidRequest
if errors.Is(err, auth.ErrOAuth2AuthorizationCodeInvalidated) {
errDescription = "authorization code already used"
errCode = oauth2_provider.AccessTokenErrorCodeInvalidGrant
}
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot proceed your request",
ErrorCode: errCode,
ErrorDescription: errDescription,
})
return
}
@@ -86,7 +86,7 @@ func oauth2UpdateSSHPubIfNeed(ctx *context.Context, authSource *auth.Source, got
if err != nil {
return err
}
if !asymkey_model.SynchronizePublicKeys(ctx, user, authSource, sshKeys) {
if !asymkey_model.SynchronizePublicKeys(ctx, user, authSource, sshKeys, false) {
return nil
}
return asymkey_service.RewriteAllPublicKeys(ctx)
+45 -50
View File
@@ -35,7 +35,7 @@ func SignInOpenID(ctx *context.Context) {
return
}
if CheckAutoLogin(ctx) {
if performAutoLogin(ctx) {
return
}
@@ -82,7 +82,7 @@ func SignInOpenIDPost(ctx *context.Context) {
id, err := openid.Normalize(form.Openid)
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form)
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &form)
return
}
form.Openid = id
@@ -91,7 +91,7 @@ func SignInOpenIDPost(ctx *context.Context) {
err = allowedOpenIDURI(id)
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form)
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &form)
return
}
@@ -99,7 +99,7 @@ func SignInOpenIDPost(ctx *context.Context) {
url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
if err != nil {
log.Error("Error in OpenID redirect URL: %s, %v", redirectTo, err.Error())
ctx.RenderWithErr("Unable to find OpenID provider in "+redirectTo, tplSignInOpenID, &form)
ctx.RenderWithErrDeprecated("Unable to find OpenID provider in "+redirectTo, tplSignInOpenID, &form)
return
}
@@ -129,7 +129,7 @@ func signInOpenIDVerify(ctx *context.Context) {
id, err := openid.Verify(fullURL)
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
@@ -143,7 +143,7 @@ func signInOpenIDVerify(ctx *context.Context) {
u, err := user_model.GetUserByOpenID(ctx, id)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
@@ -162,14 +162,14 @@ func signInOpenIDVerify(ctx *context.Context) {
parsedURL, err := url.Parse(fullURL)
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
values, err := url.ParseQuery(parsedURL.RawQuery)
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
@@ -183,7 +183,7 @@ func signInOpenIDVerify(ctx *context.Context) {
u, err = user_model.GetUserByEmail(ctx, email)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
@@ -199,7 +199,7 @@ func signInOpenIDVerify(ctx *context.Context) {
u, _ = user_model.GetUserByName(ctx, nickname)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
@@ -229,19 +229,26 @@ func signInOpenIDVerify(ctx *context.Context) {
}
}
// ConnectOpenID shows a form to connect an OpenID URI to an existing account
func ConnectOpenID(ctx *context.Context) {
oid, _ := ctx.Session.Get("openid_verified_uri").(string)
func prepareConnectOpenIDPageData(ctx *context.Context) (oid string) {
oid, _ = ctx.Session.Get("openid_verified_uri").(string)
if oid == "" {
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
return
return ""
}
ctx.Data["Title"] = "OpenID connect"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDConnect"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["OpenID"] = oid
prepareCommonAuthPageData(ctx, CommonAuthOptions{EnableCaptcha: false})
return oid
}
// ConnectOpenID shows a form to connect an OpenID URI to an existing account
func ConnectOpenID(ctx *context.Context) {
oid := prepareConnectOpenIDPageData(ctx)
if oid == "" {
return
}
userName, _ := ctx.Session.Get("openid_determined_username").(string)
if userName != "" {
ctx.Data["user_name"] = userName
@@ -252,16 +259,10 @@ func ConnectOpenID(ctx *context.Context) {
// ConnectOpenIDPost handles submission of a form to connect an OpenID URI to an existing account
func ConnectOpenIDPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.ConnectOpenIDForm)
oid, _ := ctx.Session.Get("openid_verified_uri").(string)
oid := prepareConnectOpenIDPageData(ctx)
if oid == "" {
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
return
}
ctx.Data["Title"] = "OpenID connect"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDConnect"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["OpenID"] = oid
u, _, err := auth.UserSignIn(ctx, form.UserName, form.Password)
if err != nil {
@@ -273,7 +274,7 @@ func ConnectOpenIDPost(ctx *context.Context) {
userOID := &user_model.UserOpenID{UID: u.ID, URI: oid}
if err = user_model.AddUserOpenID(ctx, userOID); err != nil {
if user_model.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form)
return
}
ctx.ServerError("AddUserOpenID", err)
@@ -287,28 +288,29 @@ func ConnectOpenIDPost(ctx *context.Context) {
handleSignIn(ctx, u, remember)
}
// RegisterOpenID shows a form to create a new user authenticated via an OpenID URI
func RegisterOpenID(ctx *context.Context) {
oid, _ := ctx.Session.Get("openid_verified_uri").(string)
func prepareRegisterOpenIDPageData(ctx *context.Context) (oid string) {
oid, _ = ctx.Session.Get("openid_verified_uri").(string)
if oid == "" {
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
return
return ""
}
ctx.Data["Title"] = "OpenID signup"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDRegister"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
ctx.Data["Captcha"] = context.GetImageCaptcha()
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["OpenID"] = oid
prepareCommonAuthPageData(ctx, CommonAuthOptions{
EnableCaptcha: setting.Service.EnableCaptcha,
})
return oid
}
// RegisterOpenID shows a form to create a new user authenticated via an OpenID URI
func RegisterOpenID(ctx *context.Context) {
oid := prepareRegisterOpenIDPageData(ctx)
if oid == "" {
return
}
userName, _ := ctx.Session.Get("openid_determined_username").(string)
if userName != "" {
ctx.Data["user_name"] = userName
@@ -322,19 +324,12 @@ func RegisterOpenID(ctx *context.Context) {
// RegisterOpenIDPost handles submission of a form to create a new user authenticated via an OpenID URI
func RegisterOpenIDPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.SignUpOpenIDForm)
oid, _ := ctx.Session.Get("openid_verified_uri").(string)
oid := prepareRegisterOpenIDPageData(ctx)
if oid == "" {
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
return
}
ctx.Data["Title"] = "OpenID signup"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDRegister"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
context.SetCaptchaData(ctx)
ctx.Data["OpenID"] = oid
form := web.GetForm(ctx).(*forms.SignUpOpenIDForm)
if setting.Service.AllowOnlyInternalRegistration {
ctx.HTTPError(http.StatusForbidden)
@@ -352,7 +347,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
length := max(setting.MinPasswordLength, 256)
password, err := util.CryptoRandomString(int64(length))
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignUpOID, form)
ctx.RenderWithErrDeprecated(err.Error(), tplSignUpOID, form)
return
}
@@ -370,7 +365,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
userOID := &user_model.UserOpenID{UID: u.ID, URI: oid}
if err = user_model.AddUserOpenID(ctx, userOID); err != nil {
if user_model.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form)
return
}
ctx.ServerError("AddUserOpenID", err)
@@ -16,7 +16,6 @@ import (
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer"
@@ -75,7 +74,7 @@ func ForgotPasswdPost(ctx *context.Context) {
if !u.IsLocal() && !u.IsOAuth2() {
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil)
return
}
@@ -172,7 +171,7 @@ func ResetPasswdPost(ctx *context.Context) {
if !twofa.VerifyScratchToken(ctx.FormString("token")) {
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Token"] = true
ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil)
return
}
regenerateScratchToken = true
@@ -186,7 +185,7 @@ func ResetPasswdPost(ctx *context.Context) {
if !ok || twofa.LastUsedPasscode == passcode {
ctx.Data["IsResetForm"] = true
ctx.Data["Err_Passcode"] = true
ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
return
}
@@ -207,13 +206,13 @@ func ResetPasswdPost(ctx *context.Context) {
ctx.Data["Err_Password"] = true
switch {
case errors.Is(err, password.ErrMinLength):
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
case errors.Is(err, password.ErrComplexity):
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
case errors.Is(err, password.ErrIsPwned):
ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplResetPassword, nil)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplResetPassword, nil)
case password.IsErrIsPwnedRequest(err):
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil)
default:
ctx.ServerError("UpdateAuth", err)
}
@@ -236,7 +235,7 @@ func ResetPasswdPost(ctx *context.Context) {
return
}
handleSignInFull(ctx, u, remember, false)
handleSignInFull(ctx, u, remember)
if ctx.Written() {
return
}
@@ -276,7 +275,7 @@ func MustChangePasswordPost(ctx *context.Context) {
if form.Password != form.Retype {
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form)
return
}
@@ -288,16 +287,16 @@ func MustChangePasswordPost(ctx *context.Context) {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
case errors.Is(err, password.ErrComplexity):
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
case errors.Is(err, password.ErrIsPwned):
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplMustChangePassword, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplMustChangePassword, &form)
case password.IsErrIsPwnedRequest(err):
ctx.Data["Err_Password"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form)
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form)
default:
ctx.ServerError("UpdateAuth", err)
}
@@ -308,11 +307,5 @@ func MustChangePasswordPost(ctx *context.Context) {
log.Trace("User updated password: %s", ctx.Doer.Name)
if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" {
middleware.DeleteRedirectToCookie(ctx.Resp)
ctx.RedirectToCurrentSite(redirectTo)
return
}
ctx.Redirect(setting.AppSubURL + "/")
redirectAfterAuth(ctx)
}
@@ -26,7 +26,7 @@ var tplWebAuthn templates.TplName = "user/auth/webauthn"
func WebAuthn(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("twofa")
if CheckAutoLogin(ctx) {
if performAutoLogin(ctx) {
return
}
@@ -156,12 +156,8 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
}
remember := false // TODO: implement remember me
redirect := handleSignInFull(ctx, user, remember, false)
if redirect == "" {
redirect = setting.AppSubURL + "/"
}
ctx.JSONRedirect(redirect)
handleSignInFull(ctx, user, remember)
ctx.JSONRedirect(consumeAuthRedirectLink(ctx))
}
// WebAuthnLoginAssertion submits a WebAuthn challenge to the browser
@@ -274,11 +270,7 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
}
remember := ctx.Session.Get("twofaRemember").(bool)
redirect := handleSignInFull(ctx, user, remember, false)
if redirect == "" {
redirect = setting.AppSubURL + "/"
}
handleSignInFull(ctx, user, remember)
_ = ctx.Session.Delete("twofaUid")
ctx.JSONRedirect(redirect)
ctx.JSONRedirect(consumeAuthRedirectLink(ctx))
}