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 (
{