From def13ece7c7994e014e34cdc2fc4887d1998f019 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 2 Dec 2024 02:03:15 +0800 Subject: [PATCH] Allow to disable the password-based login (sign-in) form (#32687) Usually enterprise/organization users would like to only allow OAuth2 login. This PR adds a new config option to disable the password-based login form. It is a simple and clear approach and won't block the future login-system refactoring works. Fix a TODO in #24821 Replace #21851 Close #7633 , close #13606 --- custom/conf/app.example.ini | 22 ++++++----- modules/setting/service.go | 2 + routers/web/auth/auth.go | 48 +++++++++--------------- templates/user/auth/oauth_container.tmpl | 5 --- templates/user/auth/signin_inner.tmpl | 10 ++++- templates/user/auth/signup_inner.tmpl | 5 ++- tests/integration/signin_test.go | 29 ++++++++++++++ 7 files changed, 73 insertions(+), 48 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index c3b78e60bb4..5c23f70d7ca 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -784,6 +784,10 @@ LEVEL = Info ;; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token ;ENABLE_BASIC_AUTHENTICATION = true ;; +;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 login methods. +;; If you set it to false, maybe it also needs to set ENABLE_BASIC_AUTHENTICATION to false to completely disable password-based authentication. +;ENABLE_PASSWORD_SIGNIN_FORM = true +;; ;; More detail: https://github.com/gogits/gogs/issues/165 ;ENABLE_REVERSE_PROXY_AUTHENTICATION = false ; Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible. @@ -1944,13 +1948,13 @@ LEVEL = Info ;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio` ;MINIO_SECRET_ACCESS_KEY = ;; -;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`. -;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables -;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, -;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION), +;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`. +;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables +;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, +;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION), ;; or the DefaultIAMRoleEndpoint if not provided otherwise. ;MINIO_IAM_ENDPOINT = -;; +;; ;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio` ;MINIO_BUCKET = gitea ;; @@ -2695,10 +2699,10 @@ LEVEL = Info ;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio` ;MINIO_SECRET_ACCESS_KEY = ;; -;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`. -;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables -;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, -;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION), +;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`. +;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables +;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, +;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION), ;; or the DefaultIAMRoleEndpoint if not provided otherwise. ;MINIO_IAM_ENDPOINT = ;; diff --git a/modules/setting/service.go b/modules/setting/service.go index c858f803549..526ad64eb40 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -41,6 +41,7 @@ var Service = struct { AllowOnlyInternalRegistration bool AllowOnlyExternalRegistration bool ShowRegistrationButton bool + EnablePasswordSignInForm bool ShowMilestonesDashboardPage bool RequireSignInView bool EnableNotifyMail bool @@ -159,6 +160,7 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true) Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool() Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true) + Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true) Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool() Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool() Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool() diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index c9ef9193f12..3f16da3cddf 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -160,54 +160,42 @@ func CheckAutoLogin(ctx *context.Context) bool { return false } -// SignIn render sign in page -func SignIn(ctx *context.Context) { +func prepareSignInPageData(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("sign_in") - - if CheckAutoLogin(ctx) { - return - } - - if ctx.IsSigned { - RedirectAfterLogin(ctx) - return - } - - oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) - if err != nil { - ctx.ServerError("UserSignIn", err) - return - } - ctx.Data["OAuth2Providers"] = oauth2Providers + ctx.Data["OAuth2Providers"], _ = oauth2.GetOAuth2Providers(ctx, optional.Some(true)) 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 if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { context.SetCaptchaData(ctx) } +} +// SignIn render sign in page +func SignIn(ctx *context.Context) { + if CheckAutoLogin(ctx) { + return + } + if ctx.IsSigned { + RedirectAfterLogin(ctx) + return + } + prepareSignInPageData(ctx) ctx.HTML(http.StatusOK, tplSignIn) } // SignInPost response for sign in request func SignInPost(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("sign_in") - - oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) - if err != nil { - ctx.ServerError("UserSignIn", err) + if !setting.Service.EnablePasswordSignInForm { + ctx.Error(http.StatusForbidden) return } - ctx.Data["OAuth2Providers"] = 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) + prepareSignInPageData(ctx) if ctx.HasError() { ctx.HTML(http.StatusOK, tplSignIn) return @@ -216,8 +204,6 @@ func SignInPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.SignInForm) if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { - context.SetCaptchaData(ctx) - context.VerifyCaptcha(ctx, tplSignIn, form) if ctx.Written() { return diff --git a/templates/user/auth/oauth_container.tmpl b/templates/user/auth/oauth_container.tmpl index 7599b49fbbf..d01aaefe1aa 100644 --- a/templates/user/auth/oauth_container.tmpl +++ b/templates/user/auth/oauth_container.tmpl @@ -1,7 +1,3 @@ -{{if or .OAuth2Providers .EnableOpenIDSignIn}} -
- {{ctx.Locale.Tr "sign_in_or"}} -
@@ -26,4 +22,3 @@
-{{end}} diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index ec61e56f4db..e0a19a97433 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -10,6 +10,7 @@ {{end}}
+ {{if .EnablePasswordSignInForm}}
{{.CsrfTokenHtml}}
@@ -46,8 +47,13 @@
- - {{template "user/auth/oauth_container" .}} + {{end}}{{/*if .EnablePasswordSignInForm*/}} + {{if and .OAuth2Providers .EnableOpenIDSignIn .EnablePasswordSignInForm}} +
{{ctx.Locale.Tr "sign_in_or"}}
+ {{end}} + {{if and .OAuth2Providers .EnableOpenIDSignIn}} + {{template "user/auth/oauth_container" .}} + {{end}}
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index 08507e545dc..69690039686 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -48,7 +48,10 @@ {{end}} - {{template "user/auth/oauth_container" .}} + {{if and .OAuth2Providers .EnableOpenIDSignIn}} +
{{ctx.Locale.Tr "sign_in_or"}}
+ {{template "user/auth/oauth_container" .}} + {{end}} diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index 886d4a82593..abad9eb5e55 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/tests" @@ -91,3 +92,31 @@ func TestSigninWithRememberMe(t *testing.T) { req = NewRequest(t, "GET", "/user/settings") session.MakeRequest(t, req, http.StatusOK) } + +func TestEnablePasswordSignInForm(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("EnablePasswordSignInForm=false", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, false)() + + req := NewRequest(t, "GET", "/user/login") + resp := MakeRequest(t, req, http.StatusOK) + NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/login']", false) + + req = NewRequest(t, "POST", "/user/login") + MakeRequest(t, req, http.StatusForbidden) + }) + + t.Run("EnablePasswordSignInForm=true", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, true)() + + req := NewRequest(t, "GET", "/user/login") + resp := MakeRequest(t, req, http.StatusOK) + NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/login']", true) + + req = NewRequest(t, "POST", "/user/login") + MakeRequest(t, req, http.StatusOK) + }) +}