Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add iOS and Android view for SAML Login #29526

Merged
merged 39 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
400156a
add native view for SAML SignInPage
NikkiWines Oct 13, 2023
ca0d065
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Oct 13, 2023
8991e7e
log in with shortlivedtoken when one is returned
NikkiWines Oct 13, 2023
ad6c373
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Oct 23, 2023
0168e72
include const
NikkiWines Oct 23, 2023
9e4b7ae
style and clean up
NikkiWines Oct 24, 2023
1866096
add SAMLLOadingINdicator
NikkiWines Oct 24, 2023
89dc792
style
NikkiWines Oct 24, 2023
526cf8f
prettier
NikkiWines Oct 24, 2023
c386ce4
include platform
NikkiWines Oct 24, 2023
ef12fe6
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Oct 25, 2023
5172f95
pull in changes from infinitered:cdanwards/saml-webview
NikkiWines Oct 25, 2023
544ff27
fix imports
NikkiWines Oct 26, 2023
7073d93
fix redirect flickering when pressing go back after choosing magic code
NikkiWines Oct 26, 2023
4586fee
reset sign in data if going back from SSO provider page
NikkiWines Oct 26, 2023
46a336f
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Oct 26, 2023
4dd5f55
style
NikkiWines Oct 27, 2023
8ff4c64
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Oct 30, 2023
14818b7
add in error handling from App/pull/30027
NikkiWines Oct 30, 2023
ed66009
update imports
NikkiWines Oct 30, 2023
b247558
prettier
NikkiWines Oct 30, 2023
380fa26
remove navigation after callback
NikkiWines Oct 30, 2023
dc9d918
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Nov 2, 2023
eaa2438
style
NikkiWines Nov 2, 2023
1c3ad08
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Nov 7, 2023
b011750
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Nov 7, 2023
efa391d
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Nov 10, 2023
dab32d8
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Nov 14, 2023
769d880
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Nov 16, 2023
fd12887
update error handing to set account error message
NikkiWines Nov 17, 2023
6b5236d
Merge branch 'nikki-saml-newdot-ios' of https://github.com/Expensify/…
NikkiWines Nov 17, 2023
79e89c2
return early when we've already initiated saml login
NikkiWines Nov 17, 2023
6bbce78
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Nov 26, 2023
1a62ccc
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Nov 28, 2023
0ebfd25
showLoginForm when error is returned during SAML login
NikkiWines Nov 28, 2023
4a020de
clear signin data after recieving error
NikkiWines Nov 29, 2023
5c8519f
remove unused onyxkey import
NikkiWines Dec 1, 2023
43d7439
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Dec 1, 2023
01c4029
Merge branch 'main' of https://github.com/Expensify/App into nikki-sa…
NikkiWines Dec 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/components/SAMLLoadingIndicator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import {StyleSheet, View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import styles from '@styles/styles';
import themeColors from '@styles/themes/default';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import * as Illustrations from './Icon/Illustrations';
import Text from './Text';

function SAMLLoadingIndicator() {
const {translate} = useLocalize();
return (
<View style={[StyleSheet.absoluteFillObject, styles.deeplinkWrapperContainer]}>
<View style={styles.deeplinkWrapperMessage}>
<View style={styles.mb2}>
<Icon
width={200}
height={164}
src={Illustrations.RocketBlue}
/>
</View>
<Text style={[styles.textHeadline, styles.textXXLarge, styles.textAlignCenter]}>{translate('samlSignIn.launching')}</Text>
<View style={[styles.mt2, styles.mh2, styles.fontSizeNormal, styles.textAlignCenter]}>
<Text style={[styles.textAlignCenter]}>{translate('samlSignIn.oneMoment')}</Text>
</View>
</View>
<View style={styles.deeplinkWrapperFooter}>
<Icon
width={154}
height={34}
fill={themeColors.success}
src={Expensicons.ExpensifyWordmark}
/>
</View>
</View>
);
}

SAMLLoadingIndicator.displayName = 'SAMLLoadingIndicator';

export default SAMLLoadingIndicator;
1 change: 0 additions & 1 deletion src/pages/LogInWithShortLivedAuthTokenPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,4 @@ LogInWithShortLivedAuthTokenPage.displayName = 'LogInWithShortLivedAuthTokenPage

export default withOnyx({
account: {key: ONYXKEYS.ACCOUNT},
session: {key: ONYXKEYS.SESSION},
})(LogInWithShortLivedAuthTokenPage);
39 changes: 2 additions & 37 deletions src/pages/signin/SAMLSignInPage/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import PropTypes from 'prop-types';
import React, {useEffect} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@styles/themes/useTheme';
import useThemeStyles from '@styles/useThemeStyles';
import SAMLLoadingIndicator from '@components/SAMLLoadingIndicator';
import CONFIG from '@src/CONFIG';
import ONYXKEYS from '@src/ONYXKEYS';

Expand All @@ -25,39 +18,11 @@ const defaultProps = {
};

function SAMLSignInPage({credentials}) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();

useEffect(() => {
window.open(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`, '_self');
}, [credentials.login]);

return (
<View style={styles.deeplinkWrapperContainer}>
<View style={styles.deeplinkWrapperMessage}>
<View style={styles.mb2}>
<Icon
width={200}
height={164}
src={Illustrations.RocketBlue}
/>
</View>
<Text style={[styles.textHeadline, styles.textXXLarge, styles.textAlignCenter]}>{translate('samlSignIn.launching')}</Text>
<View style={[styles.mt2, styles.mh2, styles.fontSizeNormal, styles.textAlignCenter]}>
<Text style={[styles.textAlignCenter]}>{translate('samlSignIn.oneMoment')}</Text>
</View>
</View>
<View style={styles.deeplinkWrapperFooter}>
<Icon
width={154}
height={34}
fill={theme.success}
src={Expensicons.ExpensifyWordmark}
/>
</View>
</View>
);
return <SAMLLoadingIndicator />;
}

SAMLSignInPage.propTypes = propTypes;
Expand Down
95 changes: 95 additions & 0 deletions src/pages/signin/SAMLSignInPage/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import PropTypes from 'prop-types';
import React, {useCallback, useState} from 'react';
import {withOnyx} from 'react-native-onyx';
import WebView from 'react-native-webview';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import SAMLLoadingIndicator from '@components/SAMLLoadingIndicator';
import ScreenWrapper from '@components/ScreenWrapper';
import getPlatform from '@libs/getPlatform';
import Navigation from '@libs/Navigation/Navigation';
import * as Session from '@userActions/Session';
import CONFIG from '@src/CONFIG';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';

const propTypes = {
/** The credentials of the logged in person */
credentials: PropTypes.shape({
/** The email/phone the user logged in with */
login: PropTypes.string,
}),
};

const defaultProps = {
credentials: {},
};

function SAMLSignInPage({credentials}) {
const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}&platform=${getPlatform()}`;
const [showNavigation, shouldShowNavigation] = useState(true);

/**
* Handles in-app navigation once we get a response back from Expensify
*
* @param {String} params.url
*/
const handleNavigationStateChange = useCallback(
({url}) => {
// If we've gotten a callback then remove the option to navigate back to the sign in page
if (url.includes('loginCallback')) {
shouldShowNavigation(false);
}

const searchParams = new URLSearchParams(new URL(url).search);
if (searchParams.has('shortLivedAuthToken')) {
const shortLivedAuthToken = searchParams.get('shortLivedAuthToken');
Session.signInWithShortLivedAuthToken(credentials.login, shortLivedAuthToken);
}

// If the login attempt is unsuccessful, set the error message for the account and redirect to sign in page
if (searchParams.has('error')) {
Session.clearSignInData();
Session.setAccountError(searchParams.get('error'));
Navigation.navigate(ROUTES.HOME);
}
},
[credentials.login, shouldShowNavigation],
);

return (
<ScreenWrapper
shouldShowOfflineIndicator={false}
includeSafeAreaPaddingBottom={false}
testID={SAMLSignInPage.displayName}
>
{showNavigation && (
<HeaderWithBackButton
title=""
onBackButtonPress={() => {
Session.clearSignInData();
Navigation.navigate(ROUTES.HOME);
}}
/>
)}
<FullPageOfflineBlockingView>
<WebView
originWhitelist={['https://*']}
source={{uri: samlLoginURL}}
incognito // 'incognito' prop required for Android, issue here https://github.com/react-native-webview/react-native-webview/issues/1352
startInLoadingState
renderLoading={() => <SAMLLoadingIndicator />}
onNavigationStateChange={handleNavigationStateChange}
/>
</FullPageOfflineBlockingView>
</ScreenWrapper>
);
}

SAMLSignInPage.propTypes = propTypes;
SAMLSignInPage.defaultProps = defaultProps;
SAMLSignInPage.displayName = 'SAMLSignInPage';

export default withOnyx({
credentials: {key: ONYXKEYS.CREDENTIALS},
})(SAMLSignInPage);
29 changes: 17 additions & 12 deletions src/pages/signin/SignInPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import useLocalize from '@hooks/useLocalize';
import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as ActiveClientManager from '@libs/ActiveClientManager';
import getPlatform from '@libs/getPlatform';
import * as Localize from '@libs/Localize';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
Expand Down Expand Up @@ -103,15 +102,9 @@ function getRenderOptions({hasLogin, hasValidateCode, account, isPrimaryLogin, i
const isSAMLRequired = Boolean(account.isSAMLRequired);
const hasEmailDeliveryFailure = Boolean(account.hasEmailDeliveryFailure);

// SAML is temporarily restricted to users on the beta or to users signing in on web and mweb
let shouldShowChooseSSOOrMagicCode = false;
let shouldInitiateSAMLLogin = false;
const platform = getPlatform();
if (platform === CONST.PLATFORM.WEB || platform === CONST.PLATFORM.DESKTOP) {
// True if the user has SAML required and we haven't already initiated SAML for their account
shouldInitiateSAMLLogin = hasAccount && hasLogin && isSAMLRequired && !hasInitiatedSAMLLogin && account.isLoading;
shouldShowChooseSSOOrMagicCode = hasAccount && hasLogin && isSAMLEnabled && !isSAMLRequired && !isUsingMagicCode;
}
// True if the user has SAML required and we haven't already initiated SAML for their account
const shouldInitiateSAMLLogin = hasAccount && hasLogin && isSAMLRequired && !hasInitiatedSAMLLogin && account.isLoading;
const shouldShowChooseSSOOrMagicCode = hasAccount && hasLogin && isSAMLEnabled && !isSAMLRequired && !isUsingMagicCode;

// SAML required users may reload the login page after having already entered their login details, in which
// case we want to clear their sign in data so they don't end up in an infinite loop redirecting back to their
Expand Down Expand Up @@ -167,6 +160,19 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer
}
App.setLocale(Localize.getDevicePreferredLocale());
}, [preferredLocale]);
useEffect(() => {
if (credentials.login) {
return;
}

// If we don't have a login set, reset the user's SAML login preferences
if (isUsingMagicCode) {
setIsUsingMagicCode(false);
}
if (hasInitiatedSAMLLogin) {
setHasInitiatedSAMLLogin(false);
}
}, [credentials.login, isUsingMagicCode, setIsUsingMagicCode, hasInitiatedSAMLLogin, setHasInitiatedSAMLLogin]);

const {
shouldShowLoginForm,
Expand Down Expand Up @@ -231,7 +237,7 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer
if (shouldShowEmailDeliveryFailurePage || shouldShowChooseSSOOrMagicCode) {
welcomeText = '';
}
} else if (!shouldInitiateSAMLLogin) {
} else if (!shouldInitiateSAMLLogin && !hasInitiatedSAMLLogin) {
Log.warn('SignInPage in unexpected state!');
}

Expand Down Expand Up @@ -261,7 +267,6 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer
<ValidateCodeForm
isUsingRecoveryCode={isUsingRecoveryCode}
setIsUsingRecoveryCode={setIsUsingRecoveryCode}
setIsUsingMagicCode={setIsUsingMagicCode}
/>
)}
{shouldShowUnlinkLoginForm && <UnlinkLoginForm />}
Expand Down
5 changes: 0 additions & 5 deletions src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,6 @@ const propTypes = {
/** Function to change `isUsingRecoveryCode` state when user toggles between 2fa code and recovery code */
setIsUsingRecoveryCode: PropTypes.func.isRequired,

/** Function to change `isUsingMagicCode` state when the user goes back to the login page */
setIsUsingMagicCode: PropTypes.func.isRequired,

...withLocalizePropTypes,
};

Expand Down Expand Up @@ -210,8 +207,6 @@ function BaseValidateCodeForm(props) {
* Clears local and Onyx sign in states
*/
const clearSignInData = () => {
// Reset the user's preference for signing in with SAML versus magic codes
props.setIsUsingMagicCode(false);
clearLocalSignInData();
Session.clearSignInData();
};
Expand Down
Loading