mirror of
https://github.com/casdoor/casdoor.git
synced 2025-08-02 18:50:32 +08:00
Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
dff65eee20 | ||
![]() |
596016456c | ||
![]() |
673261c258 | ||
![]() |
3c5985a3c0 | ||
![]() |
4f3d62520a |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -114,12 +114,12 @@ jobs:
|
|||||||
wait-on-timeout: 210
|
wait-on-timeout: 210
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: cypress-screenshots
|
name: cypress-screenshots
|
||||||
path: ./web/cypress/screenshots
|
path: ./web/cypress/screenshots
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: cypress-videos
|
name: cypress-videos
|
||||||
|
@@ -561,8 +561,9 @@ func (c *ApiController) SetPassword() {
|
|||||||
targetUser.Password = newPassword
|
targetUser.Password = newPassword
|
||||||
targetUser.UpdateUserPassword(organization)
|
targetUser.UpdateUserPassword(organization)
|
||||||
targetUser.NeedUpdatePassword = false
|
targetUser.NeedUpdatePassword = false
|
||||||
|
targetUser.LastChangePasswordTime = util.GetCurrentTime()
|
||||||
|
|
||||||
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type"}, false)
|
_, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
|
@@ -381,7 +381,13 @@ func CheckUserPassword(organization string, username string, password string, la
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = checkPasswordExpired(user, lang)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
53
object/check_password_expired.go
Normal file
53
object/check_password_expired.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2024 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 object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/i18n"
|
||||||
|
"github.com/casdoor/casdoor/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkPasswordExpired(user *User, lang string) error {
|
||||||
|
organization, err := GetOrganizationByUser(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if organization == nil {
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordExpireDays := organization.PasswordExpireDays
|
||||||
|
if passwordExpireDays <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastChangePasswordTime := user.LastChangePasswordTime
|
||||||
|
if lastChangePasswordTime == "" {
|
||||||
|
if user.CreatedTime == "" {
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
|
||||||
|
}
|
||||||
|
lastChangePasswordTime = user.CreatedTime
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTime := util.String2Time(lastChangePasswordTime)
|
||||||
|
expireTime := lastTime.AddDate(0, 0, passwordExpireDays)
|
||||||
|
if time.Now().After(expireTime) {
|
||||||
|
return fmt.Errorf(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@@ -26,6 +26,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
@@ -276,29 +277,38 @@ func GetSamlMeta(application *Application, host string, enablePostBinding bool)
|
|||||||
func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, string, error) {
|
func GetSamlResponse(application *Application, user *User, samlRequest string, host string) (string, string, string, error) {
|
||||||
// request type
|
// request type
|
||||||
method := "GET"
|
method := "GET"
|
||||||
|
samlRequest = strings.ReplaceAll(samlRequest, " ", "+")
|
||||||
// base64 decode
|
// base64 decode
|
||||||
defated, err := base64.StdEncoding.DecodeString(samlRequest)
|
defated, err := base64.StdEncoding.DecodeString(samlRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", fmt.Errorf("err: Failed to decode SAML request, %s", err.Error())
|
return "", "", "", fmt.Errorf("err: Failed to decode SAML request, %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// decompress
|
var requestByte []byte
|
||||||
var buffer bytes.Buffer
|
|
||||||
rdr := flate.NewReader(bytes.NewReader(defated))
|
|
||||||
|
|
||||||
for {
|
if strings.Contains(string(defated), "xmlns:") {
|
||||||
_, err = io.CopyN(&buffer, rdr, 1024)
|
requestByte = defated
|
||||||
if err != nil {
|
} else {
|
||||||
if err == io.EOF {
|
// decompress
|
||||||
break
|
var buffer bytes.Buffer
|
||||||
|
rdr := flate.NewReader(bytes.NewReader(defated))
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
_, err = io.CopyN(&buffer, rdr, 1024)
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return "", "", "", err
|
||||||
}
|
}
|
||||||
return "", "", "", err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestByte = buffer.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
var authnRequest saml.AuthNRequest
|
var authnRequest saml.AuthNRequest
|
||||||
err = xml.Unmarshal(buffer.Bytes(), &authnRequest)
|
err = xml.Unmarshal(requestByte, &authnRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", fmt.Errorf("err: Failed to unmarshal AuthnRequest, please check the SAML request, %s", err.Error())
|
return "", "", "", fmt.Errorf("err: Failed to unmarshal AuthnRequest, please check the SAML request, %s", err.Error())
|
||||||
}
|
}
|
||||||
|
@@ -200,8 +200,9 @@ type User struct {
|
|||||||
Permissions []*Permission `json:"permissions"`
|
Permissions []*Permission `json:"permissions"`
|
||||||
Groups []string `xorm:"groups varchar(1000)" json:"groups"`
|
Groups []string `xorm:"groups varchar(1000)" json:"groups"`
|
||||||
|
|
||||||
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
|
LastChangePasswordTime string `xorm:"varchar(100)" json:"lastChangePasswordTime"`
|
||||||
SigninWrongTimes int `json:"signinWrongTimes"`
|
LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
|
||||||
|
SigninWrongTimes int `json:"signinWrongTimes"`
|
||||||
|
|
||||||
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
|
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
|
||||||
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
|
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
|
||||||
@@ -690,7 +691,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
|
|||||||
"owner", "display_name", "avatar", "first_name", "last_name",
|
"owner", "display_name", "avatar", "first_name", "last_name",
|
||||||
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
|
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
|
||||||
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
|
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
|
||||||
"signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
|
"signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
|
||||||
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
|
||||||
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
|
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
|
||||||
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
|
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
|
||||||
|
@@ -198,11 +198,11 @@ function ManagementPage(props) {
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<OpenTour />
|
<OpenTour />
|
||||||
{Setting.isAdminUser(props.account) && !Setting.isMobile() && (props.uri.indexOf("/trees") === -1) &&
|
{Setting.isAdminUser(props.account) && (props.uri.indexOf("/trees") === -1) &&
|
||||||
<OrganizationSelect
|
<OrganizationSelect
|
||||||
initValue={Setting.getOrganization()}
|
initValue={Setting.getOrganization()}
|
||||||
withAll={true}
|
withAll={true}
|
||||||
style={{marginRight: "20px", width: "180px", display: "flex"}}
|
style={{marginRight: "20px", width: "180px", display: !Setting.isMobile() ? "flex" : "none"}}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
Setting.setOrganization(value);
|
Setting.setOrganization(value);
|
||||||
}}
|
}}
|
||||||
|
@@ -1009,6 +1009,19 @@ class UserEditPage extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
} else if (accountItem.name === "Last change password time") {
|
||||||
|
return (
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("user:Last change password time"), i18next.t("user:Last change password time"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22}>
|
||||||
|
<Input value={this.state.user.lastChangePasswordTime} onChange={e => {
|
||||||
|
this.updateUserField("lastChangePasswordTime", e.target.value);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
} else if (accountItem.name === "Managed accounts") {
|
} else if (accountItem.name === "Managed accounts") {
|
||||||
return (
|
return (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
@@ -243,7 +243,10 @@ class LoginPage extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlaceholder() {
|
getPlaceholder(defaultPlaceholder = null) {
|
||||||
|
if (defaultPlaceholder) {
|
||||||
|
return defaultPlaceholder;
|
||||||
|
}
|
||||||
switch (this.state.loginMethod) {
|
switch (this.state.loginMethod) {
|
||||||
case "verificationCode": return i18next.t("login:Email or phone");
|
case "verificationCode": return i18next.t("login:Email or phone");
|
||||||
case "verificationCodeEmail": return i18next.t("login:Email");
|
case "verificationCodeEmail": return i18next.t("login:Email");
|
||||||
@@ -679,7 +682,7 @@ class LoginPage extends React.Component {
|
|||||||
id="input"
|
id="input"
|
||||||
className="login-username-input"
|
className="login-username-input"
|
||||||
prefix={<UserOutlined className="site-form-item-icon" />}
|
prefix={<UserOutlined className="site-form-item-icon" />}
|
||||||
placeholder={this.getPlaceholder()}
|
placeholder={this.getPlaceholder(signinItem.placeholder)}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
this.setState({
|
this.setState({
|
||||||
username: e.target.value,
|
username: e.target.value,
|
||||||
@@ -1093,7 +1096,7 @@ class LoginPage extends React.Component {
|
|||||||
className="login-password-input"
|
className="login-password-input"
|
||||||
prefix={<LockOutlined className="site-form-item-icon" />}
|
prefix={<LockOutlined className="site-form-item-icon" />}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder={i18next.t("general:Password")}
|
placeholder={signinItem.placeholder ? signinItem.placeholder : i18next.t("general:Password")}
|
||||||
disabled={this.state.loginMethod === "password" ? !Setting.isPasswordEnabled(application) : !Setting.isLdapEnabled(application)}
|
disabled={this.state.loginMethod === "password" ? !Setting.isPasswordEnabled(application) : !Setting.isLdapEnabled(application)}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
@@ -113,6 +113,9 @@ export function getCasLoginParameters(owner, name) {
|
|||||||
|
|
||||||
export function getOAuthGetParameters(params) {
|
export function getOAuthGetParameters(params) {
|
||||||
const queries = (params !== undefined) ? params : new URLSearchParams(window.location.search);
|
const queries = (params !== undefined) ? params : new URLSearchParams(window.location.search);
|
||||||
|
const lowercaseQueries = {};
|
||||||
|
queries.forEach((val, key) => {lowercaseQueries[key.toLowerCase()] = val;});
|
||||||
|
|
||||||
const clientId = getRefinedValue(queries.get("client_id"));
|
const clientId = getRefinedValue(queries.get("client_id"));
|
||||||
const responseType = getRefinedValue(queries.get("response_type"));
|
const responseType = getRefinedValue(queries.get("response_type"));
|
||||||
|
|
||||||
@@ -138,9 +141,9 @@ export function getOAuthGetParameters(params) {
|
|||||||
const nonce = getRefinedValue(queries.get("nonce"));
|
const nonce = getRefinedValue(queries.get("nonce"));
|
||||||
const challengeMethod = getRefinedValue(queries.get("code_challenge_method"));
|
const challengeMethod = getRefinedValue(queries.get("code_challenge_method"));
|
||||||
const codeChallenge = getRefinedValue(queries.get("code_challenge"));
|
const codeChallenge = getRefinedValue(queries.get("code_challenge"));
|
||||||
const samlRequest = getRefinedValue(queries.get("SAMLRequest"));
|
const samlRequest = getRefinedValue(lowercaseQueries["samlRequest".toLowerCase()]);
|
||||||
const relayState = getRefinedValue(queries.get("RelayState"));
|
const relayState = getRefinedValue(lowercaseQueries["RelayState".toLowerCase()]);
|
||||||
const noRedirect = getRefinedValue(queries.get("noRedirect"));
|
const noRedirect = getRefinedValue(lowercaseQueries["noRedirect".toLowerCase()]);
|
||||||
|
|
||||||
if (clientId === "" && samlRequest === "") {
|
if (clientId === "" && samlRequest === "") {
|
||||||
// login
|
// login
|
||||||
|
Reference in New Issue
Block a user