Compare commits

...

8 Commits

13 changed files with 294 additions and 39 deletions

View File

@@ -23,6 +23,7 @@ isDemoMode = false
batchSize = 100
enableErrorMask = false
enableGzip = true
cookieSecure = false
inactiveTimeoutMinutes =
ldapServerPort = 389
ldapsCertId = ""

View File

@@ -1414,7 +1414,7 @@ func (c *ApiController) Callback() {
code := c.GetString("code")
state := c.GetString("state")
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", code, state)
frontendCallbackUrl := fmt.Sprintf("/callback?code=%s&state=%s", url.QueryEscape(code), url.QueryEscape(state))
c.Ctx.Redirect(http.StatusFound, frontendCallbackUrl)
}

View File

@@ -26,6 +26,13 @@ import (
"github.com/hsluoyz/modsecurity-go/seclang/parser"
)
// GetRules
// @Title GetRules
// @Tag Rule API
// @Description get rules
// @Param owner query string true "The owner of rules"
// @Success 200 {array} object.Rule The Response object
// @router /get-rules [get]
func (c *ApiController) GetRules() {
owner := c.Ctx.Input.Query("owner")
if owner == "admin" {
@@ -65,6 +72,13 @@ func (c *ApiController) GetRules() {
}
}
// GetRule
// @Title GetRule
// @Tag Rule API
// @Description get rule
// @Param id query string true "The id ( owner/name ) of the rule"
// @Success 200 {object} object.Rule The Response object
// @router /get-rule [get]
func (c *ApiController) GetRule() {
id := c.Ctx.Input.Query("id")
rule, err := object.GetRule(id)
@@ -76,6 +90,13 @@ func (c *ApiController) GetRule() {
c.ResponseOk(rule)
}
// AddRule
// @Title AddRule
// @Tag Rule API
// @Description add rule
// @Param body body object.Rule true "The details of the rule"
// @Success 200 {object} controllers.Response The Response object
// @router /add-rule [post]
func (c *ApiController) AddRule() {
currentTime := util.GetCurrentTime()
rule := object.Rule{
@@ -96,6 +117,14 @@ func (c *ApiController) AddRule() {
c.ServeJSON()
}
// UpdateRule
// @Title UpdateRule
// @Tag Rule API
// @Description update rule
// @Param id query string true "The id ( owner/name ) of the rule"
// @Param body body object.Rule true "The details of the rule"
// @Success 200 {object} controllers.Response The Response object
// @router /update-rule [post]
func (c *ApiController) UpdateRule() {
var rule object.Rule
err := json.Unmarshal(c.Ctx.Input.RequestBody, &rule)
@@ -115,6 +144,13 @@ func (c *ApiController) UpdateRule() {
c.ServeJSON()
}
// DeleteRule
// @Title DeleteRule
// @Tag Rule API
// @Description delete rule
// @Param body body object.Rule true "The details of the rule"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-rule [post]
func (c *ApiController) DeleteRule() {
var rule object.Rule
err := json.Unmarshal(c.Ctx.Input.RequestBody, &rule)

View File

@@ -22,6 +22,12 @@ import (
"github.com/casdoor/casdoor/util"
)
// GetGlobalSites
// @Title GetGlobalSites
// @Tag Site API
// @Description get global sites
// @Success 200 {array} object.Site The Response object
// @router /get-global-sites [get]
func (c *ApiController) GetGlobalSites() {
sites, err := object.GetGlobalSites()
if err != nil {
@@ -32,6 +38,13 @@ func (c *ApiController) GetGlobalSites() {
c.ResponseOk(object.GetMaskedSites(sites, util.GetHostname()))
}
// GetSites
// @Title GetSites
// @Tag Site API
// @Description get sites
// @Param owner query string true "The owner of sites"
// @Success 200 {array} object.Site The Response object
// @router /get-sites [get]
func (c *ApiController) GetSites() {
owner := c.Ctx.Input.Query("owner")
if owner == "admin" {
@@ -72,6 +85,13 @@ func (c *ApiController) GetSites() {
c.ResponseOk(object.GetMaskedSites(sites, util.GetHostname()), paginator.Nums())
}
// GetSite
// @Title GetSite
// @Tag Site API
// @Description get site
// @Param id query string true "The id ( owner/name ) of the site"
// @Success 200 {object} object.Site The Response object
// @router /get-site [get]
func (c *ApiController) GetSite() {
id := c.Ctx.Input.Query("id")
@@ -84,6 +104,14 @@ func (c *ApiController) GetSite() {
c.ResponseOk(object.GetMaskedSite(site, util.GetHostname()))
}
// UpdateSite
// @Title UpdateSite
// @Tag Site API
// @Description update site
// @Param id query string true "The id ( owner/name ) of the site"
// @Param body body object.Site true "The details of the site"
// @Success 200 {object} controllers.Response The Response object
// @router /update-site [post]
func (c *ApiController) UpdateSite() {
id := c.Ctx.Input.Query("id")
@@ -98,6 +126,13 @@ func (c *ApiController) UpdateSite() {
c.ServeJSON()
}
// AddSite
// @Title AddSite
// @Tag Site API
// @Description add site
// @Param body body object.Site true "The details of the site"
// @Success 200 {object} controllers.Response The Response object
// @router /add-site [post]
func (c *ApiController) AddSite() {
var site object.Site
err := json.Unmarshal(c.Ctx.Input.RequestBody, &site)
@@ -110,6 +145,13 @@ func (c *ApiController) AddSite() {
c.ServeJSON()
}
// DeleteSite
// @Title DeleteSite
// @Tag Site API
// @Description delete site
// @Param body body object.Site true "The details of the site"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-site [post]
func (c *ApiController) DeleteSite() {
var site object.Site
err := json.Unmarshal(c.Ctx.Input.RequestBody, &site)

View File

@@ -598,15 +598,11 @@ func (c *ApiController) VerifyCode() {
}
if !passed {
result, err := object.CheckVerificationCode(checkDest, authForm.Code, c.GetAcceptLanguage())
err = object.CheckVerifyCodeWithLimit(user, checkDest, authForm.Code, c.GetAcceptLanguage())
if err != nil {
c.ResponseError(err.Error())
return
}
if result.Code != object.VerificationSuccess {
c.ResponseError(result.Msg)
return
}
err = object.DisableVerificationCode(checkDest)
if err != nil {

View File

@@ -75,8 +75,10 @@ func main() {
object.InitCleanupTokens()
object.InitSiteMap()
object.InitRuleMap()
object.StartMonitorSitesLoop()
if len(object.SiteMap) != 0 {
object.InitRuleMap()
object.StartMonitorSitesLoop()
}
util.SafeGoroutine(func() { object.RunSyncUsersJob() })
util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
@@ -97,6 +99,7 @@ func main() {
web.InsertFilter("*", web.BeforeRouter, routers.RecordMessage)
web.InsertFilter("*", web.BeforeRouter, routers.FieldValidationFilter)
web.InsertFilter("*", web.AfterExec, routers.AfterRecordMessage, web.WithReturnOnOutput(false))
web.InsertFilter("*", web.AfterExec, routers.SecureCookieFilter, web.WithReturnOnOutput(false))
var logAdapter string
logConfigMap := make(map[string]interface{})

View File

@@ -453,20 +453,20 @@ func SyncLdapUsers(owner string, syncUsers []LdapUser, ldapId string) (existUser
}
tag := strings.Join(ou, ".")
for _, syncUser := range syncUsers {
existUuids, err := GetExistUuids(owner, uuids)
if err != nil {
return nil, nil, err
}
existUuids, err := GetExistUuids(owner, uuids)
if err != nil {
return nil, nil, err
}
found := false
if len(existUuids) > 0 {
for _, existUuid := range existUuids {
if syncUser.Uuid == existUuid {
existUsers = append(existUsers, syncUser)
found = true
}
}
existUuidSet := make(map[string]struct{}, len(existUuids))
for _, uuid := range existUuids {
existUuidSet[uuid] = struct{}{}
}
for _, syncUser := range syncUsers {
_, found := existUuidSet[syncUser.Uuid]
if found {
existUsers = append(existUsers, syncUser)
}
if !found {
@@ -713,11 +713,23 @@ func dnToGroupName(owner, dn string) string {
func GetExistUuids(owner string, uuids []string) ([]string, error) {
var existUuids []string
// PostgreSQL only supports up to 65535 parameters per query, so we batch the uuids
const batchSize = 100
tableNamePrefix := conf.GetConfigString("tableNamePrefix")
err := ormer.Engine.Table(tableNamePrefix+"user").Where("owner = ?", owner).Cols("ldap").
In("ldap", uuids).Select("DISTINCT ldap").Find(&existUuids)
if err != nil {
return existUuids, err
for i := 0; i < len(uuids); i += batchSize {
end := i + batchSize
if end > len(uuids) {
end = len(uuids)
}
batch := uuids[i:end]
var batchUuids []string
err := ormer.Engine.Table(tableNamePrefix+"user").Where("owner = ?", owner).Cols("ldap").
In("ldap", batch).Select("DISTINCT ldap").Find(&batchUuids)
if err != nil {
return existUuids, err
}
existUuids = append(existUuids, batchUuids...)
}
return existUuids, nil

View File

@@ -21,7 +21,9 @@ import (
"math/rand"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/casdoor/casdoor/conf"
@@ -35,7 +37,16 @@ type VerifyResult struct {
Msg string
}
var ResetLinkReg *regexp.Regexp
type verifyCodeErrorInfo struct {
wrongTimes int
lastWrongTime time.Time
}
var (
ResetLinkReg *regexp.Regexp
verifyCodeErrorMap = map[string]*verifyCodeErrorInfo{}
verifyCodeErrorMapLock sync.Mutex
)
const (
VerificationSuccess = iota
@@ -330,6 +341,107 @@ func CheckSigninCode(user *User, dest, code, lang string) error {
}
}
// getVerifyCodeErrorKey builds the in-memory key for verify-code failed attempt tracking
func getVerifyCodeErrorKey(user *User, dest string) string {
if user == nil {
return dest
}
return fmt.Sprintf("%s:%s", user.GetId(), dest)
}
func checkVerifyCodeErrorTimes(user *User, dest, lang string) error {
failedSigninLimit, failedSigninFrozenTime, err := GetFailedSigninConfigByUser(user)
if err != nil {
return err
}
key := getVerifyCodeErrorKey(user, dest)
verifyCodeErrorMapLock.Lock()
defer verifyCodeErrorMapLock.Unlock()
errorInfo, ok := verifyCodeErrorMap[key]
if !ok || errorInfo == nil {
return nil
}
if errorInfo.wrongTimes < failedSigninLimit {
return nil
}
minutes := failedSigninFrozenTime - int(time.Now().UTC().Sub(errorInfo.lastWrongTime).Minutes())
if minutes > 0 {
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), minutes)
}
delete(verifyCodeErrorMap, key)
return nil
}
func recordVerifyCodeErrorInfo(user *User, dest, lang string) error {
failedSigninLimit, failedSigninFrozenTime, err := GetFailedSigninConfigByUser(user)
if err != nil {
return err
}
key := getVerifyCodeErrorKey(user, dest)
verifyCodeErrorMapLock.Lock()
defer verifyCodeErrorMapLock.Unlock()
errorInfo, ok := verifyCodeErrorMap[key]
if !ok || errorInfo == nil {
errorInfo = &verifyCodeErrorInfo{}
verifyCodeErrorMap[key] = errorInfo
}
if errorInfo.wrongTimes < failedSigninLimit {
errorInfo.wrongTimes++
}
if errorInfo.wrongTimes >= failedSigninLimit {
errorInfo.lastWrongTime = time.Now().UTC()
}
leftChances := failedSigninLimit - errorInfo.wrongTimes
if leftChances >= 0 {
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect, you have %s remaining chances"), strconv.Itoa(leftChances))
}
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), failedSigninFrozenTime)
}
func resetVerifyCodeErrorTimes(user *User, dest string) {
key := getVerifyCodeErrorKey(user, dest)
verifyCodeErrorMapLock.Lock()
defer verifyCodeErrorMapLock.Unlock()
delete(verifyCodeErrorMap, key)
}
func CheckVerifyCodeWithLimit(user *User, dest, code, lang string) error {
if err := checkVerifyCodeErrorTimes(user, dest, lang); err != nil {
return err
}
result, err := CheckVerificationCode(dest, code, lang)
if err != nil {
return err
}
switch result.Code {
case VerificationSuccess:
resetVerifyCodeErrorTimes(user, dest)
return nil
case wrongCodeError:
return recordVerifyCodeErrorInfo(user, dest, lang)
default:
return errors.New(result.Msg)
}
}
func CheckFaceId(user *User, faceId []float64, lang string) error {
if len(user.FaceIds) == 0 {
return errors.New(i18n.Translate(lang, "check:Face data does not exist, cannot log in"))

57
routers/secure_filter.go Normal file
View File

@@ -0,0 +1,57 @@
// 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 routers
import (
"strings"
"github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/server/web/context"
"github.com/casdoor/casdoor/conf"
)
// SecureCookieFilter adds the "Secure" attribute to the session cookie when the
// deployment is configured to use HTTPS. This is determined by either the
// "cookieSecure" config option being set to true, or the "origin" config starting
// with "https://". This approach ensures the Secure flag is set even when Casdoor
// runs behind a TLS-terminating reverse proxy.
func SecureCookieFilter(ctx *context.Context) {
if !conf.GetConfigBool("cookieSecure") && !strings.HasPrefix(conf.GetConfigString("origin"), "https://") {
return
}
sessionCookieName := web.BConfig.WebConfig.Session.SessionName
if sessionCookieName == "" {
return
}
cookies := ctx.ResponseWriter.Header()["Set-Cookie"]
for i, cookie := range cookies {
if !strings.HasPrefix(cookie, sessionCookieName+"=") {
continue
}
// Check if Secure is already present (case-insensitive, handles variants like ";Secure" and "; Secure")
alreadySecure := false
for _, part := range strings.Split(cookie, ";") {
if strings.EqualFold(strings.TrimSpace(part), "secure") {
alreadySecure = true
break
}
}
if !alreadySecure {
cookies[i] = cookie + "; Secure"
}
}
}

View File

@@ -144,9 +144,6 @@
}
var state = getRefinedValue(innerParams.get("state"));
if (state.indexOf("/auth/oauth2/login.php?wantsurl") === 0) {
state = encodeURIComponent(state);
}
if (redirectUri.indexOf("#") !== -1 && state === "") {
state = getRawGetParameter("state", queryString);
}
@@ -373,7 +370,7 @@
if (responseMode === "form_post") {
createFormAndSubmit(oAuthParams.redirectUri, {code: res.data, state: oAuthParams.state});
} else {
window.location.replace(oAuthParams.redirectUri + concatChar + "code=" + res.data + "&state=" + oAuthParams.state);
window.location.replace(oAuthParams.redirectUri + concatChar + "code=" + encodeURIComponent(res.data) + "&state=" + encodeURIComponent(oAuthParams.state));
}
return;
}
@@ -387,7 +384,7 @@
state: oAuthParams.state
});
} else {
window.location.replace(oAuthParams.redirectUri + concatChar + responseType + "=" + res.data + "&state=" + oAuthParams.state + "&token_type=bearer");
window.location.replace(oAuthParams.redirectUri + concatChar + responseType + "=" + encodeURIComponent(res.data) + "&state=" + encodeURIComponent(oAuthParams.state) + "&token_type=bearer");
}
return;
}

View File

@@ -1497,7 +1497,7 @@ class ApplicationEditPage extends React.Component {
</div>
} style={{margin: (Setting.isMobile()) ? "5px" : {}, height: "calc(100vh - 145px - 48px)", overflow: "hidden"}}
styles={{body: {height: "100%"}}} type="inner">
<Layout style={{background: "inherit", height: "100%", overflow: "auto"}}>
<Layout style={{background: "inherit", height: "100%"}}>
{
this.state.menuMode === "horizontal" || !this.state.menuMode ? (
<Header style={{background: "inherit", padding: "0px", position: "sticky", top: 0, height: 38, minHeight: 38}}>
@@ -1548,7 +1548,10 @@ class ApplicationEditPage extends React.Component {
</Menu>
</Sider>) : null
}
<Content style={{padding: "15px"}}>
<Content style={{padding: "15px",
overflowY: "auto",
height: "100%",
paddingBottom: "80px"}}>
{this.renderApplicationForm()}
</Content>
</Layout>

View File

@@ -116,7 +116,7 @@ class AuthCallback extends React.Component {
createFormAndSubmit(oAuthParams?.redirectUri, params);
} else {
const code = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${encodeURIComponent(code)}&state=${encodeURIComponent(oAuthParams.state)}`);
}
} else if (responseTypes.includes("token") || responseTypes.includes("id_token")) {
if (res.data3) {
@@ -135,7 +135,7 @@ class AuthCallback extends React.Component {
createFormAndSubmit(oAuthParams?.redirectUri, params);
} else {
const token = res.data;
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${encodeURIComponent(token)}&state=${encodeURIComponent(oAuthParams.state)}&token_type=bearer`);
}
} else if (responseType === "link") {
let from = innerParams.get("from");

View File

@@ -130,10 +130,6 @@ export function getOAuthGetParameters(params) {
}
let state = getRefinedValue(queries.get("state"));
if (state.startsWith("/auth/oauth2/login.php?wantsurl")) {
// state contains URL param encoding for Moodle, URLSearchParams automatically decoded it, so here encode it again
state = encodeURIComponent(state);
}
if (redirectUri.includes("#") && state === "") {
state = getRawGetParameter("state");
}