2023-05-05 21:23:59 +08:00
// Copyright 2023 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.
2023-06-21 18:56:37 +08:00
import React , { useEffect , useState } from "react" ;
2023-05-05 21:23:59 +08:00
import { Button , Col , Form , Input , Result , Row , Steps } from "antd" ;
2023-06-21 18:56:37 +08:00
import * as ApplicationBackend from "../backend/ApplicationBackend" ;
2023-05-05 21:23:59 +08:00
import * as Setting from "../Setting" ;
import i18next from "i18next" ;
import * as MfaBackend from "../backend/MfaBackend" ;
import { CheckOutlined , KeyOutlined , LockOutlined , UserOutlined } from "@ant-design/icons" ;
import * as UserBackend from "../backend/UserBackend" ;
2023-06-21 18:56:37 +08:00
import { MfaSmsVerifyForm , MfaTotpVerifyForm , mfaSetup } from "./MfaVerifyForm" ;
2023-05-05 21:23:59 +08:00
2023-06-21 18:56:37 +08:00
export const EmailMfaType = "email" ;
2023-05-05 21:23:59 +08:00
export const SmsMfaType = "sms" ;
export const TotpMfaType = "app" ;
function CheckPasswordForm ( { user , onSuccess , onFail } ) {
const [ form ] = Form . useForm ( ) ;
const onFinish = ( { password } ) => {
const data = { ... user , password } ;
UserBackend . checkUserPassword ( data )
. then ( ( res ) => {
if ( res . status === "ok" ) {
onSuccess ( res ) ;
} else {
onFail ( res ) ;
}
} )
. finally ( ( ) => {
form . setFieldsValue ( { password : "" } ) ;
} ) ;
} ;
return (
< Form
form = { form }
style = { { width : "300px" , marginTop : "20px" } }
onFinish = { onFinish }
>
< Form . Item
name = "password"
rules = { [ { required : true , message : i18next . t ( "login:Please input your password!" ) } ] }
>
< Input . Password
prefix = { < LockOutlined / > }
placeholder = { i18next . t ( "general:Password" ) }
/ >
< / F o r m . I t e m >
< Form . Item >
< Button
style = { { marginTop : 24 } }
loading = { false }
block
type = "primary"
htmlType = "submit"
>
{ i18next . t ( "forget:Next Step" ) }
< / B u t t o n >
< / F o r m . I t e m >
< / F o r m >
) ;
}
2023-06-21 18:56:37 +08:00
export function MfaVerifyForm ( { mfaType , application , user , onSuccess , onFail } ) {
2023-05-05 21:23:59 +08:00
const [ form ] = Form . useForm ( ) ;
2023-06-21 18:56:37 +08:00
const [ mfaProps , setMfaProps ] = useState ( { mfaType : mfaType } ) ;
useEffect ( ( ) => {
if ( mfaType === SmsMfaType ) {
setMfaProps ( {
mfaType : mfaType ,
secret : user . phone ,
countryCode : user . countryCode ,
} ) ;
}
if ( mfaType === EmailMfaType ) {
setMfaProps ( {
mfaType : mfaType ,
secret : user . email ,
} ) ;
}
} , [ mfaType ] ) ;
2023-05-05 21:23:59 +08:00
const onFinish = ( { passcode } ) => {
2023-06-21 18:56:37 +08:00
const data = { passcode , mfaType : mfaType , ... user } ;
2023-05-05 21:23:59 +08:00
MfaBackend . MfaSetupVerify ( data )
. then ( ( res ) => {
if ( res . status === "ok" ) {
onSuccess ( res ) ;
} else {
onFail ( res ) ;
}
} )
. catch ( ( error ) => {
Setting . showMessage ( "error" , ` ${ i18next . t ( "general:Failed to connect to server" ) } : ${ error } ` ) ;
} )
. finally ( ( ) => {
form . setFieldsValue ( { passcode : "" } ) ;
} ) ;
} ;
2023-06-21 18:56:37 +08:00
if ( mfaType === null || mfaType === undefined || mfaProps . secret === undefined ) {
return < div > < / d i v > ;
}
if ( mfaType === SmsMfaType || mfaType === EmailMfaType ) {
return < MfaSmsVerifyForm onFinish = { onFinish } application = { application } method = { mfaSetup } mfaProps = { mfaProps } / > ;
} else if ( mfaType === TotpMfaType ) {
return < MfaTotpVerifyForm onFinish = { onFinish } / > ;
2023-05-05 21:23:59 +08:00
} else {
return < div > < / d i v > ;
}
}
2023-06-21 18:56:37 +08:00
function EnableMfaForm ( { user , mfaType , recoveryCodes , onSuccess , onFail } ) {
2023-05-05 21:23:59 +08:00
const [ loading , setLoading ] = useState ( false ) ;
const requestEnableTotp = ( ) => {
const data = {
2023-06-21 18:56:37 +08:00
mfaType ,
2023-05-05 21:23:59 +08:00
... user ,
} ;
setLoading ( true ) ;
MfaBackend . MfaSetupEnable ( data ) . then ( res => {
if ( res . status === "ok" ) {
onSuccess ( res ) ;
} else {
onFail ( res ) ;
}
}
) . finally ( ( ) => {
setLoading ( false ) ;
} ) ;
} ;
return (
< div style = { { width : "400px" } } >
< p > { i18next . t ( "mfa:Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code" ) } < / p >
< br / >
2023-06-21 18:56:37 +08:00
< code style = { { fontStyle : "solid" } } > { recoveryCodes [ 0 ] } < / c o d e >
2023-05-05 21:23:59 +08:00
< Button style = { { marginTop : 24 } } loading = { loading } onClick = { ( ) => {
requestEnableTotp ( ) ;
} } block type = "primary" >
{ i18next . t ( "general:Enable" ) }
< / B u t t o n >
< / d i v >
) ;
}
class MfaSetupPage extends React . Component {
constructor ( props ) {
super ( props ) ;
this . state = {
account : props . account ,
2023-06-21 18:56:37 +08:00
application : this . props . application ? ? null ,
applicationName : props . account . signupApplication ? ? "" ,
2023-05-17 01:13:13 +08:00
isAuthenticated : props . isAuthenticated ? ? false ,
isPromptPage : props . isPromptPage ,
redirectUri : props . redirectUri ,
current : props . current ? ? 0 ,
2023-06-21 18:56:37 +08:00
mfaType : props . mfaType ? ? new URLSearchParams ( props . location ? . search ) ? . get ( "mfaType" ) ? ? SmsMfaType ,
2023-05-05 21:23:59 +08:00
mfaProps : null ,
} ;
}
componentDidMount ( ) {
this . getApplication ( ) ;
}
2023-05-17 01:13:13 +08:00
componentDidUpdate ( prevProps , prevState , snapshot ) {
2023-05-24 23:27:04 +08:00
if ( this . state . isAuthenticated === true && this . state . mfaProps === null ) {
2023-05-17 01:13:13 +08:00
MfaBackend . MfaSetupInitiate ( {
2023-06-21 18:56:37 +08:00
mfaType : this . state . mfaType ,
2023-05-17 01:13:13 +08:00
... this . getUser ( ) ,
} ) . then ( ( res ) => {
if ( res . status === "ok" ) {
this . setState ( {
mfaProps : res . data ,
} ) ;
} else {
Setting . showMessage ( "error" , i18next . t ( "mfa:Failed to initiate MFA" ) ) ;
}
} ) ;
}
}
2023-05-05 21:23:59 +08:00
getApplication ( ) {
2023-06-21 18:56:37 +08:00
if ( this . state . application !== null ) {
return ;
}
2023-05-17 01:13:13 +08:00
ApplicationBackend . getApplication ( "admin" , this . state . applicationName )
2023-05-05 21:23:59 +08:00
. then ( ( application ) => {
if ( application !== null ) {
this . setState ( {
application : application ,
} ) ;
} else {
Setting . showMessage ( "error" , i18next . t ( "mfa:Failed to get application" ) ) ;
}
} ) ;
}
getUser ( ) {
return {
name : this . state . account . name ,
owner : this . state . account . owner ,
} ;
}
renderStep ( ) {
switch ( this . state . current ) {
case 0 :
return < CheckPasswordForm
user = { this . getUser ( ) }
onSuccess = { ( ) => {
2023-05-17 01:13:13 +08:00
this . setState ( {
current : this . state . current + 1 ,
isAuthenticated : true ,
2023-05-05 21:23:59 +08:00
} ) ;
} }
onFail = { ( res ) => {
Setting . showMessage ( "error" , i18next . t ( "mfa:Failed to initiate MFA" ) ) ;
} }
/ > ;
case 1 :
2023-05-17 01:13:13 +08:00
if ( ! this . state . isAuthenticated ) {
return null ;
}
2023-06-21 18:56:37 +08:00
return (
< div >
< MfaVerifyForm
mfaType = { this . state . mfaType }
application = { this . state . application }
user = { this . props . account }
onSuccess = { ( ) => {
this . setState ( {
current : this . state . current + 1 ,
} ) ;
} }
onFail = { ( res ) => {
Setting . showMessage ( "error" , i18next . t ( "general:Failed to verify" ) ) ;
} }
/ >
< Col span = { 24 } style = { { display : "flex" , justifyContent : "left" } } >
{ ( this . state . mfaType === EmailMfaType || this . props . account . mfaEmailEnabled ) ? null :
< Button type = { "link" } onClick = { ( ) => {
if ( this . state . isPromptPage ) {
this . props . history . push ( ` /prompt/ ${ this . state . application . name } ?promptType=mfa&mfaType= ${ EmailMfaType } ` ) ;
} else {
this . props . history . push ( ` /mfa-authentication/setup?mfaType= ${ EmailMfaType } ` ) ;
}
this . setState ( {
mfaType : EmailMfaType ,
} ) ;
}
} > { i18next . t ( "mfa:Use Email" ) } < / B u t t o n >
}
{
( this . state . mfaType === SmsMfaType || this . props . account . mfaPhoneEnabled ) ? null :
< Button type = { "link" } onClick = { ( ) => {
if ( this . state . isPromptPage ) {
this . props . history . push ( ` /prompt/ ${ this . state . application . name } ?promptType=mfa&mfaType= ${ SmsMfaType } ` ) ;
} else {
this . props . history . push ( ` /mfa-authentication/setup?mfaType= ${ SmsMfaType } ` ) ;
}
this . setState ( {
mfaType : SmsMfaType ,
} ) ;
}
} > { i18next . t ( "mfa:Use SMS" ) } < / B u t t o n >
}
< / C o l >
< / d i v >
) ;
2023-05-05 21:23:59 +08:00
case 2 :
2023-05-17 01:13:13 +08:00
if ( ! this . state . isAuthenticated ) {
return null ;
}
2023-06-21 18:56:37 +08:00
return < EnableMfaForm user = { this . getUser ( ) } mfaType = { this . state . mfaType } recoveryCodes = { this . state . mfaProps . recoveryCodes }
2023-05-05 21:23:59 +08:00
onSuccess = { ( ) => {
Setting . showMessage ( "success" , i18next . t ( "general:Enabled successfully" ) ) ;
2023-05-17 01:13:13 +08:00
if ( this . state . isPromptPage && this . state . redirectUri ) {
Setting . goToLink ( this . state . redirectUri ) ;
} else {
Setting . goToLink ( "/account" ) ;
}
2023-05-05 21:23:59 +08:00
} }
onFail = { ( res ) => {
Setting . showMessage ( "error" , ` ${ i18next . t ( "general:Failed to enable" ) } : ${ res . msg } ` ) ;
} } / > ;
default :
return null ;
}
}
render ( ) {
if ( ! this . props . account ) {
return (
< Result
status = "403"
title = "403 Unauthorized"
subTitle = { i18next . t ( "general:Sorry, you do not have permission to access this page or logged in status invalid." ) }
extra = { < a href = "/" > < Button type = "primary" > { i18next . t ( "general:Back Home" ) } < / B u t t o n > < / a > }
/ >
) ;
}
return (
< Row >
< Col span = { 24 } style = { { justifyContent : "center" } } >
< Row >
< Col span = { 24 } >
< div style = { { textAlign : "center" , fontSize : "28px" } } >
{ i18next . t ( "mfa:Protect your account with Multi-factor authentication" ) } < / d i v >
< div style = { { textAlign : "center" , fontSize : "16px" , marginTop : "10px" } } > { i18next . t ( "mfa:Each time you sign in to your Account, you'll need your password and a authentication code" ) } < / d i v >
< / C o l >
< / R o w >
< Row >
< Col span = { 24 } >
2023-06-21 18:56:37 +08:00
< Steps current = { this . state . current }
items = { [
{ title : i18next . t ( "mfa:Verify Password" ) , icon : < UserOutlined / > } ,
{ title : i18next . t ( "mfa:Verify Code" ) , icon : < KeyOutlined / > } ,
{ title : i18next . t ( "general:Enable" ) , icon : < CheckOutlined / > } ,
] }
style = { { width : "90%" , maxWidth : "500px" , margin : "auto" , marginTop : "80px" ,
} } >
2023-05-05 21:23:59 +08:00
< / S t e p s >
< / C o l >
< / R o w >
< / C o l >
< Col span = { 24 } style = { { display : "flex" , justifyContent : "center" } } >
2023-05-17 01:13:13 +08:00
< div style = { { marginTop : "10px" , textAlign : "center" } } >
{ this . renderStep ( ) }
< / d i v >
2023-05-05 21:23:59 +08:00
< / C o l >
< / R o w >
) ;
}
}
export default MfaSetupPage ;