Skip to content

Commit f2666de

Browse files
osiastedianTed Ian Osias
andauthored
Fix: enforce 2FA before persisting session; refactor Login.jsx for clarity (bug-001-two-factor-does-not-prevent-login) (#158)
* Fix: enforce 2FA before persisting session; refactor Login.jsx for clarity (bug-001-two-factor-does-not-prevent-login) * Chore: fix CI lint by removing unused useMemo import in Login.jsx --------- Co-authored-by: Ted Ian Osias <[email protected]>
1 parent 4cc147e commit f2666de

File tree

3 files changed

+113
-105
lines changed

3 files changed

+113
-105
lines changed

src/context/user-context.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export function UserProvider(props) {
116116
firebase
117117
.loginWithEmailAndPassword(loginData)
118118
.then(({ user }) => {
119-
saveUserData(user);
119+
// Do not persist user here. Persist only after passing 2FA (if enabled).
120120
resolve(user);
121121
})
122122
.catch((err) => {

src/pages/Login.jsx

Lines changed: 90 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from "react";
1+
import React, { useCallback, useEffect, useState } from "react";
22
import { MetaTags } from "react-meta-tags";
33
import { withTranslation } from "react-i18next";
44

@@ -26,70 +26,103 @@ import { deleteUserData } from "../utils/auth-token";
2626
function Login({ t }) {
2727
const history = useHistory();
2828
const { loginUser, loginWithPhoneNumber, setUserDataLogin } = useUser();
29-
const [openSMS2Fa, setOpenSMS2Fa] = useState(false);
30-
const [openGAuthFa, setOpenGAuth2Fa] = useState(false);
29+
// UI state
3130
const [submitting, setSubmitting] = useState(false);
32-
const [userSignInSms, setUserSignInSms] = useState({});
33-
const [userSignInGAuth, setUserSignInGAuth] = useState("");
31+
const [active2FAMethod, setActive2FAMethod] = useState(null); // 'sms' | 'gauth' | null
32+
33+
// 2FA session state (in-memory, only during current login flow)
34+
const [pendingUser, setPendingUser] = useState(null); // firebase user from base login
35+
const [pendingPassword, setPendingPassword] = useState(""); // used to create seed after 2FA
36+
const [smsConfirmation, setSmsConfirmation] = useState(null); // result from signInWithPhoneNumber
37+
const [gauthContext, setGauthContext] = useState(null); // { ...user, secret }
3438

3539
useEffect(() => {
3640
return () => {
3741
setSubmitting(false);
38-
setOpenGAuth2Fa(false);
39-
setOpenSMS2Fa(false);
42+
setActive2FAMethod(null);
4043
};
4144
}, []);
4245

46+
// Alert helpers
47+
const showLoading = useCallback((title) => {
48+
swal.fire({
49+
title: title || "Processing",
50+
showConfirmButton: false,
51+
willOpen: () => swal.showLoading(),
52+
});
53+
}, []);
54+
55+
const showSuccess = useCallback(async (message = "Logged in") => {
56+
await swal.fire({
57+
icon: "success",
58+
title: message,
59+
timer: 2000,
60+
showConfirmButton: false,
61+
});
62+
}, []);
63+
64+
const showError = useCallback((message = "Error") => {
65+
swal.fire({ icon: "error", title: "Error", text: message });
66+
}, []);
67+
68+
// Finalize login: persist and navigate
69+
const finalizeLogin = useCallback(
70+
async (user) => {
71+
if (pendingPassword) {
72+
createSeed(pendingPassword);
73+
}
74+
setUserDataLogin(user);
75+
history.push("/governance");
76+
},
77+
[history, pendingPassword, setUserDataLogin]
78+
);
79+
4380
/**
4481
* Function that verifies the user2fa and proceeds to open the 2faModal; in case 2fa isn't active, it signs in the user
4582
* @param {{email:string, password: string}} loginData Login data received from LoginForm it has email and password
4683
*/
4784
const loginToApp = async (loginData) => {
48-
swal.fire({
49-
title: "Submitting",
50-
showConfirmButton: false,
51-
willOpen: () => {
52-
swal.showLoading();
53-
},
54-
});
85+
showLoading("Submitting");
5586
setSubmitting(true);
5687
try {
57-
let user = await loginUser(loginData);
58-
createSeed(loginData.password);
59-
let user2fa = await get2faInfoUser(user.uid);
60-
if (user2fa.twoFa === true && user2fa.sms === true) {
88+
const user = await loginUser(loginData);
89+
// store ephemeral data for post-2FA persistence
90+
setPendingPassword(loginData.password);
91+
setPendingUser(user);
92+
93+
const user2fa = await get2faInfoUser(user.uid, user.accessToken);
94+
const hasSms2FA = user2fa.twoFa === true && user2fa.sms === true;
95+
const hasGAuth2FA = user2fa.twoFa === true && user2fa.gAuth === true;
96+
97+
if (hasSms2FA) {
6198
swal.close();
62-
let phoneProvider = await loginWithPhoneNumber(
99+
const confirmation = await loginWithPhoneNumber(
63100
user.phoneNumber,
64101
window.recaptchaVerifier
65102
);
66-
setUserSignInSms(phoneProvider);
67-
setOpenSMS2Fa(true);
103+
setSmsConfirmation(confirmation);
104+
setActive2FAMethod("sms");
68105
setSubmitting(false);
69-
} else if (user2fa.twoFa === true && user2fa.gAuth === true) {
106+
return;
107+
}
108+
109+
if (hasGAuth2FA) {
70110
swal.close();
71-
setUserSignInGAuth({ ...user, secret: user2fa.gAuthSecret });
72-
setOpenGAuth2Fa(true);
111+
setGauthContext({ ...user, secret: user2fa.gAuthSecret });
112+
setActive2FAMethod("gauth");
73113
setSubmitting(false);
74-
} else {
75-
await swal.fire({
76-
icon: "success",
77-
title: "Logged in",
78-
timer: 2000,
79-
showConfirmButton: false,
80-
});
81-
setUserDataLogin(user);
82-
history.push("/governance");
114+
return;
83115
}
116+
117+
// No 2FA: finalize immediately
118+
await showSuccess("Logged in");
119+
createSeed(loginData.password);
120+
await finalizeLogin(user);
84121
} catch (error) {
85122
deleteUserData();
86-
swal.fire({
87-
icon: "error",
88-
title: "Error",
89-
text: error.message,
90-
});
123+
showError(error.message);
91124
removeSeed();
92-
return setSubmitting(false);
125+
setSubmitting(false);
93126
}
94127
};
95128

@@ -98,67 +131,30 @@ function Login({ t }) {
98131
* @param {string} gAuthCode gAuthcode submitted by the user in the 2fa modal
99132
*/
100133
const verifyGAuth = async ({ gAuthCode }) => {
101-
swal.fire({
102-
title: "Verifying",
103-
showConfirmButton: false,
104-
willOpen: () => {
105-
swal.showLoading();
106-
},
107-
});
108-
109-
verifyGauthCode(gAuthCode)
110-
.then(() => {
111-
setOpenGAuth2Fa(false);
112-
swal.fire({
113-
icon: "success",
114-
title: "Logged in",
115-
text: "Code verified",
116-
timer: 2000,
117-
showConfirmButton: false,
118-
});
119-
setUserDataLogin(userSignInGAuth);
120-
history.push("/governance");
121-
})
122-
.catch((err) => {
123-
swal.fire({
124-
icon: "error",
125-
title: "Invalid code",
126-
});
127-
});
134+
showLoading("Verifying");
135+
try {
136+
await verifyGauthCode(gAuthCode, pendingUser?.accessToken);
137+
setActive2FAMethod(null);
138+
await showSuccess("Logged in");
139+
await finalizeLogin(gauthContext);
140+
} catch (err) {
141+
showError("Invalid code");
142+
}
128143
};
129144

130145
/**
131146
* Function that verifies the SMS verification code and proceeds to login
132147
* @param {string} phoneCode phoneCode submitted by the user in the 2fa modal
133148
*/
134149
const verifyPhone = async ({ phoneCode }) => {
135-
swal.fire({
136-
title: "Verifying",
137-
showConfirmButton: false,
138-
willOpen: () => {
139-
swal.showLoading();
140-
},
141-
});
150+
showLoading("Verifying");
142151
try {
143-
let { user } = await userSignInSms.confirm(phoneCode).catch((err) => {
144-
throw err;
145-
});
146-
setOpenSMS2Fa(false);
147-
await swal.fire({
148-
icon: "success",
149-
title: "Logged in",
150-
text: "Code verified",
151-
timer: 2000,
152-
showConfirmButton: false,
153-
});
154-
setUserDataLogin(user);
155-
history.push("/governance");
152+
const { user } = await smsConfirmation.confirm(phoneCode);
153+
setActive2FAMethod(null);
154+
await showSuccess("Logged in");
155+
await finalizeLogin(user);
156156
} catch (error) {
157-
swal.fire({
158-
icon: "error",
159-
title: "Error",
160-
text: error.message,
161-
});
157+
showError(error.message);
162158
}
163159
};
164160

@@ -193,13 +189,13 @@ function Login({ t }) {
193189
</div>
194190
</div>
195191
</main>
196-
<CustomModal open={openSMS2Fa} onClose={() => setOpenSMS2Fa(false)}>
192+
<CustomModal open={active2FAMethod === "sms"} onClose={() => setActive2FAMethod(null)}>
197193
<SMSTwoFAFormLogin
198194
userSignInSms={verifyPhone}
199-
closeModal={() => setOpenSMS2Fa(false)}
195+
closeModal={() => setActive2FAMethod(null)}
200196
/>
201197
</CustomModal>
202-
<CustomModal open={openGAuthFa} onClose={() => setOpenGAuth2Fa(false)}>
198+
<CustomModal open={active2FAMethod === "gauth"} onClose={() => setActive2FAMethod(null)}>
203199
<GAuthTwoFAFormLogin userSignInGAuth={verifyGAuth} />
204200
</CustomModal>
205201
</Background>

src/utils/request.js

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -693,14 +693,20 @@ export const getUserInfo = async (id, cancelToken) => {
693693
});
694694
};
695695

696-
export const get2faInfoUser = async (id) => {
696+
export const get2faInfoUser = async (id, tokenOverride) => {
697697
return new Promise((resolve, reject) => {
698-
let { accessToken } = getUserData();
698+
let accessToken = tokenOverride;
699+
if (!accessToken) {
700+
const stored = getUserData();
701+
accessToken = stored?.accessToken;
702+
}
699703
apiClient
700704
.get(`/user/verify2fa/${id}`, {
701-
headers: {
702-
Authorization: `Bearer ${accessToken}`,
703-
},
705+
headers: accessToken
706+
? {
707+
Authorization: `Bearer ${accessToken}`,
708+
}
709+
: {},
704710
})
705711
.then(({ data }) => {
706712
let { user } = data;
@@ -974,17 +980,23 @@ export const logout = async () => {
974980
});
975981
};
976982

977-
export const verifyGauthCode = async (code) => {
978-
let { accessToken } = getUserData();
983+
export const verifyGauthCode = async (code, tokenOverride) => {
984+
let accessToken = tokenOverride;
985+
if (!accessToken) {
986+
const stored = getUserData();
987+
accessToken = stored?.accessToken;
988+
}
979989
return new Promise((resolve, reject) => {
980990
apiClient
981991
.post(
982992
`/user/verify-gauth-code`,
983993
{ code },
984994
{
985-
headers: {
986-
Authorization: `Bearer ${accessToken}`,
987-
},
995+
headers: accessToken
996+
? {
997+
Authorization: `Bearer ${accessToken}`,
998+
}
999+
: {},
9881000
}
9891001
)
9901002
.then((resp) => resolve(resp))

0 commit comments

Comments
 (0)