From 97f375472e6665f2bc58f3a89c326e2351a10197 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Fri, 17 May 2024 13:42:41 -0700 Subject: [PATCH] adding mfa login for mobile --- app/mobile/src/access/login/Login.jsx | 35 +++++++++-- app/mobile/src/access/login/Login.styled.js | 64 +++++++++++++++++++- app/mobile/src/access/login/useLogin.hook.js | 10 ++- app/mobile/src/api/fetchUtil.js | 1 + app/mobile/src/api/setLogin.js | 5 +- app/mobile/src/constants/Strings.js | 42 +++++++++++++ app/mobile/src/context/useAppContext.hook.js | 6 +- app/mobile/src/utils/InputCode.jsx | 46 ++++++++++++++ 8 files changed, 194 insertions(+), 15 deletions(-) create mode 100644 app/mobile/src/utils/InputCode.jsx diff --git a/app/mobile/src/access/login/Login.jsx b/app/mobile/src/access/login/Login.jsx index f46b3055..ad64f954 100644 --- a/app/mobile/src/access/login/Login.jsx +++ b/app/mobile/src/access/login/Login.jsx @@ -6,6 +6,7 @@ import { Colors } from 'constants/Colors'; import MatIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import { tos } from 'constants/TermsOfService'; import { BlurView } from "@react-native-community/blur"; +import { InputCode } from 'utils/InputCode'; export function Login() { @@ -132,14 +133,38 @@ export function Login() { supportedOrientations={['portrait', 'landscape']} onRequestClose={actions.dismissMFA} > - + - + - Multi-Factor Authentication - Enter your verification code + { state.strings.mfaTitle } + { state.strings.mfaEnter } + + + { state.mfaError == '403' && ( + { state.strings.mfaError } + )} + { state.mfaError == '429' && ( + { state.strings.mfaDisabled } + )} + + + + { state.strings.cancel } + + { state.mfaCode != '' && ( + + { state.strings.mfaConfirm } + + )} + { state.mfaCode == '' && ( + + { state.strings.mfaConfirm } + + )} + - + diff --git a/app/mobile/src/access/login/Login.styled.js b/app/mobile/src/access/login/Login.styled.js index 34401765..cf2274db 100644 --- a/app/mobile/src/access/login/Login.styled.js +++ b/app/mobile/src/access/login/Login.styled.js @@ -26,6 +26,56 @@ export const styles = StyleSheet.create({ width: '100%', height: '100%', }, + mfaError: { + width: '100%', + height: 24, + display: 'flex', + alignItems: 'center', + }, + mfaErrorLabel: { + color: Colors.dangerText, + }, + mfaControl: { + height: 32, + display: 'flex', + flexDirection: 'row', + width: '100%', + justifyContent: 'flex-end', + gap: 16, + }, + mfaCancel: { + width: 72, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: Colors.cancelButton, + borderRadius: 4, + }, + mfaCancelLabel: { + color: Colors.cancelButtonText, + }, + mfaConfirm: { + width: 72, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: Colors.primaryButton, + borderRadius: 4, + }, + mfaConfirmLabel: { + color: Colors.primaryButtonText, + }, + mfaDisabled: { + width: 72, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: Colors.disabledButton, + borderRadius: 4, + }, + mfaDisabledLabel: { + color: Colors.disabledButtonText, + }, mfaBase: { position: 'absolute', top: 0, @@ -43,16 +93,26 @@ export const styles = StyleSheet.create({ width: '80%', maxWidth: 400, display: 'flex', - gap: 16, + gap: 8, alignItems: 'center', borderRadius: 8, padding: 16, }, mfaTitle: { fontSize: 20, + color: Colors.descriptionText, + paddingBottom: 8, }, mfaDescription: { - fontSize: 16, + fontSize: 14, + color: Colors.descriptionText, + }, + mfaCode: { + width: 400, + borderWidth: 1, + borderColor: '#333333', + width: '100%', + opacity: 0, }, tos: { display: 'flex', diff --git a/app/mobile/src/access/login/useLogin.hook.js b/app/mobile/src/access/login/useLogin.hook.js index cf4697b2..1930fe19 100644 --- a/app/mobile/src/access/login/useLogin.hook.js +++ b/app/mobile/src/access/login/useLogin.hook.js @@ -19,7 +19,7 @@ export function useLogin() { agree: false, showTerms: false, mfaModal: false, - mfaCode: null, + mfaCode: '', mfaError: null, }); @@ -77,11 +77,12 @@ export function useLogin() { if (!state.busy) { updateState({ busy: true }); try { - await app.actions.login(state.login.trim(), state.password); + await app.actions.login(state.login.trim(), state.password, state.mfaCode); } catch (err) { +console.log(err); if (err.message == '405' || err.message == '403' || err.message == '429') { - updateState({ mfaModal: true, mfaError: err.message, mfaCode: '' }); + updateState({ mfaModal: true, mfaError: err.message }); } else { console.log(err.message); @@ -92,6 +93,9 @@ export function useLogin() { updateState({ busy: false }); } }, + setCode: (mfaCode) => { + updateState({ mfaCode }); + }, dismissMFA: () => { updateState({ mfaModal: false }); }, diff --git a/app/mobile/src/api/fetchUtil.js b/app/mobile/src/api/fetchUtil.js index dc230514..853735e7 100644 --- a/app/mobile/src/api/fetchUtil.js +++ b/app/mobile/src/api/fetchUtil.js @@ -8,6 +8,7 @@ export function createWebsocket(url) { export function checkResponse(response) { if(response.status >= 400 && response.status < 600) { + console.log(`${response.url} failed [${response?.status}]`); throw new Error(response.status); } } diff --git a/app/mobile/src/api/setLogin.js b/app/mobile/src/api/setLogin.js index dc2cf15c..0550ecc1 100644 --- a/app/mobile/src/api/setLogin.js +++ b/app/mobile/src/api/setLogin.js @@ -1,13 +1,14 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; import base64 from 'react-native-base64' -export async function setLogin(username, server, password, appName, appVersion, platform, deviceToken, pushType, notifications) { +export async function setLogin(username, server, password, code, appName, appVersion, platform, deviceToken, pushType, notifications) { const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server); const protocol = insecure ? 'http' : 'https'; + const mfa = code ? `&code=${code}` : ''; let headers = new Headers() headers.append('Authorization', 'Basic ' + base64.encode(username + ":" + password)); - let login = await fetchWithTimeout(`${protocol}://${server}/account/apps?appName=${appName}&appVersion=${appVersion}&platform=${platform}&deviceToken=${deviceToken}&pushType=${pushType}`, { method: 'POST', body: JSON.stringify(notifications), headers: headers }) + let login = await fetchWithTimeout(`${protocol}://${server}/account/apps?appName=${appName}&appVersion=${appVersion}&platform=${platform}&deviceToken=${deviceToken}&pushType=${pushType}${mfa}`, { method: 'POST', body: JSON.stringify(notifications), headers: headers }) checkResponse(login) return await login.json() } diff --git a/app/mobile/src/constants/Strings.js b/app/mobile/src/constants/Strings.js index 18b48531..c0467e47 100644 --- a/app/mobile/src/constants/Strings.js +++ b/app/mobile/src/constants/Strings.js @@ -203,6 +203,13 @@ const Strings = [ reportMessage: 'Report Message', select: 'Select', selectTopic: 'Select Topic for Sharing', + + mfaTitle: 'Multi-Factor Authentication', + mfaSteps: 'Store the secret and confirm the verification code', + mfaError: 'verification code error', + mfaDisabled: 'verification temporarily disabled', + mfaConfirm: 'Confirm', + mfaEnter: 'Enter your verification code', }, { languageCode: 'fr', @@ -402,6 +409,13 @@ const Strings = [ reportMessage: 'Signaler le Message', select: 'Choisir', selectTopic: 'Choisissez le sujet à partager', + + mfaTitle: 'Authentification Multi-Factor', + mfaSteps: 'Enregistrez le secret et confirmez le code de vérification', + mfaEnter: 'Entrez votre code de vérification', + mfaError: 'erreur de code de vérification', + mfaDisabled: 'vérification temporairement désactivée', + mfaConfirm: 'Confirmer', }, { languageCode: 'es', @@ -602,6 +616,13 @@ const Strings = [ select: 'Elegir', selectTopic: 'Elija un tema para compartir', + + mfaTitle: 'Autenticación de Dos Factores', + mfaSteps: 'Guarde el secreto y confirme el código de verificación', + mfaEnter: 'Ingresa tu código de verificación', + mfaError: 'error de código de verificación', + mfaDisabled: 'verificación temporalmente deshabilitada', + mfaConfirm: 'Confirmar', }, { languageCode: 'de', @@ -802,6 +823,13 @@ const Strings = [ select: 'Wählen', selectTopic: 'Wählen Sie ein Thema zum Teilen aus', + + mfaTitle: 'Zwei-Faktor-Authentifizierung', + mfaSteps: 'Speichern Sie das Geheimnis und bestätigen Sie den Verifizierungscode', + mfaEnter: 'Geben Sie Ihren Bestätigungs-Code ein', + mfaError: 'Verifizierungscodefehler', + mfaDisabled: 'Verifizierung vorübergehend deaktiviert', + mfaConfirm: 'Bestätigen', }, { languageCode: 'pt', @@ -987,6 +1015,13 @@ const Strings = [ select: 'Escolher', selectTopic: 'Escolha o tópico para compartilhar', + + mfaTitle: 'Autenticação de Dois Fatores', + mfaSteps: 'Salve o segredo e confirme o código de verificação', + mfaEnter: 'Digite seu código de verificação', + mfaError: 'erro de código de verificação', + mfaDisabled: 'verificação temporariamente desativada', + mfaConfirm: 'Confirmar', }, { languageCode: 'ru', @@ -1170,6 +1205,13 @@ const Strings = [ select: 'выбирать', selectTopic: 'Выберите тему для обмена', + + mfaTitle: 'Двухфакторная аутентификация', + mfaSteps: 'Сохраните секрет и подтвердите проверочный код', + mfaEnter: 'Введите Ваш верификационный код', + mfaError: 'ошибка проверочного кода', + mfaDisabled: 'проверка временно отключена', + mfaConfirm: 'Подтвердить', } ]; diff --git a/app/mobile/src/context/useAppContext.hook.js b/app/mobile/src/context/useAppContext.hook.js index abbdfaa6..13246365 100644 --- a/app/mobile/src/context/useAppContext.hook.js +++ b/app/mobile/src/context/useAppContext.hook.js @@ -121,7 +121,7 @@ export function useAppContext() { await setDeviceToken(); updateState({ loggedOut: false }); await addAccount(server, username, password, token); - const session = await setLogin(username, server, password, getApplicationName(), getVersion(), getDeviceId(), deviceToken.current, pushType.current, notifications) + const session = await setLogin(username, server, password, null, getApplicationName(), getVersion(), getDeviceId(), deviceToken.current, pushType.current, notifications) access.current = { loginTimestamp: session.created, server, token: session.appToken, guid: session.guid }; await store.actions.setSession(access.current); await setSession(); @@ -143,14 +143,14 @@ export function useAppContext() { messaging().requestPermission().then(status => {}) } }, - login: async (username, password) => { + login: async (username, password, code) => { if (!init.current || access.current) { throw new Error('invalid session state'); } await setDeviceToken(); updateState({ loggedOut: false }); const acc = username.includes('/') ? username.split('/') : username.split('@'); - const session = await setLogin(acc[0], acc[1], password, getApplicationName(), getVersion(), getDeviceId(), deviceToken.current, pushType.current, notifications) + const session = await setLogin(acc[0], acc[1], password, code, getApplicationName(), getVersion(), getDeviceId(), deviceToken.current, pushType.current, notifications) access.current = { loginTimestamp: session.created, server: acc[1], token: session.appToken, guid: session.guid }; await store.actions.setSession(access.current); await setSession(); diff --git a/app/mobile/src/utils/InputCode.jsx b/app/mobile/src/utils/InputCode.jsx new file mode 100644 index 00000000..612b7533 --- /dev/null +++ b/app/mobile/src/utils/InputCode.jsx @@ -0,0 +1,46 @@ +import { TextInput, Text, View, TouchableOpacity } from 'react-native'; +import { useState } from 'react'; + +export function InputCode({ onChangeText, style }) { + + const [code, setCode] = useState(''); + + const updateCode = (value) => { + if (value.length >= 6) { + onChangeText(value.slice(0, 6)); + } + else { + onChangeText(''); + } + setCode(value.slice(0, 6)); + } + + return ( + + + + + { code.charAt(0) } + + + { code.charAt(1) } + + + { code.charAt(2) } + + + { code.charAt(3) } + + + { code.charAt(4) } + + + { code.charAt(5) } + + + + + + ); +} +