React Native Mobile Client

How can we retrieve password related information and derive cloud account passwords?

Introduction

Given that we want to build our own password manager and that we have already deployed the server-side component for it in AWS (as motivated in the introduction and explained in the Python serverless server section), how should we build and deploy a client component for mobile platforms that will work with this server-side component and be free or low cost over time? The general answer to that is that we have a lot of flexibility with respect to which free client technology we choose, assuming that whatever technology we choose will have no ongoing "carrying" costs. The specific answer to that is that I chose to use the React Native framework and the JavaScript language to specify and code the client user interface and behavior and the Expo framework and services to deploy it and make new versions of it available over time. Thus, we can develop and deploy a client mobile component without having to pre-provision any server resources and with little to no cost (excluding our development efforts).

Context

During the early and middle parts of the 2010s, a lot of mobile applications were developed with frameworks that helped developers leverage their web application development skills and assets and to develop cross-platform mobile applications — the Cordova and Ionic frameworks are two such examples. However, since the early and middle parts of the 2010s, society as a whole has continued to use mobile applications more and more and this has highlighted some shortcomings between those approaches on one end of the spectrum and purely native mobile application development on the other end of the spectrum. Increasingly, we as consumers/users of mobile applications want/need mobile applications that have native platform UI/UX experiences, native platform performance, and better access to the platform devices.

Hybrid mobile application development frameworks, like NativeScript, React Native, and Flutter, have addressed these shortcomings in various ways while still allowing mobile application developers to develop and deploy their mobile applications across mobile platforms (Android and iOS in particular). However, only one of these three allows mobile application developers to release "dot updates" to their application without incurring the overhead of preparing a new version for the App stores (the Apple App store in particular seems to change almost constantly with respect to what needs to be done to do this) — React Native. Also, because I made the decision to use the React framework to develop the client browser application and because some amount of code can be shared between React and React Native applications, that made React Native a more favorable choice in this respect. Therefore, I made the decision to use React Native for this client mobile application.

Problem

Again, the essence of the problem that we want to solve with this client mobile application is to retrieve the relevant cloud account salt value and password transformation information, and then combine it with the user's passcode to derive and make available their password for that cloud account, when a user wants to log in to that cloud account (as described in the introduction). This is the client interaction that will provide the most value to the user, because it will allow them to copy the secure password that the client has derived for them and then to paste it in to their cloud provider's authentication component so that they can log in to that cloud account, without having to remember anything about that secure password. And, this interaction is the one that will be executed the most often, with the assumption that once these salt values and password transformation rules are set up by the user with some combination of their client applications, getting cloud account passwords will be the primary user actions.

For example, a typical user session might include getting their password for their Mint account, logging in to Mint to find out how much they owe for all of their credit cards, then getting their password for their bank, logging in to that bank account to set up the transfers/payments for those credit cards, and then doing the same sorts of things to set up transfers/payments for their utilities. Each time the user needs a password for a particular cloud account of theirs, this client application will request the salt value and the password transformation rules for that cloud account, combine it with the user's passcode, derive the user's password, and finally make it available to them so that they can use it to log in to that cloud account. And these user interactions could take place between them and a browser application, or a mobile application, or both.

Surrounding the essence of the problem is that this client application needs to conform to the security protocols that are dictated by the server-side, in order to keep the confidentiality, integrity, and availability (CIA) of the salt values and password transformation rules for each cloud account for each user. As described in the Python serverless server section, this means that this client application will first need to interact with the AWS Cognito service to authenticate the user with their email address and a password, so that they can obtain a Cognito JWT. This email/password pair is the first factor of the two-factor authentication (2FA) or multi-factor authentication (MFA); it is something that the user knows. Then, the client application will need to use that Cognito JWT to request and then claim a Rosetta Salt JWT with a code that the user will receive in their email. Once the client application has that Rosetta JWT, it will be able to use that JWT to make subsequent requests against the other server-side API endpoints. This code that the user will receive in their email is the second factor of the 2FA or MFA; it is something that the user has (or will have because of the email account that they do have).

Finally, although they are not the essence of the problem that we're trying to solve either, we need to provide the user with the capabilities to manage the information associated with the cloud accounts they are targeting with the help of Rosetta Salt; we can think of the information in Rosetta Salt that represents a cloud account for them as a target site. So, the client application needs to allow them to add new target sites, update existing target sites, and delete existing target sites. These additional behaviors are necessary to enable the steady-state use of the client application, even though the frequency of these interactions will likely be much lower than the actual steady-state interactions (getting the information for target sites and for a particular target site). And, although maintaining these cloud accounts/target sites may not be quite as easy on a mobile application, we should provide it anyhow as some people don't use or rarely use browser applications anymore.

Solution

Multi-Factor Authentication via AWS Cognito and Rosetta Salt

Even though it surrounds the essence of the problem, I will first describe the solution for conforming to the server-side security protocol.

Below are some relevant snippets of this React Native application, along with a link to a page with more detail. Also, an accompanying YouTube video that shows how to deploy the Rosetta Salt mobile app can be seen here.

To support the first factor, I "simply" used some components that are made available by the AWS Amplify project to support the email/password authentication. I put the word simply in quotes here because these Amplify components were (and maybe still are) in a bit of a state of flux and it did take some time to figure out how to use them properly. But, as a motivation and bonus for persevering, these components provide support for registration, verification, and forgotten password scenarios "out of the box," which means that I didn't need to implement them.

Support for the first factor of authentiction is largely implemented in this screens/RosettaEntry.js file. But, you can look at the additional details related to this first factor of authentication here.

In this file, the Authenticator is used to "wrap" the rest of the application (which is represented by the AmplifyAuthComponent), which means that the first factor of authentication must be completed before moving on. And, although it may not be easy to see, there is some room for customizing the appearance of some of the aspects of authenticating with Cognito, to make it appear to be Rosetta Salt's custom version of Cognito or even Rosetta Salt's own authentication mechanism. I chose to keep the color theme for the Authenticator, to convey in color that the first factor of authentication is being completed by AWS Cognito. Finally, although it is not obvious from the code here, once you have authenticated and passed through the Authenticator to the rest of the application, you will continue to be able to do so without re-performing this Cognito authentication until either the Cognito JWT expires (30 days) or it is explicitly invalidated (via sign out or revocation).

--

screens/RosettaEntry.js

--

import React from 'react';

import {
  Authenticator,
  ConfirmSignIn,
  ConfirmSignUp,
  ForgotPassword,
  RequireNewPassword,
  SignIn,
  SignUp,
} from 'aws-amplify-react-native';

import AmplifyAuthComponent from '../components/AmplifyAuthComponent';
import AmplifyAuthLoading from '../components/AmplifyAuthLoading';

function RosettaEntry(props) {
  return (
    <Authenticator 
      hideDefault={true}
      usernameAttributes="email"
    >
      <ConfirmSignIn />
      <ConfirmSignUp />
      <ForgotPassword />
      <RequireNewPassword />
      <SignIn />
      <SignUp />

      <AmplifyAuthComponent />
    </Authenticator>
  );
}

export default RosettaEntry;

--

To support the second factor, I used the NativeBase cross-platform UI component library, the Redux state management library, its bindings for React and thunk, and the axios networking library to support sending the Cognito JWT, making API endpoint requests, receiving the Rosetta JWT, and then using the Rosetta JWT for subsequent requests. By structuring the React Native application in this way, I was able keep the React Native user interface (UI) components separated from most of the application behavior and to allow them to focus on presenting application state and triggering application behavior. As above, I have stripped out some of the "noise" to allow you to focus in on the requesting of the Rosetta JWT and then the claiming of it. You can look at the details related to this second factor of authentication here.

--

components/RequestTokenRequest.js

--

import { connect } from 'react-redux'
import {
  serverTokenRefreshUserThenRequestToken,
} from '../redux/actions-serverToken'

function RequestTokenRequest(props) {
  const handleSubmitTokenRequest = () => {
    props.serverTokenRefreshUserThenRequestToken();
  }

  return (
      <Container>
        <Header>
          <Body>
            <Title>Request Rosetta token</Title>
          </Body>
        </Header>

        <Content>
          <Form>
            <Item stackedLabel>
              <Label>Token Email</Label>
              <Input editable={false}>{props.requestTokenUserid}</Input>
            </Item>
            <Item stackedLabel>
              <Label>Token Expiration</Label>
              <Input editable={false}>{props.serverTokenExpiration || 'No token'}</Input>
            </Item>
            <Item>
              <Button rounded onPress={() => handleSubmitTokenRequest()} >
                <Text>OK</Text>
              </Button>
            </Item>
          </Form>
        </Content>

        <Footer>
        </Footer>
      </Container>
  );
}

const mapStateToProps = (state /*, ownProps*/) => {
  return {
    requestTokenUserid: state.overallAppReducer.appAmplifyUserIdJwtEmail,
    serverTokenExpiration: state.serverTokenReducer.serverTokenExpiration,
  }
}

const mapDispatchToProps = {
  serverTokenRefreshUserThenRequestToken,
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(RequestTokenRequest)

--

components/RequestTokenClaim.js

--

import { connect } from 'react-redux'
import {
  serverTokenClaimTokenCancel,
  serverTokenRefreshUserThenClaimToken,
  serverTokenSetRequestTokenInfo,
} from '../redux/actions-serverToken'

function RequestTokenClaim(props) {
  const handleCancelTokenClaim = () => {
    props.serverTokenClaimTokenCancel();
  }

  const handleClaimValueChange = (text) => {
    let newRequestTokenInfo = {
      ...props.requestTokenInfo,
      'claimValue': text.trim()
    }
    props.serverTokenSetRequestTokenInfo(newRequestTokenInfo)
  }

  const handleSubmitTokenClaim = () => {
    props.serverTokenRefreshUserThenClaimToken();
  }

  return (
      <Container>
        <Header>
          <Body>
            <Title>Claim Rosetta token</Title>
          </Body>
        </Header>

        <Content>
          <Form>
            <View style={{ padding: 10 }}>
              <Label>Request Identifier</Label>
              <Text numberOfLines={1}>{props.requestId}</Text>
            </View>
            <View style={{ padding: 10 }}>
              <Label>Device Identifier</Label>
              <Text numberOfLines={1}>{props.deviceId}</Text>
            </View>
            <Item stackedLabel>
              <Label>Claim Value (from Email)</Label>
              <Input
                keyboardType="numeric"
                onChangeText={text => handleClaimValueChange(text)}
              />
            </Item>
            <View style={{ padding: 10 }}>
              <Item>
                <Button rounded onPress={() => handleSubmitTokenClaim()} >
                  <Text>OK</Text>
                </Button>
              </Item>
              <Item>
                <Button rounded onPress={() => handleCancelTokenClaim()} >
                  <Text>Cancel</Text>
                </Button>
              </Item>
            </View>
          </Form>
        </Content>

        <Footer>
        </Footer>
      </Container>
  );
}

const mapStateToProps = (state /*, ownProps*/) => {
  return {
    deviceId: state.serverTokenReducer.deviceId,
    requestId: state.serverTokenReducer.claimTokenRequestId,
    requestTokenInfo: state.serverTokenReducer.requestTokenInfo,
  }
}

const mapDispatchToProps = {
  serverTokenClaimTokenCancel,
  serverTokenRefreshUserThenClaimToken,
  serverTokenSetRequestTokenInfo,
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(RequestTokenClaim)

--

Cloud account salt value and password transformation information via Rosetta Salt

Once the client application has obtained a Rosetta JWT on behalf of the user, it can use it when it needs to interact with any of the API endpoints. And, again, the essence of the problem that we want to solve with this client application is to retrieve the relevant cloud account salt value and password transformation information, so that it can derive their password for that cloud account and make it accessible to them so that they can log in to it.

Once the user can see the list of their cloud accounts, they will most often select one of them and then the client application will request the salt value and the password transformation information for it. When they request that specific information, the client application just needs to pass the Rosetta JWT when it interacts with an API endpoint. Below is a stripped down version of this for the case where the client application is getting the information for a particular cloud account. To see more detail related to this, you can find it here.

--

screens/SelectSite.js

--

import { connect } from 'react-redux';
import {
  targetSiteSelectTargetAndPreloadSiteSpec,
} from '../redux/actions-targetSite';

function SiteItem({ siteSpec, onPressHandler }) {
  return (
    <TouchableHighlight
      key={siteSpec.siteId}
      onPress={() => onPressHandler(siteSpec)}
    >
      <View style={{ padding: 20 }}>
        <Text>{siteSpec.siteName}</Text>
      </View>
    </TouchableHighlight>
  );
}

function SelectSite(props) {
  const { navigation } = props;
  
  const handleNext = () => {
    navigation.navigate('CopyInfo');
  }

  const handlePrev = () => {
    navigation.navigate('SetPasscode');
  }

  const handleSelectSite = (siteSpec) => {
    props.targetSiteSelectTargetAndPreloadSiteSpec(siteSpec);
  }

  const isSiteSelected = () => {
    return props.selectedSiteSpec != null && props.selectedSiteSpec.siteId != null;
  }

  // Problem if FlatList enclosed within Content and NativeBase List really slow
  return (
      <Container>
        <Header>
          <Body>
            <Title>{ props.selectedSiteSpec.siteName || 'N/A' }</Title>
          </Body>
        </Header>

        <FlatList
          data={props.siteSpecs}
          renderItem={ ({ item }) => <SiteItem siteSpec={item} onPressHandler={handleSelectSite} /> }
          keyExtractor={ item => item.siteId }
        />

        <Footer>
            <Item>
              <Button onPress={() => handlePrev()} >
                <Text>Prev</Text>
              </Button>
            </Item>
            <Item>
              <Button disabled={!isSiteSelected()} onPress={() => handleNext()} >
                <Text>Next</Text>
              </Button>
            </Item>
        </Footer>
      </Container>
  );
}

const mapStateToProps = (state /*, ownProps*/) => {
  return {
    selectedSiteSpec: state.targetSiteReducer.selectedTargetSiteSpec,
    siteSpecs: state.targetSiteReducer.potentialTargetSiteSpecs,
  }
}

const mapDispatchToProps = {
  targetSiteSelectTargetAndPreloadSiteSpec,
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(SelectSite)

--

Maintain cloud account information via Rosetta Salt

Maintaining the cloud account information via Rosetta Salt is done in much the same way as the cloud account information for a particular cloud account is obtained (see above). The client application "just" needs to gather the relevant information and send it to Rosetta Salt, with the Rosetta JWT. Therefore, I'm not providing any additional detail here; the pattern established above should be sufficient to point you in the right direction.