From 0974ac9260c81dc0b6bcc6e6807d43945de7b301 Mon Sep 17 00:00:00 2001 From: Roman Date: Sun, 7 Apr 2024 14:23:59 +0200 Subject: [PATCH] 2FA totp, bugfix --- Core/API/MailAPI.class.php | 1 - Core/API/TfaAPI.class.php | 30 +++++- Core/Localization/de_DE/account.php | 2 + Core/Localization/de_DE/general.php | 1 + Core/Localization/en_US/account.php | 2 + Core/Localization/en_US/general.php | 1 + img/icons/nitrokey.png | Bin 11663 -> 8373 bytes react/admin-panel/src/AdminDashboard.jsx | 2 +- react/admin-panel/src/elements/dialog.jsx | 47 --------- .../src/views/access-control-list.js | 7 +- .../admin-panel/src/views/group/group-edit.js | 64 ++++++------- .../admin-panel/src/views/profile/mfa-fido.js | 26 +++++ .../admin-panel/src/views/profile/mfa-totp.js | 55 +++++++++++ .../admin-panel/src/views/profile/profile.js | 90 +++++++++++++++++- .../admin-panel/src/views/route/route-list.js | 2 +- react/package.json | 2 + react/shared/elements/dialog.jsx | 23 +++-- react/shared/elements/search-field.js | 3 +- react/shared/views/login.jsx | 2 +- react/yarn.lock | 7 +- test/TimeBasedTwoFactorToken.test.php | 1 - 21 files changed, 262 insertions(+), 106 deletions(-) delete mode 100644 react/admin-panel/src/elements/dialog.jsx create mode 100644 react/admin-panel/src/views/profile/mfa-fido.js create mode 100644 react/admin-panel/src/views/profile/mfa-totp.js diff --git a/Core/API/MailAPI.class.php b/Core/API/MailAPI.class.php index da3a354..3388a77 100644 --- a/Core/API/MailAPI.class.php +++ b/Core/API/MailAPI.class.php @@ -55,7 +55,6 @@ namespace Core\API\Mail { use Core\External\PHPMailer\PHPMailer; use Core\Objects\Context; use Core\Objects\DatabaseEntity\GpgKey; - use PhpParser\Node\Param; class Test extends MailAPI { diff --git a/Core/API/TfaAPI.class.php b/Core/API/TfaAPI.class.php index 76d83b6..8e8366e 100644 --- a/Core/API/TfaAPI.class.php +++ b/Core/API/TfaAPI.class.php @@ -62,6 +62,7 @@ namespace Core\API\TFA { use Core\API\Parameter\StringType; use Core\API\TfaAPI; use Core\Driver\SQL\Condition\Compare; + use Core\Driver\SQL\Query\Insert; use Core\Objects\Context; use Core\Objects\TwoFactor\AttestationObject; use Core\Objects\TwoFactor\AuthenticationData; @@ -131,6 +132,10 @@ namespace Core\API\TFA { return $this->success; } + + public static function getDefaultACL(Insert $insert): void { + $insert->addRow(self::getEndpoint(), [], "Allows users to remove their 2FA-Tokens", true); + } } // TOTP @@ -167,11 +172,16 @@ namespace Core\API\TFA { $this->disableCache(); die($twoFactorToken->generateQRCode($this->context)); } + + public static function getDefaultACL(Insert $insert): void { + $insert->addRow(self::getEndpoint(), [], "Allows users generate a QR-code to add a time-based 2FA-Token", true); + } } class ConfirmTotp extends VerifyTotp { public function __construct(Context $context, bool $externalCall = false) { parent::__construct($context, $externalCall); + $this->loginRequired = true; } public function _execute(): bool { @@ -196,6 +206,10 @@ namespace Core\API\TFA { return $this->success; } + + public static function getDefaultACL(Insert $insert): void { + $insert->addRow(self::getEndpoint(), [], "Allows users to confirm their time-based 2FA-Token", true); + } } class VerifyTotp extends TfaAPI { @@ -211,10 +225,6 @@ namespace Core\API\TFA { public function _execute(): bool { $currentUser = $this->context->getUser(); - if (!$currentUser) { - return $this->createError("You are not logged in."); - } - $twoFactorToken = $currentUser->getTwoFactorToken(); if (!$twoFactorToken) { return $this->createError("You did not add a two factor token yet."); @@ -230,6 +240,10 @@ namespace Core\API\TFA { $twoFactorToken->authenticate(); return $this->success; } + + public static function getDefaultACL(Insert $insert): void { + $insert->addRow(self::getEndpoint(), [], "Allows users to verify time-based 2FA-Tokens", true); + } } // Key @@ -326,6 +340,10 @@ namespace Core\API\TFA { return $this->success; } + + public static function getDefaultACL(Insert $insert): void { + $insert->addRow(self::getEndpoint(), [], "Allows users to register a 2FA hardware-key", true); + } } class VerifyKey extends TfaAPI { @@ -384,5 +402,9 @@ namespace Core\API\TFA { return $this->success; } + + public static function getDefaultACL(Insert $insert): void { + $insert->addRow(self::getEndpoint(), [], "Allows users to verify a 2FA hardware-key", true); + } } } \ No newline at end of file diff --git a/Core/Localization/de_DE/account.php b/Core/Localization/de_DE/account.php index b377c25..6af0dcd 100644 --- a/Core/Localization/de_DE/account.php +++ b/Core/Localization/de_DE/account.php @@ -90,6 +90,8 @@ return [ "gpg_key_placeholder_text" => "GPG-Key im ASCII format reinziehen oder einfügen...", # 2fa + "2fa_type_totp" => "Zeitbasiertes 2FA (TOTP)", + "2fa_type_fido" => "Schlüsselbasiertes 2FA", "register_2fa_device" => "Ein 2FA-Gerät registrieren", "register_2fa_totp_text" => "Scan den QR-Code mit einem Gerät, das du als Zwei-Faktor-Authentifizierung (2FA) benutzen willst. " . "Unter Android kannst du den Google Authenticator benutzen.", diff --git a/Core/Localization/de_DE/general.php b/Core/Localization/de_DE/general.php index 150e549..c7e307a 100644 --- a/Core/Localization/de_DE/general.php +++ b/Core/Localization/de_DE/general.php @@ -35,6 +35,7 @@ return [ "no" => "Nein", "create_new" => "Erstellen", "unchanged" => "Unverändert", + "click_to_copy" => "Klicken zum Kopieren", # dialog / actions "action" => "Aktion", diff --git a/Core/Localization/en_US/account.php b/Core/Localization/en_US/account.php index cc49424..19a10dd 100644 --- a/Core/Localization/en_US/account.php +++ b/Core/Localization/en_US/account.php @@ -90,6 +90,8 @@ return [ "gpg_key_placeholder_text" => "Paste or drag'n'drop your GPG-Key in ASCII format...", # 2fa + "2fa_type_totp" => "Time-Based 2FA (TOTP)", + "2fa_type_fido" => "Key-Based 2FA", "register_2fa_device" => "Register a 2FA-Device", "register_2fa_totp_text" => "Scan the QR-Code with a device you want to use for Two-Factor-Authentication (2FA). " . "On Android, you can use the Google Authenticator.", diff --git a/Core/Localization/en_US/general.php b/Core/Localization/en_US/general.php index 3559ddb..aaa8366 100644 --- a/Core/Localization/en_US/general.php +++ b/Core/Localization/en_US/general.php @@ -17,6 +17,7 @@ return [ "no" => "No", "create_new" => "Create", "unchanged" => "Unchanged", + "click_to_copy" => "Click to copy", # dialog / actions "action" => "Action", diff --git a/img/icons/nitrokey.png b/img/icons/nitrokey.png index e463ffddbc6b2aaef1a635892258ca378e05a528..5c69624b48c5d95b21ccd52b825c561bc65d33e3 100644 GIT binary patch literal 8373 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 LNkvXXu0mjf!n`P; literal 11663 zcmXw9Ra9GBw>|`lOK^(2dvSMfad#*V#hv2rQrsPiySoN=3-0c2z4;$*#z^v#v9tG@ zYt8vtgwhXb6hwT)|Ni?AMOH>a1@b-mzc)NAQf8@18B!%*@XRMaZK6q1OiZ=K-BcA(|wmpZ&SVST<`_2def z+GdeFoGyyWgAb24tB}V2&`m~FwGk=*Pz1JXjbZw-VGwlrI2ipC{rU4Jt2-t}_~$b) zP4W2nxPPjeYF$s?Xn78&fKZqdikJ7o1Oc#4=CMrd;q|&ZJekScv!iYF>%Hfn zhCdi+aeoFz`htX0WNCFN0u0Z%eg>&}yx9i&^=fqfyAoDuTkU?P(*Cru{TaR4<=sEI z_*H$fLaCO=ofJM;laj}i9+^P!x8wOULikX6J%K`xB4q+X$Vnb&lT~DX^;E6>awzic z7vDJ6s3B-NI+9vWOSsNX9|z$}DBj2Kc*JDzq9E~4`Ch@RtL0~(gdJ7mJ%MRGb}9gb&Fs>QREOPi_eAIm7jaHCAw zzkPFf9!2o*dfA>xZ&*zW+l7m;T`*OzVP0R?n`x4#*`G`bvv2T_2Et4Ge7=7<D1Qccyj@

Bb8Rx zE%S10F(knD=Le66*PAi8`9kI;zMwCg_8`8d91jn#KVRCtzQn{zJITI*NA%dlEJ?wU zI+hqIq_h<6M_7nslU_dhyES#S?{AyAkJI&X!CTAO^VFH4SEQ>?Z#2JuRo9O<14*Td z*hro}&G^EaN{eo1z7HD|F(DUdLU^L5v8U)v}tJxT$PDO=M8=+5ENoF?8 zr$#Lcs7OdikOx>TWI#qidEugyh6g}hzq*q9aN7msZsBzLJVX-i^;rKd^m(}*MENQ5 zZ1I#*9Deh=PWOs#%Yup2_e8T=jn-NOSk&V%GUixX3yl=bj(T}!>~NQC z@>>;-^nwk7dXle(J-2pPqC2h(bD|X~&3LDWfDVo3nOV@P#+oy3KPb3AASf)N18Kn$ zl=#AI2W}=~%C|!z*pz&yamp5fR6RDEC&Kd%XJ@UcqCR$%n+8cJp7z~?idL%@`1*7P zy-fDwxusvYay&Lp(LZj>+;)@DpW7Y-dnhkXtx5;`mw47pM+umRL{?_*PJni{m`i{rs zQZF@deo3gm?#TL79SC;%%Js+Z&IA&?LJ6;T{z9RLA{I5CsQ&8 zvL`{~mT!+nKJWJlgsQ2`c;X+h+JsaOCwy|&!6M-UM!g!lKW^?%Fs<4UgPwIdo}0at z|Nf-o;YSJG+lHdmsHA0iEDL-B(ZR#7OXUm?RzHsSd4N=MSf#sz z>X0*RqMoCJ_;Wc2SpWNBcPKn7XYA`Oi}4>860gTUkH2lyYK9xs z-|wz>C_G-C$yiz8L7*$o2bpJdDaI6>6bEPLP;vJILaIe6Qp35qS|(aN0xqwe#|v1};PzkqxKZz}suK~Uw z=H*Tp*Y5UUdo;>5c%uIU2zBcg#U{$i?x*nyn6mrYE&z48JPXyz6a56jxB~=!KN1r0 zyVF1cpED3Zrvu~fU(m(H zwFf*t#i^h#;QdI*^#-Rx%Mdo&p%u+{rp?`#&EKLvm&VTBD)I?92Dfk{cqp~a^<){U z`lC+g^!UeJQQNL!gcr5kuCn|pbxM0xskCHUkL^OFDD;Wkqdzkt5^%wCzg*wZBb>mk zQftG|U^Wbedor!_StS=G#FtuSFJ^OpL>}DefU=)P8T_V`a~{Tk2SwfODem*RlGiB0 zWw+KZ+d!vWB(y{Z){Gy*A$Xd@#d&-NJF2j}vKd@B`?h>Gj( z77E*7K|uI72wU8)b+!2c*KnKsbH~DR{}(o0vv~#%_!ZS%^jXf9{>W z+3>>bZ%ton47!U9gj8!xswH|m<;-pyc8+UTzG& zw|z5~0wf^p&bUS5vV>ZxQtxhF*ucZD1c#y!(0ZYB7+KGhs~XGY*8CBv){97axcyVp zO9@Ws7Lw)ieBF1H2o6IZ5&e#2MkfMSQnA=&W057`h?hz}Wqh3%P{cd~ly{j>FP>JH zBYoQ2)#?IAVKcx!S*beyn?iZEe>A?>W69HBs8l8*SgPs|dTXgQ>eEY0Pp{Ka^}OEB zpF5&Y{v>6)iBZ8d?KS}=mV`%k<|U%M0x~B5eQLs+N~6#~J=hyId}ap4>pnf6mm{u?_C~b$|vChMVt`Q)6*%#q2{yK8r)fU zQ;6cuTU&V&VOd(p;U&({DFnzdzT4`UtXwO5KxS*~;|*)Beyan5EHHxr9j0JN!0!_P z4ck_rUg6g2b&)aW?V+Ng!s>kq7+bP}4HRjNhcE!+&p@i$O1qeij;DCjISp~%2pQ78 zLnbInD3xF0!0?nh8+Uj{o8^)BFHtf$EIQ#sCJI>`$vU1Hfuukt$mk}hZE6_l6^-?{ z*$p|T&CbKUJ%+|*4mQ2&hE^L}z=8>|;G%Rd#5i5PB=o0SoqX01j%%BU0bWx zh&TdDguAg6oL){DrPCVkTx-)fYHz_HQBD!;tS2r&hERK zY!b`yYYq<`ciQ;ib_mgtHJXGQzgRB0QM@DD& zt|1do*mtSFA25O%mgmKJm+NZ*8KIzF$t^4W;IcA?m2Ok+hr~q$QBuPG&Ku{kq|)h` z(tk#cR$wS@`(gUEC}EU-S65e=_VBHpnL-A9PTP1>-Oo^XjEOWhf^w2%w26{Sc)*Uo zW{u&1>#@xJ@hS`Wh;^g%L^0;C}h2m{g=wv%) zfD|TO$tE>dF(5qWpvU7m@2bs>;umKa*k40+dm}r0Pe(A!hCL;w7kr9;nBQ1dJMUk{ zj|dIZt#sEzVB|U z(z!~qY`^|~aAj)0(Z})hHH(Rs9r`ivf}fWzQxPVBR(4|X%yPD#36{ToThiySjucAA{Y3J(r?1pv91f1y>H%$iJdq5?=0?M!WbVGk5M;p`U(fVI zb&$pr-a^<2WEsSgN|=D?o)Ls^;O@)lXk>+)-F%)DX<;-RmMrPd@B9H~9v(bP_V-dc zUih8Ax$;9L1wFhxlVtzn@)oNX?;lUqYWCr@IJgTSjkl%fi&VFXaL)4+Aw6+FD*8(# z>{2byUCQ7*8&zv+CWWUY(Ccy| zYOXrp5&8|QBIKh~&@bDeqFVBUra{9Ahb03}Q1BIk2!$eiVr82kWBkkIyhZu*-*qzh zeYDOW3WK#fg|n^YOyuO`+C@ysIqp}!gaTdfSkerdf8jMXaU*dmpfQ4j&j%_rYhJj5 z5d%ehMZLWER&9)ptwNYS1F)WSWNM9e60epR)=Pf`Mnpv546*zvdC1|lA>ImA5(SQ; z5t4Z`;nO|p_}>~tJ%O%zbZa2AjnsU?Ar)o0Nt}A(W38PDo{eq!nJa`dPy~LLRkaw%n%nbg}%Xr*Zg;Hf27Yy;LRWBpv}F5H&9-5R<2W>{ z=2R@Q@699ydekYi&ffN>Oyl|un~sy*VDxE=t@ofSKRcffS3RO9Hq*=mFX#*Z`ue(1 zgG)jZht068#G|M zuU=RXBRWYsv>EQpuZav@f!9 zGX1F2Tg-m|tP*!3aoT8`-Xs>wsG8J#SuQFmF&VCGi!6t_qfcfV97-tcYlmU7%S@6D zFjUSHftpAc40>ovzq`Z7GrHZ6X!U(y_j)>?8pseLYSbXrET}2aj^c7~a*`Ai6H9S# zeibZ(h&L5t?-KP`ic8doAV=4`qij7jFR#sM8-6kkE%_!^b zF^p}9xU*p1kHUi4;Sa>DWKgPW&oQVek3^t z-8~;4BZ|4#dgx@I?lMF3ZK`m3F|mW^>GMXmwi2rr1Dk0g!N>^dyb8d&w8fr1qh4ztv5Cb#fMY+HA-x~V{T)Z2lL)MoZygD$>U zVqX6X@Vjz#R{HSP+g5%Z0AZ~g38$$N2y9JuDQ(=l31?m0fQB$NENL9(H1XZDIci?2 z7ydXfzx!4G?~z}}iybV)f}XcvM8m;Bmxe_vi&!ck5*s?aGT)?V2vgc#4wXf-2o#f_eQq@po3%>jd-D7A41Klba;PBrca-po|KPS!Bh4U9(1O=Jj_FkN3s z7|l$r=O=d9OxT23#EPv`>pwpgQc1bwdpVjHs?8!kKi&=_xq^3wqK+v0JG1J| z#*8M?Dlu{DqbbPIArf8G*)p?ff9vdMCN~SqD8k(>t4iMu{_9^C9p|dFiQ{<(SF*Ml}R~J+UDej znhxRZiA3%7-DD^^7e`Z!6r{48L`8D{wW}&So@WeFks~~ymgr)4&TX2M`><=+s1^I%Q|M?aO zlyF727*=xDOu%eTb}wv~1} zxUCY0snf-?KfX6?EQyeqprtB5#BJxj2u7z~M69EAMeod6ct~NngsNN&Z8<^nl zOd?ctYX@&ZDC`(n)TFV=s~&97Oq5$@MMe#fz#33gNpbBQ3h|! znF|p#7&4<+&-BtlwN7` zMvKDmHD8?z*&p5yt;QK>e;~NIp2h5*%$rJgK5}Fu4^;okq}wP`9Qm)FK4_8szBa$Q z^3j~NYPvvj+;*I$tq&v(3CWCnYDed|rSPYF#L_VfE49$A}98JxXtt?zAY>am1y{+_Rc3$EJn3-dV5#Wtt!kD z+qtpM2DFB=6suq=75DR@+MM_r!q^Nge)<8*NxuZ|Bw!+ zXaC-hVQACZP?%Coq;o>EDdWzT2MKPOD^myx5`D9nG$Wd`nkd^@2-HTv)qz;Og}}Na1~R)Nsbf}QzTqTHs%f{R%=woqO!({1 zw`<>al1|#%0>ApAR%kMZL?C=rs-6bm&97UKXDckwvLuWpGZdzZbN@|I-iYNM5YK?H zp}({}OsRzhK$+DM%Ocy}>QA2z=Y#QdV{hQM|F*(T7b|Y9v6r(@`Qtv|P-%a6OFS^<}{p@U^x@e7@xS zLUjcdXfE4&t_Y#pFsi=@!BpE0bCGhXazlpQpe|b~UC@S)<_4iG)l#{E&r3Ni=<()8 zd?6W+o=bMNZ;lEnucGjmF((z}&Ovk4Cy%d7t_IWdN6d&~j>%2|Rizgp>E4!kt&N%d zWObzj2FA}NWOohKcm-~GMjX@qvO2YoMr*YiDObdJ6ouqbi=V;i-vnf0d#bbft(~x_ zi}3`_0jpw_woh`zc_ur7ybE?o10YN~)e;V=XuPp@e&>&2luCn!fDC=)ZzYP7eSu?e z*+xMoAQeh-hOXrr18R^Xa+9qf1;6c99TE-Xp0p!je!o42epv-fq41_uQ)UKKsi>+d z&(9&U7=+Kn3xzgi$`P)Ggj>9@l+NEB0fNR`aPFM zTN_`#6V>7iGn@UN-Jz&~ac9A)(CpF0T_e12ZWuO>tSr;Am?MtFYvJ=h?6-taYnz+R zS7#GR*>5p4YKoT^=a!d8b|vq4GG`NMKUv7WTGK=mh&$QeUoGoaB%U*fUr!0Hv>3i{ zqVY<-M327WEIYh5N{|y(>eK}v98#UYZtyxEQ9Lf$SvNN|+e9q6xe-Q4=$MB+UZ6Wu z0i2IKEE_wJm<$GKj3!zmDo=w16yJ&>_Kfoa!xOT4rWl#53q(I&E5~K!A=?pOYzB=%OSw0} z&rrFwH_nPwRoOFa0)nWoSX}d?7>K$OOS= zqe_D`okb>^eSu%t<_GkxY#_bhM@haWvteL=NC=^yaAIO&AQW`A47_qP7Ly8A6i(Sv z=?^mi8wbbW>qCWB=zc_DOc62n?N2N%-zK%-c%$)KFJ|Smg@~@7a zvIld&IGUQ8PGk!vQ5K)9h&Ck6IZU>r7_1Orh~-lv@VVH=U2e21(K1++`Kb7AW~I{_ z#_GSq@IsnlU-w5C?H?EpYb_F+{P6EQXz7VTUyaoWYwKCTlMHu9nx z1)r;TiU_>D*MwR^6{*^m!j+Vg659O%N3mzqUa4ByS%FSbwOEmo&l9qKJ=UAfva|)^ z!Z;dLI^W5^)$3E0DdhvIha}>=ZW>3iqHefOh!An;MVQ1wAI0BocDQV@-l3bv)>l@~ z+2On%&P8_-XTOO}R;>ybm3SrN>{zFD17V;lKZf_u%@F>!%BM;@?U_yv|4Avp4JEH;RhR%35QG(IJu_KSOhPTOrHO8|2VB!O+BXu_Enrevx zB^&BtMQOnCS~1d@JL=ui>nkTw*DG>w71*6ln=G%^;o| zt69AlrTKwADe`A#7wEh36$h=DoD+KnSHsW&Z9CL4jPG~7b!ZMtOL6wlI&nS=^~Fxa z#|xBD;vocFuCfr3`}_M*k0OW&t0rJ0{QEVydc#X4^Ar1-69Iu`Heb~pgDVAgc=!dq zagyX^ftIXX2Og##J{;=P7ZO0R5F|Hl*R|j3qC&emX|PzZ#d?;S1GiXil@mlvD2`+s z!c3#sD7UFdBAU&L+ra?R86pMHX<{xl#C!zK!S0g%J9n1bWh` z)Cq^7D3Ij$3u6Oohf>Ev`IwdD1Mu+i$vvb=<hl- z`b0xNI{c5+u7x)7yb~6q(tjOpzf{kDb6C>>Lsh80*D-hyEPjmvyI&T#X2ynF2vVhY zc>ELkO2p@uRBV<~JEqSpt*D6>5RhqPAlU_bAY9N4FfLOCV`Tbtkh-$F>$Y6@mzB}dZ_tnG#9@!8<|W%9xz+?)|044LL39zpk2rZPpqL{R zU4Wmch61-C+y1{p23QE+I)iMdu^m`_i({o9;ZCD+xc@X3IoU+&Bc zEfnE2$VhxCd3nDQ@w*OG3vx7vSfiB=Oy!%|cz1feBu29C6Xe?Ja)h-U>HN6$a0nUJ z`de5C4IwVI4K}h(>y3>Z$b|a}f&)>~d#$-sCn^Tv3&yC3+kRlHf3(v8aq4*wp zjzSH^!hsTl0WaR|^HS@+?6y#@|EAlGLLXl1sta6eJ`2>Waaz`qb&^B$x<97@1HPjj zo~V^wv!D{KASLx@q~~ky!SpNTiTDz(^bp{1+NcmDjJ~a*r!wa&C*icZydWk!EoOH> zbc^!u@`QqJ3h3_9u+cmapi}fs~yJ{`v&o zkkE}Xou>Y37Y)q!j3#n#K1*s$@?BPjFjMKrLYEVjg|P$Z7;Jb*uFIH^7b`FW^6%Lj zCiL=NubRNZ#HeKHp_URd2^5J4W=}bSmMdkv1%+1T>j!ZySDO&EZ0Y*`F{LeLMq!dvv=8vb z@w(UG50h$!N!xq=4TM!+^hWH3DV5l2pPo3RA>JR)Qq)^EyOX?4$oOEm9THZafwzYr z2o~0*_F0+2AW1Yr-cGcKswSGDEDiYJoh`(F+iJ(?L$MrdVxZ?&?2lKJz`{GFPQ@Q3 zdVDRAxYSonnvQ{Paa=_2kp}XF56wUoD*1#|wF{jTtnnj~Ds*`96P03z+d=e&DtQYQ zP|Zpm1)l3E=V0AlKH+FQmx+XgL7*!%L8WdF6b3?eyucO^-BInY7JBIqVk+aTMwBYZ z7Dync{%Kv;q+h|vwt8~QP8I+YVx*H)RMfICUfLIiR*uJQjIXn{x(Bg+_VG5Dld)_V z7_txr|6)Vm8s@tKj3JtAfAw}1B#J^RedS>#3GSxAYdX~$5OyH&)U7~zLnD;B@ItAZ zokzl`TVQ_W-J!DF>L&W?%*sM#C&};yp#Ap<Vv%L73}=)1cupYP`D-ovlR*9Mkkh|#j}$`k5g~CodVOMw=%zC+lfqJNA&2u%@~g6x-C+{Ab$kMa01|54Lo zhP~8I=x{A6EgcxK$F$a267PYE^>Zzc>vARdH89-UnL(vdV-OWrPQu%SsXe&9xk(H$ z?<|vf{*WyQ#JY{3D2LttNqFtowI7w4VB)4?6=7x7~gIuIl*xU zlM;cgobWd{H<2xE^hVW%r4>;iOqy-I6@myud*0=nyeetixPD1j&inp~Kt~xVXBq`Wyo#r1d4<9v>t3G8nNVCOX<=_oN_%3GS&pDOljz z4`ShvpeH2n>GK2XY%G9?@mU)g3<*A=FWWmiv8E%a1#$;+KADB+@Bk1F`AUmTQ08(W ztH+E;xQh!%37D#--X?Xb72g>_l|~Ur?1c)vR7a)NDS~&*@A23{&RAV3>yn_P43V%4 z&fy8H3dnTICc#F5*mePulcztIcQ*z5$~3z0w+`R^7m9lz{?$tTD%{ajmPldf!wZ+3 zDLPDlU~`o&8T&gjMD4v+4DlOf>(;@3F~C)wPt@wN=B<{JF0hFE_48Cm#51^K1Vdk6 z|Ne3@NNqHRVCk?IA%QxYRiVxnL_W9~Jq)yv-Uk&<2NA7>kpkt8&rXcD{kvOFaM z<%rMyRK~{5S`tEaV@)Z^}NJfuCYVANt~a=q-`B#j`_+>r>@)HkzF*r(kL zK_VvG93DWqWEEfO0{`vHYBCw1=Lw>ixFXr@t=W2qcW#mH)aWxJBx#g}#YRS^R&}}< zQf24NVYTpIyO_LaNEV_*>sco$IaH1x>MbLVSC@6@HOB(tj$;|YKeYe;`}Ysw1@XV& U6gFr`(E2}F$sZEcVn%`g2S>NAR{#J2 diff --git a/react/admin-panel/src/AdminDashboard.jsx b/react/admin-panel/src/AdminDashboard.jsx index 49f5533..fb91d08 100644 --- a/react/admin-panel/src/AdminDashboard.jsx +++ b/react/admin-panel/src/AdminDashboard.jsx @@ -1,7 +1,7 @@ import React, {lazy, Suspense, useCallback, useState} from "react"; import {BrowserRouter, Route, Routes} from "react-router-dom"; +import Dialog from "shared/elements/dialog"; import Sidebar from "./elements/sidebar"; -import Dialog from "./elements/dialog"; import Footer from "./elements/footer"; import {useContext, useEffect} from "react"; import {LocaleContext} from "shared/locale"; diff --git a/react/admin-panel/src/elements/dialog.jsx b/react/admin-panel/src/elements/dialog.jsx deleted file mode 100644 index 0bc909e..0000000 --- a/react/admin-panel/src/elements/dialog.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; -import clsx from "clsx"; - -export default function Dialog(props) { - - const show = props.show; - const classes = ["modal", "fade"]; - const style = { paddingRight: "12px", display: (show ? "block" : "none") }; - const onClose = props.onClose || function() { }; - const onOption = props.onOption || function() { }; - const options = props.options || ["Close"]; - - let buttons = []; - for (let name of options) { - let type = "default"; - if (name === "Yes") type = "warning"; - else if(name === "No") type = "danger"; - - buttons.push( - - ) - } - - return ( -

onClose()}> -
e.stopPropagation()}> -
-
-

{props.title}

- -
-
-

{props.message}

-
-
- { buttons } -
-
-
-
- ); -} \ No newline at end of file diff --git a/react/admin-panel/src/views/access-control-list.js b/react/admin-panel/src/views/access-control-list.js index 06eef97..9826c1c 100644 --- a/react/admin-panel/src/views/access-control-list.js +++ b/react/admin-panel/src/views/access-control-list.js @@ -169,8 +169,7 @@ export default function AccessControlList(props) { { type: "label", value: L("permissions.description") + ":" }, { type: "text", name: "description", value: permission.description, maxLength: 128 } ], - onOption: (option, inputData) => option === 0 && onUpdatePermission(inputData, permission.groups) - })} > + onOption: (option, inputData) => option === 0 ? onUpdatePermission(inputData, permission.groups) : true })} > option === 0 && onDeletePermission(permission.method) + onOption: (option) => option === 0 ? onDeletePermission(permission.method) : true })} > @@ -253,7 +252,7 @@ export default function AccessControlList(props) { { type: "label", value: L("permissions.description") + ":" }, { type: "text", name: "description", maxLength: 128, placeholder: L("permissions.description") } ], - onOption: (option, inputData) => option === 0 && onUpdatePermission(inputData, []) + onOption: (option, inputData) => option === 0 ? onUpdatePermission(inputData, []) : true })} > {L("general.add")} diff --git a/react/admin-panel/src/views/group/group-edit.js b/react/admin-panel/src/views/group/group-edit.js index 0327a7e..0d685ee 100644 --- a/react/admin-panel/src/views/group/group-edit.js +++ b/react/admin-panel/src/views/group/group-edit.js @@ -3,11 +3,12 @@ import {Link, useNavigate, useParams} from "react-router-dom"; import {LocaleContext} from "shared/locale"; import SearchField from "shared/elements/search-field"; import React from "react"; -import {ControlsColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table"; +import {sprintf} from "sprintf-js"; +import {DataTable, ControlsColumn, NumericColumn, StringColumn} from "shared/elements/data-table"; import EditIcon from "@mui/icons-material/Edit"; import usePagination from "shared/hooks/pagination"; import Dialog from "shared/elements/dialog"; -import {FormControl, FormGroup, FormLabel, styled, TextField, Button, CircularProgress} from "@mui/material"; +import {FormControl, FormGroup, FormLabel, TextField, Button, CircularProgress, Box} from "@mui/material"; import {Add, Delete, KeyboardArrowLeft, Save} from "@mui/icons-material"; import {MuiColorInput} from "mui-color-input"; import ButtonBar from "../../elements/button-bar"; @@ -27,6 +28,7 @@ export default function EditGroupView(props) { const isNewGroup = groupId === "new"; const pagination = usePagination(); const api = props.api; + const showDialog = props.showDialog; // data const [fetchGroup, setFetchGroup] = useState(!isNewGroup); @@ -41,7 +43,7 @@ export default function EditGroupView(props) { useEffect(() => { requestModules(props.api, ["general", "account"], currentLocale).then(data => { if (!data.success) { - props.showDialog(data.msg, "Error fetching localization"); + showDialog(data.msg, "Error fetching localization"); } }); }, [currentLocale]); @@ -51,7 +53,7 @@ export default function EditGroupView(props) { setFetchGroup(false); api.getGroup(groupId).then(res => { if (!res.success) { - props.showDialog(res.msg, "Error fetching group"); + showDialog(res.msg, "Error fetching group"); navigate("/admin/groups"); } else { setGroup(res.group); @@ -66,11 +68,11 @@ export default function EditGroupView(props) { setMembers(res.users); pagination.update(res.pagination); } else { - props.showDialog(res.msg, L("account.fetch_group_members_error")); + showDialog(res.msg, L("account.fetch_group_members_error")); return null; } }); - }, [groupId, api, pagination]); + }, [api, showDialog, pagination, groupId]); const onRemoveMember = useCallback(userId => { api.removeGroupMember(groupId, userId).then(data => { @@ -78,16 +80,16 @@ export default function EditGroupView(props) { let newMembers = members.filter(u => u.id !== userId); setMembers(newMembers); } else { - props.showDialog(data.msg, L("account.remove_group_member_error")); + showDialog(data.msg, L("account.remove_group_member_error")); } }); - }, [api, groupId, members]); + }, [api, showDialog, groupId, members]); const onAddMember = useCallback(() => { if (selectedUser) { api.addGroupMember(groupId, selectedUser.id).then(data => { if (!data.success) { - props.showDialog(data.msg, L("account.add_group_member_error")); + showDialog(data.msg, L("account.add_group_member_error")); } else { let newMembers = [...members]; newMembers.push(selectedUser); @@ -96,7 +98,7 @@ export default function EditGroupView(props) { setSelectedUser(null); }); } - }, [api, groupId, selectedUser]) + }, [api, showDialog, groupId, selectedUser, members]) const onSave = useCallback(() => { setSaving(true); @@ -104,7 +106,7 @@ export default function EditGroupView(props) { api.createGroup(group.name, group.color).then(data => { setSaving(false); if (!data.success) { - props.showDialog(data.msg, L("account.create_group_error")); + showDialog(data.msg, L("account.create_group_error")); } else { navigate(`/admin/group/${data.id}`) } @@ -113,31 +115,31 @@ export default function EditGroupView(props) { api.updateGroup(groupId, group.name, group.color).then(data => { setSaving(false); if (!data.success) { - props.showDialog(data.msg, L("account.update_group_error")); + showDialog(data.msg, L("account.update_group_error")); } }); } - }, [api, groupId, isNewGroup, group]); + }, [api, showDialog, groupId, isNewGroup, group]); const onSearchUser = useCallback((async (query) => { let data = await api.searchUser(query); if (!data.success) { - props.showDialog(data.msg, L("account.search_users_error")); + showDialog(data.msg, L("account.search_users_error")); return []; } return data.users; - }), [api]); + }), [api, showDialog]); const onDeleteGroup = useCallback(() => { api.deleteGroup(groupId).then(data => { if (!data.success) { - props.showDialog(data.msg, L("account.delete_group_error")); + showDialog(data.msg, L("account.delete_group_error")); } else { navigate("/admin/groups"); } }); - }, [api, groupId]); + }, [api, showDialog, groupId]); const onOpenMemberDialog = useCallback(() => { setDialogData({ @@ -146,30 +148,24 @@ export default function EditGroupView(props) { message: L("account.add_group_member_text"), inputs: [ { - type: "custom", name: "search", element: SearchField, + type: "custom", name: "search", size: "small", key: "search", + element: SearchField, onSearch: v => onSearchUser(v), onSelect: u => setSelectedUser(u), - displayText: u => u.fullName || u.name + getOptionLabel: u => u.fullName || u.name } ], - onOption: (option) => option === 0 ? onAddMember() : setSelectedUser(null) + onOption: (option) => option === 0 ? + onAddMember() : + setSelectedUser(null) }); - }, []); + }, [onAddMember, onSearchUser, setSelectedUser, setDialogData]); useEffect(() => { onFetchGroup(); }, []); - const complementaryColor = (color) => { - if (color.startsWith("#")) { - color = color.substring(1); - } - - let numericValue = parseInt(color, 16); - return "#" + (0xFFFFFF - numericValue).toString(16); - } - if (group === null) { return } @@ -243,7 +239,7 @@ export default function EditGroupView(props) { open: true, title: L("account.delete_group_title"), message: L("account.delete_group_text"), - onOption: option => option === 0 && onDeleteGroup() + onOption: option => option === 0 ? onDeleteGroup() : true })}> {L("general.delete")} @@ -252,7 +248,7 @@ export default function EditGroupView(props) { {!isNewGroup && api.hasPermission("groups/getMembers") ? -
+

{L("account.members")}

option === 0 && onRemoveMember(entry.id) + onOption: (option) => option === 0 ? onRemoveMember(entry.id) : true }) } ]), @@ -295,7 +291,7 @@ export default function EditGroupView(props) { onClick: onOpenMemberDialog }]} /> -
+ : <> } diff --git a/react/admin-panel/src/views/profile/mfa-fido.js b/react/admin-panel/src/views/profile/mfa-fido.js new file mode 100644 index 0000000..8267dc6 --- /dev/null +++ b/react/admin-panel/src/views/profile/mfa-fido.js @@ -0,0 +1,26 @@ +import {Box, Paper} from "@mui/material"; +import {LocaleContext} from "shared/locale"; +import {useCallback, useContext} from "react"; + +export default function MfaFido(props) { + + const {api, showDialog, setDialogData, ...other} = props; + const {translate: L} = useContext(LocaleContext); + + const openDialog = useCallback(() => { + if (api.hasPermission("tfa/registerKey")) { + + } + }, [api, showDialog]); + + const disabledStyle = { + background: "gray", + cursor: "not-allowed" + } + + return +
{"[Nitro
+
{L("account.2fa_type_fido")}
+
; +} \ No newline at end of file diff --git a/react/admin-panel/src/views/profile/mfa-totp.js b/react/admin-panel/src/views/profile/mfa-totp.js new file mode 100644 index 0000000..e8f6848 --- /dev/null +++ b/react/admin-panel/src/views/profile/mfa-totp.js @@ -0,0 +1,55 @@ +import {Box, Paper} from "@mui/material"; +import {useCallback, useContext} from "react"; +import {LocaleContext} from "shared/locale"; + +export default function MfaTotp(props) { + + const {setDialogData, api, showDialog, ...other} = props; + const {translate: L} = useContext(LocaleContext); + + const onConfirmTOTP = useCallback((code) => { + api.confirmTOTP(code).then(data => { + if (!data.success) { + showDialog(data.msg, L("account.confirm_totp_error")); + } else { + setDialogData({show: false}); + showDialog(L("account.confirm_totp_success"), L("general.success")); + } + }); + return false; + }, [api, showDialog]); + + const openDialog = useCallback(() => { + if (api.hasPermission("tfa/generateQR")) { + setDialogData({ + show: true, + title: L("Register a 2FA-Device"), + message: L("Scan the QR-Code with a device you want to use for Two-Factor-Authentication (2FA). " + + "On Android, you can use the Google Authenticator."), + inputs: [ + { + type: "custom", element: Box, textAlign: "center", children: + {"[QR-Code]"}/ + }, + { + type: "number", placeholder: L("account.6_digit_code"), + inputProps: { maxLength: 6 }, name: "code", + sx: { "& input": { textAlign: "center", fontFamily: "monospace" } }, + } + ], + onOption: (option, data) => option === 0 ? onConfirmTOTP(data.code) : true + }) + } + }, [api, onConfirmTOTP]); + + const disabledStyle = { + background: "gray", + cursor: "not-allowed" + } + + return +
{"[Google
+
{L("account.2fa_type_totp")}
+
+} \ No newline at end of file diff --git a/react/admin-panel/src/views/profile/profile.js b/react/admin-panel/src/views/profile/profile.js index 1d44daa..15b588d 100644 --- a/react/admin-panel/src/views/profile/profile.js +++ b/react/admin-panel/src/views/profile/profile.js @@ -7,7 +7,7 @@ import { CircularProgress, FormControl, FormGroup, - FormLabel, styled, + FormLabel, Paper, styled, TextField } from "@mui/material"; import { @@ -23,6 +23,9 @@ import { } from "@mui/icons-material"; import CollapseBox from "./collapse-box"; import ButtonBar from "../../elements/button-bar"; +import MfaTotp from "./mfa-totp"; +import MfaFido from "./mfa-fido"; +import Dialog from "shared/elements/dialog"; const GpgKeyField = styled(TextField)((props) => ({ "& > div": { @@ -46,6 +49,29 @@ const ProfileFormGroup = styled(FormGroup)((props) => ({ marginBottom: props.theme.spacing(2) })); +const MFAOptions = styled(Box)((props) => ({ + "& > div": { + borderColor: props.theme.palette.divider, + borderStyle: "solid", + borderWidth: 1, + borderRadius: 5, + maxWidth: 150, + cursor: "pointer", + textAlign: "center", + display: "inline-grid", + gridTemplateRows: "130px 50px", + alignItems: "center", + padding: props.theme.spacing(1), + marginRight: props.theme.spacing(1), + "&:hover": { + backgroundColor: "lightgray", + }, + "& img": { + width: "100%", + }, + } +})); + const VisuallyHiddenInput = styled('input')({ clip: 'rect(0 0 0 0)', clipPath: 'inset(50%)', @@ -78,12 +104,15 @@ export default function ProfileView(props) { const [changePassword, setChangePassword] = useState({ old: "", new: "", confirm: "" }); const [gpgKey, setGpgKey] = useState(""); const [gpgKeyPassword, setGpgKeyPassword] = useState(""); + const [mfaPassword, set2FAPassword] = useState(""); // ui const [openedTab, setOpenedTab] = useState(null); const [isSaving, setSaving] = useState(false); const [isGpgKeyUploading, setGpgKeyUploading] = useState(false); const [isGpgKeyRemoving, setGpgKeyRemoving] = useState(false); + const [is2FARemoving, set2FARemoving] = useState(false); + const [dialogData, setDialogData] = useState({show: false}); const onUpdateProfile = useCallback(() => { @@ -147,7 +176,22 @@ export default function ProfileView(props) { } }); } - }, [api, showDialog, isGpgKeyRemoving, gpgKeyPassword]); + }, [api, showDialog, isGpgKeyRemoving, gpgKeyPassword, profile]); + + const onRemove2FA = useCallback(() => { + if (!is2FARemoving) { + set2FARemoving(true); + api.remove2FA(mfaPassword).then(data => { + set2FARemoving(false); + set2FAPassword(""); + if (!data.success) { + showDialog(data.msg, L("account.remove_2fa_error")); + } else { + setProfile({...profile, twoFactorToken: null}); + } + }); + } + }, [api, showDialog, is2FARemoving, mfaPassword, profile]); const getFileContents = useCallback((file, callback) => { let reader = new FileReader(); @@ -167,6 +211,8 @@ export default function ProfileView(props) { reader.readAsText(file); }, [showDialog]); + console.log("SELECTED USER:", profile.twoFactorToken); + return <>
@@ -315,7 +361,37 @@ export default function ProfileView(props) { setOpenedTab(openedTab === "2fa" ? "" : "2fa")} icon={}> - test + {profile.twoFactorToken && profile.twoFactorToken.confirmed ? + + + { profile.twoFactorToken.confirmed ? + : + + } + {L("account.2fa_type_" + profile.twoFactorToken.type)} + + + {L("account.password")} + + set2FAPassword(e.target.value)} + placeholder={L("account.password")} + /> + + + + : + + + + + } @@ -327,5 +403,13 @@ export default function ProfileView(props) {
+ + setDialogData({show: false})} + options={[L("general.ok"), L("general.cancel")]} + onOption={dialogData.onOption} /> } \ No newline at end of file diff --git a/react/admin-panel/src/views/route/route-list.js b/react/admin-panel/src/views/route/route-list.js index adf6236..2322557 100644 --- a/react/admin-panel/src/views/route/route-list.js +++ b/react/admin-panel/src/views/route/route-list.js @@ -201,7 +201,7 @@ export default function RouteListView(props) { { type: "text", name: "pattern", value: route.pattern, disabled: true} ], options: [L("general.ok"), L("general.cancel")], - onOption: btn => btn === 0 && onDeleteRoute(route.id) + onOption: btn => btn === 0 ? onDeleteRoute(route.id) : true })}> diff --git a/react/package.json b/react/package.json index ab8b0a4..64d7a68 100644 --- a/react/package.json +++ b/react/package.json @@ -21,6 +21,8 @@ "devDependencies": { "@babel/core": "^7.20.5", "@babel/plugin-transform-react-jsx": "^7.19.0", + "@eslint/js": "^9.0.0", + "eslint-plugin-react": "^7.34.1", "customize-cra": "^1.0.0", "parcel": "^2.8.0", "react-app-rewired": "^2.2.1", diff --git a/react/shared/elements/dialog.jsx b/react/shared/elements/dialog.jsx index a7204d2..001a69a 100644 --- a/react/shared/elements/dialog.jsx +++ b/react/shared/elements/dialog.jsx @@ -12,7 +12,7 @@ import { export default function Dialog(props) { - const show = props.show; + const show = !!props.show; const onClose = props.onClose || function() { }; const onOption = props.onOption || function() { }; const options = props.options || ["Close"]; @@ -36,7 +36,13 @@ export default function Dialog(props) { for (const [index, name] of options.entries()) { buttons.push( ) @@ -54,16 +60,21 @@ export default function Dialog(props) { inputElements.push({input.value}); break; case 'text': - case 'password': + case 'number': + case 'password': { + let onChange = (input.type === "number") ? + e => setInputData({ ...inputData, [input.name]: e.target.value.replace(/[^0-9,.]/, '') }) : + e => setInputData({ ...inputData, [input.name]: e.target.value }); + inputElements.push( setInputData({ ...inputData, [input.name]: e.target.value })} + onChange={onChange} />) - break; + } break; case 'list': delete inputProps.items; let listItems = input.items.map((item, index) => {item}); diff --git a/react/shared/elements/search-field.js b/react/shared/elements/search-field.js index 3c81533..c2cd317 100644 --- a/react/shared/elements/search-field.js +++ b/react/shared/elements/search-field.js @@ -4,12 +4,11 @@ import useAsyncSearch from "../hooks/async-search"; export default function SearchField(props) { - const { onSearch, displayText, onSelect, ...other } = props; + const { onSearch, onSelect, ...other } = props; const [searchString, setSearchString, results] = useAsyncSearch(props.onSearch, 3); return displayText(r)} options={Object.values(results ?? {})} onChange={(e, n) => onSelect(n)} renderInput={(params) => ( diff --git a/react/shared/views/login.jsx b/react/shared/views/login.jsx index fa58581..4a3b985 100644 --- a/react/shared/views/login.jsx +++ b/react/shared/views/login.jsx @@ -171,7 +171,7 @@ export default function LoginForm(props) { autoComplete={"code"} required fullWidth autoFocus value={tfaCode} onChange={(e) => set2FACode(e.target.value)} - /> + onKeyDown={e => e.key === "Enter" && onSubmit2FA()} /> { tfaToken.error ? {tfaToken.error} : <> } diff --git a/react/yarn.lock b/react/yarn.lock index 9faed0f..5bcf891 100644 --- a/react/yarn.lock +++ b/react/yarn.lock @@ -1417,6 +1417,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@eslint/js@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.0.0.tgz#1a9e4b4c96d8c7886e0110ed310a0135144a1691" + integrity sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ== + "@floating-ui/core@^1.0.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1" @@ -5386,7 +5391,7 @@ eslint-plugin-react-hooks@^4.3.0: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== -eslint-plugin-react@^7.27.1: +eslint-plugin-react@^7.27.1, eslint-plugin-react@^7.34.1: version "7.34.1" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz#6806b70c97796f5bbfb235a5d3379ece5f4da997" integrity sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw== diff --git a/test/TimeBasedTwoFactorToken.test.php b/test/TimeBasedTwoFactorToken.test.php index a137530..deac727 100644 --- a/test/TimeBasedTwoFactorToken.test.php +++ b/test/TimeBasedTwoFactorToken.test.php @@ -1,7 +1,6 @@