diff --git a/controllers/auth.go b/controllers/auth.go
index c92281f8..763c9305 100644
--- a/controllers/auth.go
+++ b/controllers/auth.go
@@ -306,6 +306,35 @@ func isProxyProviderType(providerType string) bool {
return false
}
+func checkMfaEnable(c *ApiController, user *object.User, organization *object.Organization, verificationType string) bool {
+ if object.IsNeedPromptMfa(organization, user) {
+ // The prompt page needs the user to be srigned in
+ c.SetSessionUsername(user.GetId())
+ c.ResponseOk(object.RequiredMfa)
+ return true
+ }
+
+ if user.IsMfaEnabled() {
+ c.setMfaUserSession(user.GetId())
+ mfaList := object.GetAllMfaProps(user, true)
+ mfaAllowList := []*object.MfaProps{}
+ for _, prop := range mfaList {
+ if prop.MfaType == verificationType || !prop.Enabled {
+ continue
+ }
+ mfaAllowList = append(mfaAllowList, prop)
+ }
+ if len(mfaAllowList) >= 1 {
+ c.SetSession("verificationCodeType", verificationType)
+ c.Ctx.Input.CruSession.SessionRelease(c.Ctx.ResponseWriter)
+ c.ResponseOk(object.NextMfa, mfaAllowList)
+ return true
+ }
+ }
+
+ return false
+}
+
// Login ...
// @Title Login
// @Tag Login API
@@ -523,30 +552,10 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
}
- if object.IsNeedPromptMfa(organization, user) {
- // The prompt page needs the user to be signed in
- c.SetSessionUsername(user.GetId())
- c.ResponseOk(object.RequiredMfa)
+ if checkMfaEnable(c, user, organization, verificationType) {
return
}
- if user.IsMfaEnabled() {
- c.setMfaUserSession(user.GetId())
- mfaList := object.GetAllMfaProps(user, true)
- mfaAllowList := []*object.MfaProps{}
- for _, prop := range mfaList {
- if prop.MfaType == verificationType || !prop.Enabled {
- continue
- }
- mfaAllowList = append(mfaAllowList, prop)
- }
- if len(mfaAllowList) >= 1 {
- c.SetSession("verificationCodeType", verificationType)
- c.ResponseOk(object.NextMfa, mfaAllowList)
- return
- }
- }
-
resp = c.HandleLoggedIn(application, user, &authForm)
c.Ctx.Input.SetParam("recordUserId", user.GetId())
@@ -679,6 +688,11 @@ func (c *ApiController) Login() {
c.ResponseError(err.Error())
return
}
+
+ if checkMfaEnable(c, user, organization, verificationType) {
+ return
+ }
+
resp = c.HandleLoggedIn(application, user, &authForm)
c.Ctx.Input.SetParam("recordUserId", user.GetId())
@@ -914,7 +928,11 @@ func (c *ApiController) Login() {
}
var application *object.Application
- application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
+ if authForm.ClientId == "" {
+ application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application))
+ } else {
+ application, err = object.GetApplicationByClientId(authForm.ClientId)
+ }
if err != nil {
c.ResponseError(err.Error())
return
@@ -944,6 +962,10 @@ func (c *ApiController) Login() {
return
}
+ if authForm.Provider == "" {
+ authForm.Provider = authForm.ProviderBack
+ }
+
user := c.getCurrentUser()
resp = c.HandleLoggedIn(application, user, &authForm)
diff --git a/form/auth.go b/form/auth.go
index ca704223..72057b90 100644
--- a/form/auth.go
+++ b/form/auth.go
@@ -37,13 +37,14 @@ type AuthForm struct {
Region string `json:"region"`
InvitationCode string `json:"invitationCode"`
- Application string `json:"application"`
- ClientId string `json:"clientId"`
- Provider string `json:"provider"`
- Code string `json:"code"`
- State string `json:"state"`
- RedirectUri string `json:"redirectUri"`
- Method string `json:"method"`
+ Application string `json:"application"`
+ ClientId string `json:"clientId"`
+ Provider string `json:"provider"`
+ ProviderBack string `json:"providerBack"`
+ Code string `json:"code"`
+ State string `json:"state"`
+ RedirectUri string `json:"redirectUri"`
+ Method string `json:"method"`
EmailCode string `json:"emailCode"`
PhoneCode string `json:"phoneCode"`
diff --git a/routers/static_filter.go b/routers/static_filter.go
index 47cb0360..0d1addb4 100644
--- a/routers/static_filter.go
+++ b/routers/static_filter.go
@@ -133,6 +133,9 @@ func StaticFilter(ctx *context.Context) {
path += urlPath
}
+ // Preventing synchronization problems from concurrency
+ ctx.Input.CruSession = nil
+
organizationThemeCookie, err := appendThemeCookie(ctx, urlPath)
if err != nil {
fmt.Println(err)
diff --git a/web/src/App.js b/web/src/App.js
index 6961dec1..4c22028d 100644
--- a/web/src/App.js
+++ b/web/src/App.js
@@ -361,6 +361,14 @@ class App extends Component {
}
};
+ onLoginSuccess(redirectUrl) {
+ window.google?.accounts?.id?.cancel();
+ if (redirectUrl) {
+ localStorage.setItem("mfaRedirectUrl", redirectUrl);
+ }
+ this.getAccount();
+ }
+
renderPage() {
if (this.isDoorPages()) {
let themeData = this.state.themeData;
@@ -401,19 +409,13 @@ class App extends Component {
application: application,
});
}}
- onLoginSuccess={(redirectUrl) => {
- window.google?.accounts?.id?.cancel();
- if (redirectUrl) {
- localStorage.setItem("mfaRedirectUrl", redirectUrl);
- }
- this.getAccount();
- }}
+ onLoginSuccess={(redirectUrl) => {this.onLoginSuccess(redirectUrl);}}
onUpdateAccount={(account) => this.onUpdateAccount(account)}
updataThemeData={this.setTheme}
/> :
-
-
+ {this.onLoginSuccess(redirectUrl);}} />} />
+ {this.onLoginSuccess(redirectUrl);}} />} />
} />} />
diff --git a/web/src/Setting.js b/web/src/Setting.js
index 14cbc779..e523344b 100644
--- a/web/src/Setting.js
+++ b/web/src/Setting.js
@@ -14,7 +14,7 @@
import React from "react";
import {Link} from "react-router-dom";
-import {Select, Tag, Tooltip, message, theme} from "antd";
+import {Button, Select, Tag, Tooltip, message, theme} from "antd";
import {QuestionCircleTwoTone} from "@ant-design/icons";
import {isMobile as isMobileDevice} from "react-device-detect";
import "./i18n";
@@ -25,6 +25,8 @@ import {Helmet} from "react-helmet";
import * as Conf from "./Conf";
import * as phoneNumber from "libphonenumber-js";
import moment from "moment";
+import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./auth/mfa/MfaAuthVerifyForm";
+import {EmailMfaType, SmsMfaType, TotpMfaType} from "./auth/MfaSetupPage";
const {Option} = Select;
@@ -1588,3 +1590,114 @@ export function getCurrencyText(product) {
export function isDarkTheme(themeAlgorithm) {
return themeAlgorithm && themeAlgorithm.includes("dark");
}
+
+function getPreferredMfaProp(mfaProps) {
+ for (const i in mfaProps) {
+ if (mfaProps[i].isPreffered) {
+ return mfaProps[i];
+ }
+ }
+ return mfaProps[0];
+}
+
+export function checkLoginMfa(res, body, params, handleLogin, componentThis, requireRedirect = null) {
+ if (res.data === RequiredMfa) {
+ if (!requireRedirect) {
+ componentThis.props.onLoginSuccess(window.location.href);
+ } else {
+ componentThis.props.onLoginSuccess(requireRedirect);
+ }
+ } else if (res.data === NextMfa) {
+ componentThis.setState({
+ mfaProps: res.data2,
+ selectedMfaProp: getPreferredMfaProp(res.data2),
+ }, () => {
+ body["providerBack"] = body["provider"];
+ body["provider"] = "";
+ componentThis.setState({
+ getVerifyTotp: () => renderMfaAuthVerifyForm(body, params, handleLogin, componentThis),
+ });
+ });
+ } else if (res.data === "SelectPlan") {
+ // paid-user does not have active or pending subscription, go to application default pricing page to select-plan
+ const pricing = res.data2;
+ goToLink(`/select-plan/${pricing.owner}/${pricing.name}?user=${body.username}`);
+ } else if (res.data === "BuyPlanResult") {
+ // paid-user has pending subscription, go to buy-plan/result apge to notify payment result
+ const sub = res.data2;
+ goToLink(`/buy-plan/${sub.owner}/${sub.pricing}/result?subscription=${sub.name}`);
+ } else {
+ handleLogin(res);
+ }
+}
+
+export function getApplicationObj(componentThis) {
+ return componentThis.props.application;
+}
+
+export function parseOffset(offset) {
+ if (offset === 2 || offset === 4 || inIframe() || isMobile()) {
+ return "0 auto";
+ }
+ if (offset === 1) {
+ return "0 10%";
+ }
+ if (offset === 3) {
+ return "0 60%";
+ }
+}
+
+function renderMfaAuthVerifyForm(values, authParams, onSuccess, componentThis) {
+ return (
+
+
{
+ showMessage("error", errorMessage);
+ }}
+ onSuccess={(res) => onSuccess(res)}
+ />
+
+ {
+ componentThis.state.mfaProps.map((mfa) => {
+ if (componentThis.state.selectedMfaProp.mfaType === mfa.mfaType) {return null;}
+ let mfaI18n = "";
+ switch (mfa.mfaType) {
+ case SmsMfaType: mfaI18n = i18next.t("mfa:Use SMS"); break;
+ case TotpMfaType: mfaI18n = i18next.t("mfa:Use Authenticator App"); break ;
+ case EmailMfaType: mfaI18n = i18next.t("mfa:Use Email") ;break;
+ }
+ return
;
+ })
+ }
+
+ );
+}
+
+export function renderLoginPanel(application, getInnerComponent, componentThis) {
+ return (
+
+ {inIframe() || isMobile() ? null :
}
+ {inIframe() || !isMobile() ? null :
}
+
+
+
+
+ {
+ getInnerComponent()
+ }
+
+
+
+
+ );
+}
diff --git a/web/src/auth/AuthCallback.js b/web/src/auth/AuthCallback.js
index da88c46c..a3b53b3c 100644
--- a/web/src/auth/AuthCallback.js
+++ b/web/src/auth/AuthCallback.js
@@ -21,6 +21,7 @@ import {authConfig} from "./Auth";
import * as Setting from "../Setting";
import i18next from "i18next";
import RedirectForm from "../common/RedirectForm";
+import {renderLoginPanel} from "../Setting";
class AuthCallback extends React.Component {
constructor(props) {
@@ -131,19 +132,23 @@ class AuthCallback extends React.Component {
// user is using casdoor as cas sso server, and wants the ticket to be acquired
AuthBackend.loginCas(body, {"service": casService}).then((res) => {
if (res.status === "ok") {
- let msg = "Logged in successfully.";
- if (casService === "") {
- // If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
- msg += "Now you can visit apps protected by Casdoor.";
- }
- Setting.showMessage("success", msg);
+ const handleCasLogin = (res) => {
+ let msg = "Logged in successfully.";
+ if (casService === "") {
+ // If service was not specified, Casdoor must display a message notifying the client that it has successfully initiated a single sign-on session.
+ msg += "Now you can visit apps protected by Casdoor.";
+ }
+ Setting.showMessage("success", msg);
- if (casService !== "") {
- const st = res.data;
- const newUrl = new URL(casService);
- newUrl.searchParams.append("ticket", st);
- window.location.href = newUrl.toString();
- }
+ if (casService !== "") {
+ const st = res.data;
+ const newUrl = new URL(casService);
+ newUrl.searchParams.append("ticket", st);
+ window.location.href = newUrl.toString();
+ }
+ };
+
+ Setting.checkLoginMfa(res, body, {"service": casService}, handleCasLogin, this);
} else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
}
@@ -159,54 +164,58 @@ class AuthCallback extends React.Component {
.then((res) => {
if (res.status === "ok") {
const responseType = this.getResponseType();
- if (responseType === "login") {
- if (res.data2) {
- sessionStorage.setItem("signinUrl", signinUrl);
- Setting.goToLinkSoft(this, `/forget/${applicationName}`);
- return;
- }
- Setting.showMessage("success", "Logged in successfully");
- // Setting.goToLinkSoft(this, "/");
- const link = Setting.getFromLink();
- Setting.goToLink(link);
- } else if (responseType === "code") {
- if (res.data2) {
- sessionStorage.setItem("signinUrl", signinUrl);
- Setting.goToLinkSoft(this, `/forget/${applicationName}`);
- return;
- }
- const code = res.data;
- Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
- // Setting.showMessage("success", `Authorization code: ${res.data}`);
- } else if (responseType === "token" || responseType === "id_token") {
- if (res.data2) {
- sessionStorage.setItem("signinUrl", signinUrl);
- Setting.goToLinkSoft(this, `/forget/${applicationName}`);
- return;
- }
- const token = res.data;
- Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
- } else if (responseType === "link") {
- const from = innerParams.get("from");
- Setting.goToLinkSoftOrJumpSelf(this, from);
- } else if (responseType === "saml") {
- if (res.data2.method === "POST") {
- this.setState({
- samlResponse: res.data,
- redirectUrl: res.data2.redirectUrl,
- relayState: oAuthParams.relayState,
- });
- } else {
- if (res.data2.needUpdatePassword) {
+ const handleLogin = (res) => {
+ if (responseType === "login") {
+ if (res.data2) {
sessionStorage.setItem("signinUrl", signinUrl);
Setting.goToLinkSoft(this, `/forget/${applicationName}`);
return;
}
- const SAMLResponse = res.data;
- const redirectUri = res.data2.redirectUrl;
- Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
+ Setting.showMessage("success", "Logged in successfully");
+ // Setting.goToLinkSoft(this, "/");
+ const link = Setting.getFromLink();
+ Setting.goToLink(link);
+ } else if (responseType === "code") {
+ if (res.data2) {
+ sessionStorage.setItem("signinUrl", signinUrl);
+ Setting.goToLinkSoft(this, `/forget/${applicationName}`);
+ return;
+ }
+ const code = res.data;
+ Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}code=${code}&state=${oAuthParams.state}`);
+ // Setting.showMessage("success", `Authorization code: ${res.data}`);
+ } else if (responseType === "token" || responseType === "id_token") {
+ if (res.data2) {
+ sessionStorage.setItem("signinUrl", signinUrl);
+ Setting.goToLinkSoft(this, `/forget/${applicationName}`);
+ return;
+ }
+ const token = res.data;
+ Setting.goToLink(`${oAuthParams.redirectUri}${concatChar}${responseType}=${token}&state=${oAuthParams.state}&token_type=bearer`);
+ } else if (responseType === "link") {
+ const from = innerParams.get("from");
+ Setting.goToLinkSoftOrJumpSelf(this, from);
+ } else if (responseType === "saml") {
+ if (res.data2.method === "POST") {
+ this.setState({
+ samlResponse: res.data,
+ redirectUrl: res.data2.redirectUrl,
+ relayState: oAuthParams.relayState,
+ });
+ } else {
+ if (res.data2.needUpdatePassword) {
+ sessionStorage.setItem("signinUrl", signinUrl);
+ Setting.goToLinkSoft(this, `/forget/${applicationName}`);
+ return;
+ }
+ const SAMLResponse = res.data;
+ const redirectUri = res.data2.redirectUrl;
+ Setting.goToLink(`${redirectUri}${redirectUri.includes("?") ? "&" : "?"}SAMLResponse=${encodeURIComponent(SAMLResponse)}&RelayState=${oAuthParams.relayState}`);
+ }
}
- }
+ };
+
+ Setting.checkLoginMfa(res, body, oAuthParams, handleLogin, this, window.location.origin);
} else {
this.setState({
msg: res.msg,
@@ -220,6 +229,11 @@ class AuthCallback extends React.Component {
return ;
}
+ if (this.state.getVerifyTotp !== undefined) {
+ const application = Setting.getApplicationObj(this);
+ return renderLoginPanel(application, this.state.getVerifyTotp, this);
+ }
+
return (
{
diff --git a/web/src/auth/LoginPage.js b/web/src/auth/LoginPage.js
index 3a22b7d2..3d737173 100644
--- a/web/src/auth/LoginPage.js
+++ b/web/src/auth/LoginPage.js
@@ -34,10 +34,9 @@ import {SendCodeInput} from "../common/SendCodeInput";
import LanguageSelect from "../common/select/LanguageSelect";
import {CaptchaModal, CaptchaRule} from "../common/modal/CaptchaModal";
import RedirectForm from "../common/RedirectForm";
-import {MfaAuthVerifyForm, NextMfa, RequiredMfa} from "./mfa/MfaAuthVerifyForm";
+import {RequiredMfa} from "./mfa/MfaAuthVerifyForm";
import {GoogleOneTapLoginVirtualButton} from "./GoogleLoginButton";
import * as ProviderButton from "./ProviderButton";
-import {EmailMfaType, SmsMfaType, TotpMfaType} from "./MfaSetupPage";
const FaceRecognitionModal = lazy(() => import("../common/modal/FaceRecognitionModal"));
class LoginPage extends React.Component {
@@ -439,18 +438,7 @@ class LoginPage extends React.Component {
};
if (res.status === "ok") {
- if (res.data === NextMfa) {
- this.setState({
- mfaProps: res.data2,
- selectedMfaProp: this.getPreferredMfaProp(res.data2),
- }, () => {
- this.setState({
- getVerifyTotp: () => this.renderMfaAuthVerifyForm(values, casParams, loginHandler),
- });
- });
- } else {
- loginHandler(res);
- }
+ Setting.checkLoginMfa(res, values, casParams, loginHandler, this);
} else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
}
@@ -505,26 +493,7 @@ class LoginPage extends React.Component {
};
if (res.status === "ok") {
- if (res.data === NextMfa) {
- this.setState({
- mfaProps: res.data2,
- selectedMfaProp: this.getPreferredMfaProp(res.data2),
- }, () => {
- this.setState({
- getVerifyTotp: () => this.renderMfaAuthVerifyForm(values, oAuthParams, loginHandler),
- });
- });
- } else if (res.data === "SelectPlan") {
- // paid-user does not have active or pending subscription, go to application default pricing page to select-plan
- const pricing = res.data2;
- Setting.goToLink(`/select-plan/${pricing.owner}/${pricing.name}?user=${values.username}`);
- } else if (res.data === "BuyPlanResult") {
- // paid-user has pending subscription, go to buy-plan/result apge to notify payment result
- const sub = res.data2;
- Setting.goToLink(`/buy-plan/${sub.owner}/${sub.pricing}/result?subscription=${sub.name}`);
- } else {
- loginHandler(res);
- }
+ Setting.checkLoginMfa(res, values, oAuthParams, loginHandler, this);
} else {
Setting.showMessage("error", `${i18next.t("application:Failed to sign in")}: ${res.msg}`);
}
@@ -532,49 +501,6 @@ class LoginPage extends React.Component {
}
}
- renderMfaAuthVerifyForm(values, authParams, onSuccess) {
- return (
-
-
{
- Setting.showMessage("error", errorMessage);
- }}
- onSuccess={(res) => onSuccess(res)}
- />
-
- {
- this.state.mfaProps.map((mfa) => {
- if (this.state.selectedMfaProp.mfaType === mfa.mfaType) {return null;}
- let mfaI18n = "";
- switch (mfa.mfaType) {
- case SmsMfaType: mfaI18n = i18next.t("mfa:Use SMS"); break;
- case TotpMfaType: mfaI18n = i18next.t("mfa:Use Authenticator App"); break ;
- case EmailMfaType: mfaI18n = i18next.t("mfa:Use Email") ;break;
- }
- return
;
- })
- }
-
- );
- }
-
- getPreferredMfaProp(mfaProps) {
- for (const i in mfaProps) {
- if (mfaProps[i].isPreffered) {
- return mfaProps[i];
- }
- }
- return mfaProps[0];
- }
-
isProviderVisible(providerItem) {
if (this.state.mode === "signup") {
return Setting.isProviderVisibleForSignUp(providerItem);
diff --git a/web/src/auth/SamlCallback.js b/web/src/auth/SamlCallback.js
index 18351c5b..61f8a89c 100644
--- a/web/src/auth/SamlCallback.js
+++ b/web/src/auth/SamlCallback.js
@@ -20,6 +20,7 @@ import * as Util from "./Util";
import * as Setting from "../Setting";
import i18next from "i18next";
import {authConfig} from "./Auth";
+import {renderLoginPanel} from "../Setting";
class SamlCallback extends React.Component {
constructor(props) {
@@ -81,13 +82,26 @@ class SamlCallback extends React.Component {
.then((res) => {
if (res.status === "ok") {
const responseType = this.getResponseType(redirectUri);
- if (responseType === "login") {
- Setting.showMessage("success", "Logged in successfully");
- Setting.goToLink("/");
- } else if (responseType === "code") {
- const code = res.data;
- Setting.goToLink(`${redirectUri}?code=${code}&state=${state}`);
- }
+ const handleLogin = (res2) => {
+ if (responseType === "login") {
+ Setting.showMessage("success", "Logged in successfully");
+ Setting.goToLink("/");
+ } else if (responseType === "code") {
+ const code = res2.data;
+ Setting.goToLink(`${redirectUri}?code=${code}&state=${state}`);
+ }
+ };
+ Setting.checkLoginMfa(res, body, {
+ clientId: clientId,
+ responseType: responseType,
+ redirectUri: messages[3],
+ state: state,
+ nonce: "",
+ scope: "read",
+ challengeMethod: "",
+ codeChallenge: "",
+ type: "code",
+ }, handleLogin, this);
} else {
this.setState({
msg: res.msg,
@@ -97,6 +111,11 @@ class SamlCallback extends React.Component {
}
render() {
+ if (this.state.getVerifyTotp !== undefined) {
+ const application = Setting.getApplicationObj(this);
+ return renderLoginPanel(application, this.state.getVerifyTotp, this, window.location.origin);
+ }
+
return (
{