Skip to content

Commit

Permalink
feat: add dynamic mode for provider to enable verification code when …
Browse files Browse the repository at this point in the history
…the login password is wrong (casdoor#1753)

* fix: update webAuthnBufferDecode to support Base64URL for WebAuthn updates

* feat: enable verification code when the login password is wrong

* fix: only enable captcha when login in password

* fix: disable login error limits when captcha on

* fix: pass "enableCaptcha" as an optional param

* fix: change enbleCapctah to optional bool param
  • Loading branch information
XDTD authored Apr 22, 2023
1 parent ee8c265 commit 6d6cbc7
Show file tree
Hide file tree
Showing 31 changed files with 150 additions and 29 deletions.
1 change: 1 addition & 0 deletions authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ p, *, *, GET, /api/userinfo, *, *
p, *, *, GET, /api/user, *, *
p, *, *, POST, /api/webhook, *, *
p, *, *, GET, /api/get-webhook-event, *, *
p, *, *, GET, /api/get-captcha-status, *, *
p, *, *, *, /api/login/oauth, *, *
p, *, *, GET, /api/get-application, *, *
p, *, *, GET, /api/get-organization-applications, *, *
Expand Down
25 changes: 22 additions & 3 deletions controllers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,8 @@ func (c *ApiController) Login() {
c.ResponseError(c.T("auth:The login method: login with password is not enabled for the application"))
return
}

if object.CheckToEnableCaptcha(application) {
var enableCaptcha bool
if enableCaptcha = object.CheckToEnableCaptcha(application, form.Organization, form.Username); enableCaptcha {
isHuman, err := captcha.VerifyCaptchaByCaptchaType(form.CaptchaType, form.CaptchaToken, form.ClientSecret)
if err != nil {
c.ResponseError(err.Error())
Expand All @@ -296,7 +296,8 @@ func (c *ApiController) Login() {
}

password := form.Password
user, msg = object.CheckUserPassword(form.Organization, form.Username, password, c.GetAcceptLanguage())
user, msg = object.CheckUserPassword(form.Organization, form.Username, password, c.GetAcceptLanguage(), enableCaptcha)

}

if msg != "" {
Expand Down Expand Up @@ -610,3 +611,21 @@ func (c *ApiController) GetWebhookEventType() {
wechatScanType = ""
c.ServeJSON()
}

// GetCaptchaStatus
// @Title GetCaptchaStatus
// @Tag Token API
// @Description Get Login Error Counts
// @Param id query string true "The id ( owner/name ) of user"
// @Success 200 {object} controllers.Response The Response object
// @router /api/get-captcha-status [get]
func (c *ApiController) GetCaptchaStatus() {
organization := c.Input().Get("organization")
userId := c.Input().Get("user_id")
user := object.GetUserByFields(organization, userId)
var captchaEnabled bool
if user != nil && user.SigninWrongTimes >= object.SigninWrongTimesLimit {
captchaEnabled = true
}
c.ResponseOk(captchaEnabled)
}
1 change: 1 addition & 0 deletions i18n/locales/de/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"Username must have at least 2 characters": "Benutzername muss mindestens 2 Zeichen lang sein",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Sie haben zu oft das falsche Passwort oder den falschen Code eingegeben. Bitte warten Sie %d Minuten und versuchen Sie es erneut",
"Your region is not allow to signup by phone": "Ihre Region ist nicht berechtigt, sich telefonisch anzumelden",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Das Passwort oder der Code ist falsch. Du hast noch %d Versuche übrig",
"unsupported password type: %s": "Nicht unterstützter Passworttyp: %s"
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/en/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/es/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"Username must have at least 2 characters": "Nombre de usuario debe tener al menos 2 caracteres",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Has ingresado la contraseña o código incorrecto demasiadas veces, por favor espera %d minutos e intenta de nuevo",
"Your region is not allow to signup by phone": "Tu región no está permitida para registrarse por teléfono",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Contraseña o código incorrecto, tienes %d intentos restantes",
"unsupported password type: %s": "Tipo de contraseña no compatible: %s"
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/fr/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"Username must have at least 2 characters": "Le nom d'utilisateur doit comporter au moins 2 caractères",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Vous avez entré le mauvais mot de passe ou code plusieurs fois, veuillez attendre %d minutes et réessayer",
"Your region is not allow to signup by phone": "Votre région n'est pas autorisée à s'inscrire par téléphone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Le mot de passe ou le code est incorrect, il vous reste %d chances",
"unsupported password type: %s": "Type de mot de passe non pris en charge : %s"
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/id/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"Username must have at least 2 characters": "Nama pengguna harus memiliki setidaknya 2 karakter",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Anda telah memasukkan kata sandi atau kode yang salah terlalu banyak kali, mohon tunggu selama %d menit dan coba lagi",
"Your region is not allow to signup by phone": "Wilayah Anda tidak diizinkan untuk mendaftar melalui telepon",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Kata sandi atau kode salah, Anda memiliki %d kesempatan tersisa",
"unsupported password type: %s": "jenis sandi tidak didukung: %s"
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/ja/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"Username must have at least 2 characters": "ユーザー名は少なくとも2文字必要です",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "あなたは間違ったパスワードまたはコードを何度も入力しました。%d 分間待ってから再度お試しください",
"Your region is not allow to signup by phone": "あなたの地域は電話でサインアップすることができません",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "パスワードまたはコードが間違っています。あと%d回の試行機会があります",
"unsupported password type: %s": "サポートされていないパスワードタイプ:%s"
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/ko/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"Username must have at least 2 characters": "사용자 이름은 적어도 2개의 문자가 있어야 합니다",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "올바르지 않은 비밀번호나 코드를 여러 번 입력했습니다. %d분 동안 기다리신 후 다시 시도해주세요",
"Your region is not allow to signup by phone": "당신의 지역은 전화로 가입할 수 없습니다",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "암호 또는 코드가 올바르지 않습니다. %d번의 기회가 남아 있습니다",
"unsupported password type: %s": "지원되지 않는 암호 유형: %s"
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/ru/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"Username must have at least 2 characters": "Имя пользователя должно содержать не менее 2 символов",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Вы ввели неправильный пароль или код слишком много раз, пожалуйста, подождите %d минут и попробуйте снова",
"Your region is not allow to signup by phone": "Ваш регион не разрешает регистрацию по телефону",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Неправильный пароль или код, у вас осталось %d попыток",
"unsupported password type: %s": "неподдерживаемый тип пароля: %s"
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/vi/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"Username must have at least 2 characters": "Tên đăng nhập phải có ít nhất 2 ký tự",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "Bạn đã nhập sai mật khẩu hoặc mã quá nhiều lần, vui lòng đợi %d phút và thử lại",
"Your region is not allow to signup by phone": "Vùng của bạn không được phép đăng ký bằng điện thoại",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "Mật khẩu hoặc mã không chính xác, bạn còn %d lần cơ hội",
"unsupported password type: %s": "Loại mật khẩu không được hỗ trợ: %s"
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/zh/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"Username must have at least 2 characters": "用户名至少要有2个字符",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "密码错误次数已达上限,请在 %d 分后重试",
"Your region is not allow to signup by phone": "所在地区不支持手机号注册",
"password or code is incorrect": "密码错误",
"password or code is incorrect, you have %d remaining chances": "密码错误,您还有 %d 次尝试的机会",
"unsupported password type: %s": "不支持的密码类型: %s"
},
Expand Down
28 changes: 21 additions & 7 deletions object/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,16 @@ func checkSigninErrorTimes(user *User, lang string) string {
return ""
}

func CheckPassword(user *User, password string, lang string) string {
func CheckPassword(user *User, password string, lang string, options ...bool) string {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
// check the login error times
if msg := checkSigninErrorTimes(user, lang); msg != "" {
return msg
if !enableCaptcha {
if msg := checkSigninErrorTimes(user, lang); msg != "" {
return msg
}
}

organization := GetOrganizationByUser(user)
Expand All @@ -182,7 +188,7 @@ func CheckPassword(user *User, password string, lang string) string {
return ""
}

return recordSigninErrorInfo(user, lang)
return recordSigninErrorInfo(user, lang, enableCaptcha)
} else {
return fmt.Sprintf(i18n.Translate(lang, "check:unsupported password type: %s"), organization.PasswordType)
}
Expand Down Expand Up @@ -231,7 +237,11 @@ func checkLdapUserPassword(user *User, password string, lang string) string {
return ""
}

func CheckUserPassword(organization string, username string, password string, lang string) (*User, string) {
func CheckUserPassword(organization string, username string, password string, lang string, options ...bool) (*User, string) {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
user := GetUserByFields(organization, username)
if user == nil || user.IsDeleted == true {
return nil, fmt.Sprintf(i18n.Translate(lang, "general:The user: %s doesn't exist"), util.GetId(organization, username))
Expand All @@ -250,7 +260,7 @@ func CheckUserPassword(organization string, username string, password string, la
return nil, msg
}
} else {
if msg := CheckPassword(user, password, lang); msg != "" {
if msg := CheckPassword(user, password, lang, enableCaptcha); msg != "" {
return nil, msg
}
}
Expand Down Expand Up @@ -380,7 +390,7 @@ func CheckUpdateUser(oldUser, user *User, lang string) string {
return ""
}

func CheckToEnableCaptcha(application *Application) bool {
func CheckToEnableCaptcha(application *Application, organization, username string) bool {
if len(application.Providers) == 0 {
return false
}
Expand All @@ -390,6 +400,10 @@ func CheckToEnableCaptcha(application *Application) bool {
continue
}
if providerItem.Provider.Category == "Captcha" {
if providerItem.Rule == "Dynamic" {
user := GetUserByFields(organization, username)
return user != nil && user.SigninWrongTimes >= SigninWrongTimesLimit
}
return providerItem.Rule == "Always"
}
}
Expand Down
15 changes: 11 additions & 4 deletions object/check_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ func resetUserSigninErrorTimes(user *User) {
UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, user.IsGlobalAdmin)
}

func recordSigninErrorInfo(user *User, lang string) string {
func recordSigninErrorInfo(user *User, lang string, options ...bool) string {
enableCaptcha := false
if len(options) > 0 {
enableCaptcha = options[0]
}
// increase failed login count
user.SigninWrongTimes++
if user.SigninWrongTimes < SigninWrongTimesLimit {
user.SigninWrongTimes++
}

if user.SigninWrongTimes >= SigninWrongTimesLimit {
// record the latest failed login time
Expand All @@ -57,10 +63,11 @@ func recordSigninErrorInfo(user *User, lang string) string {
// update user
UpdateUser(user.GetId(), user, []string{"signin_wrong_times", "last_signin_wrong_time"}, user.IsGlobalAdmin)
leftChances := SigninWrongTimesLimit - user.SigninWrongTimes
if leftChances > 0 {
if leftChances == 0 && enableCaptcha {
return fmt.Sprint(i18n.Translate(lang, "check:password or code is incorrect"))
} else if leftChances >= 0 {
return fmt.Sprintf(i18n.Translate(lang, "check:password or code is incorrect, you have %d remaining chances"), leftChances)
}

// don't show the chance error message if the user has no chance left
return fmt.Sprintf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), int(LastSignWrongTimeDuration.Minutes()))
}
1 change: 1 addition & 0 deletions routers/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func initAPI() {
beego.Router("/api/saml/metadata", &controllers.ApiController{}, "GET:GetSamlMeta")
beego.Router("/api/webhook", &controllers.ApiController{}, "POST:HandleOfficialAccountEvent")
beego.Router("/api/get-webhook-event", &controllers.ApiController{}, "GET:GetWebhookEventType")
beego.Router("/api/get-captcha-status", &controllers.ApiController{}, "GET:GetCaptchaStatus")

beego.Router("/api/get-organizations", &controllers.ApiController{}, "GET:GetOrganizations")
beego.Router("/api/get-organization", &controllers.ApiController{}, "GET:GetOrganization")
Expand Down
8 changes: 8 additions & 0 deletions swagger/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,14 @@
}
},
"/api/api/get-webhook-event": {
"get": {
"tags": [
"GetCaptchaStatus API"
],
"operationId": "ApiController.GetCaptchaStatus"
}
},
"/api/api/get-captcha-status": {
"get": {
"tags": [
"GetWebhookEventType API"
Expand Down
5 changes: 5 additions & 0 deletions swagger/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,11 @@ paths:
tags:
- GetWebhookEventType API
operationId: ApiController.GetWebhookEventType
/api/api/get-captcha-status:
get:
tags:
- GetCaptchaStatus API
operationId: ApiController.GetCaptchaStatus
/api/api/reset-email-or-phone:
post:
tags:
Expand Down
10 changes: 10 additions & 0 deletions web/src/auth/AuthBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,13 @@ export function getWechatMessageEvent() {
},
}).then(res => res.json());
}

export function getCaptchaStatus(values) {
return fetch(`${Setting.ServerUrl}/api/get-captcha-status?organization=${values["organization"]}&user_id=${values["username"]}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
59 changes: 44 additions & 15 deletions web/src/auth/LoginPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import CustomGithubCorner from "../common/CustomGithubCorner";
import {SendCodeInput} from "../common/SendCodeInput";
import LanguageSelect from "../common/select/LanguageSelect";
import {CaptchaModal} from "../common/modal/CaptchaModal";
import {CaptchaRule} from "../common/modal/CaptchaModal";
import RedirectForm from "../common/RedirectForm";

class LoginPage extends React.Component {
Expand All @@ -47,7 +48,7 @@ class LoginPage extends React.Component {
validEmailOrPhone: false,
validEmail: false,
loginMethod: "password",
enableCaptchaModal: false,
enableCaptchaModal: CaptchaRule.Never,
openCaptchaModal: false,
verifyCaptcha: undefined,
samlResponse: "",
Expand Down Expand Up @@ -81,7 +82,13 @@ class LoginPage extends React.Component {
if (prevProps.application !== this.props.application) {
const captchaProviderItems = this.getCaptchaProviderItems(this.props.application);
if (captchaProviderItems) {
this.setState({enableCaptchaModal: captchaProviderItems.some(providerItem => providerItem.rule === "Always")});
if (captchaProviderItems.some(providerItem => providerItem.rule === "Always")) {
this.setState({enableCaptchaModal: CaptchaRule.Always});
} else if (captchaProviderItems.some(providerItem => providerItem.rule === "Dynamic")) {
this.setState({enableCaptchaModal: CaptchaRule.Dynamic});
} else {
this.setState({enableCaptchaModal: CaptchaRule.Never});
}
}

if (this.props.account && this.props.account.owner === this.props.application?.organization) {
Expand Down Expand Up @@ -110,6 +117,22 @@ class LoginPage extends React.Component {
}
}

checkCaptchaStatus(values) {
AuthBackend.getCaptchaStatus(values)
.then((res) => {
if (res.status === "ok") {
if (res.data) {
this.setState({
openCaptchaModal: true,
values: values,
});
return null;
}
}
this.login(values);
});
}

getApplicationLogin() {
const oAuthParams = Util.getOAuthGetParameters();
AuthBackend.getApplicationLogin(oAuthParams)
Expand Down Expand Up @@ -255,15 +278,19 @@ class LoginPage extends React.Component {
this.signInWithWebAuthn(username, values);
return;
}

if (this.state.loginMethod === "password" && this.state.enableCaptchaModal) {
this.setState({
openCaptchaModal: true,
values: values,
});
} else {
this.login(values);
if (this.state.loginMethod === "password") {
if (this.state.enableCaptchaModal === CaptchaRule.Always) {
this.setState({
openCaptchaModal: true,
values: values,
});
return;
} else if (this.state.enableCaptchaModal === CaptchaRule.Dynamic) {
this.checkCaptchaStatus(values);
return;
}
}
this.login(values);
}

login(values) {
Expand Down Expand Up @@ -544,13 +571,15 @@ class LoginPage extends React.Component {
}

renderCaptchaModal(application) {
if (!this.state.enableCaptchaModal) {
if (this.state.enableCaptchaModal === CaptchaRule.Never) {
return null;
}

const provider = this.getCaptchaProviderItems(application)
.filter(providerItem => providerItem.rule === "Always")
.map(providerItem => providerItem.provider)[0];
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;

return <CaptchaModal
owner={provider.owner}
Expand Down
6 changes: 6 additions & 0 deletions web/src/common/modal/CaptchaModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,9 @@ export const CaptchaModal = (props) => {
</Modal>
);
};

export const CaptchaRule = {
Always: "Always",
Never: "Never",
Dynamic: "Dynamic",
};
Loading

0 comments on commit 6d6cbc7

Please sign in to comment.