diff --git a/controllers/auth.go b/controllers/auth.go index 07877448..ea9e180e 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -555,8 +555,11 @@ func (c *ApiController) Login() { c.ResponseError(c.T("auth:The login method: login with LDAP is not enabled for the application")) return } + + clientIp := util.GetClientIpFromRequest(c.Ctx.Request) + var enableCaptcha bool - if enableCaptcha, err = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username); err != nil { + if enableCaptcha, err = object.CheckToEnableCaptcha(application, authForm.Organization, authForm.Username, clientIp); err != nil { c.ResponseError(err.Error()) return } else if enableCaptcha { @@ -1222,27 +1225,26 @@ func (c *ApiController) GetQRCode() { func (c *ApiController) GetCaptchaStatus() { organization := c.Input().Get("organization") userId := c.Input().Get("userId") - user, err := object.GetUserByFields(organization, userId) + applicationName := c.Input().Get("application") + + application, err := object.GetApplication(fmt.Sprintf("admin/%s", applicationName)) if err != nil { c.ResponseError(err.Error()) return } - - captchaEnabled := false - if user != nil { - var failedSigninLimit int - failedSigninLimit, _, err = object.GetFailedSigninConfigByUser(user) - if err != nil { - c.ResponseError(err.Error()) - return - } - - if user.SigninWrongTimes >= failedSigninLimit { - captchaEnabled = true - } + if application == nil { + c.ResponseError("application not found") + return } + clientIp := util.GetClientIpFromRequest(c.Ctx.Request) + captchaEnabled, err := object.CheckToEnableCaptcha(application, organization, userId, clientIp) + if err != nil { + c.ResponseError(err.Error()) + return + } c.ResponseOk(captchaEnabled) + return } // Callback diff --git a/object/check.go b/object/check.go index adedd683..9cb2c5f6 100644 --- a/object/check.go +++ b/object/check.go @@ -593,7 +593,7 @@ func CheckUpdateUser(oldUser, user *User, lang string) string { return "" } -func CheckToEnableCaptcha(application *Application, organization, username string) (bool, error) { +func CheckToEnableCaptcha(application *Application, organization, username string, clientIp string) (bool, error) { if len(application.Providers) == 0 { return false, nil } @@ -603,6 +603,12 @@ func CheckToEnableCaptcha(application *Application, organization, username strin continue } + if providerItem.Rule == "Internet-Only" { + if util.IsInternetIp(clientIp) { + return true, nil + } + } + if providerItem.Rule == "Dynamic" { user, err := GetUserByFields(organization, username) if err != nil { diff --git a/routers/base.go b/routers/base.go index 82498afc..614eb74a 100644 --- a/routers/base.go +++ b/routers/base.go @@ -185,17 +185,3 @@ func removePort(s string) string { } return ipStr } - -func isHostIntranet(s string) bool { - ipStr, _, err := net.SplitHostPort(s) - if err != nil { - ipStr = s - } - - ip := net.ParseIP(ipStr) - if ip == nil { - return false - } - - return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() -} diff --git a/routers/cors_filter.go b/routers/cors_filter.go index 36032da2..0e8f44d5 100644 --- a/routers/cors_filter.go +++ b/routers/cors_filter.go @@ -83,7 +83,7 @@ func CorsFilter(ctx *context.Context) { setCorsHeaders(ctx, origin) } else if originHostname == host { setCorsHeaders(ctx, origin) - } else if isHostIntranet(host) { + } else if util.IsHostIntranet(host) { setCorsHeaders(ctx, origin) } else { ok, err := object.IsOriginAllowed(origin) diff --git a/util/network.go b/util/network.go new file mode 100644 index 00000000..03a5f593 --- /dev/null +++ b/util/network.go @@ -0,0 +1,47 @@ +// Copyright 2025 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "net" +) + +func IsInternetIp(ip string) bool { + ipStr, _, err := net.SplitHostPort(ip) + if err != nil { + ipStr = ip + } + + parsedIP := net.ParseIP(ipStr) + if parsedIP == nil { + return false + } + + return !parsedIP.IsPrivate() && !parsedIP.IsLoopback() && !parsedIP.IsMulticast() && !parsedIP.IsUnspecified() +} + +func IsHostIntranet(ip string) bool { + ipStr, _, err := net.SplitHostPort(ip) + if err != nil { + ipStr = ip + } + + parsedIP := net.ParseIP(ipStr) + if parsedIP == nil { + return false + } + + return parsedIP.IsPrivate() || parsedIP.IsLoopback() || parsedIP.IsLinkLocalUnicast() || parsedIP.IsLinkLocalMulticast() +} diff --git a/web/src/auth/AuthBackend.js b/web/src/auth/AuthBackend.js index a62a737c..7fe234a6 100644 --- a/web/src/auth/AuthBackend.js +++ b/web/src/auth/AuthBackend.js @@ -163,7 +163,7 @@ export function getWechatQRCode(providerId) { } export function getCaptchaStatus(values) { - return fetch(`${Setting.ServerUrl}/api/get-captcha-status?organization=${values["organization"]}&userId=${values["username"]}`, { + return fetch(`${Setting.ServerUrl}/api/get-captcha-status?organization=${values["organization"]}&userId=${values["username"]}&application=${values["application"]}`, { method: "GET", credentials: "include", headers: { diff --git a/web/src/auth/LoginPage.js b/web/src/auth/LoginPage.js index 23f010af..02962439 100644 --- a/web/src/auth/LoginPage.js +++ b/web/src/auth/LoginPage.js @@ -134,6 +134,8 @@ class LoginPage extends React.Component { return CaptchaRule.Always; } else if (captchaProviderItems.some(providerItem => providerItem.rule === "Dynamic")) { return CaptchaRule.Dynamic; + } else if (captchaProviderItems.some(providerItem => providerItem.rule === "Internet-Only")) { + return CaptchaRule.InternetOnly; } else { return CaptchaRule.Never; } @@ -443,6 +445,9 @@ class LoginPage extends React.Component { } else if (captchaRule === CaptchaRule.Dynamic) { this.checkCaptchaStatus(values); return; + } else if (captchaRule === CaptchaRule.InternetOnly) { + this.checkCaptchaStatus(values); + return; } } this.login(values); @@ -961,9 +966,23 @@ class LoginPage extends React.Component { const captchaProviderItems = this.getCaptchaProviderItems(application); const alwaysProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Always"); const dynamicProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Dynamic"); - const provider = alwaysProviderItems.length > 0 - ? alwaysProviderItems[0].provider - : dynamicProviderItems[0].provider; + const internetOnlyProviderItems = captchaProviderItems.filter(providerItem => providerItem.rule === "Internet-Only"); + + // Select provider based on the active captcha rule, not fixed priority + const captchaRule = this.getCaptchaRule(this.getApplicationObj()); + let provider = null; + + if (captchaRule === CaptchaRule.Always && alwaysProviderItems.length > 0) { + provider = alwaysProviderItems[0].provider; + } else if (captchaRule === CaptchaRule.Dynamic && dynamicProviderItems.length > 0) { + provider = dynamicProviderItems[0].provider; + } else if (captchaRule === CaptchaRule.InternetOnly && internetOnlyProviderItems.length > 0) { + provider = internetOnlyProviderItems[0].provider; + } + + if (!provider) { + return null; + } return {i18next.t("general:None")} + ); } else if (record.provider?.category === "SMS" || record.provider?.category === "Email") {