React Browser 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 browsers 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 and that we want to use serverless computing to make our chosen client-side component available to browsers (if we can). The specific answer to that is that I chose to use the React framework and the JavaScript language to specify and code the client user interface and behavior and the AWS Amplify framework and platform to deploy it and make it highly available for little or no cost through the AWS CloudFront network. Thus, we can develop and deploy a client browser 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, and to this date in 2020, the Angular and React projects emerged and remain as either the two top toolsets for building single page applications in JavaScript, or close to that. Single page applications are a nice alternative to what used to be the standard, server-side generation of web pages, because they allow us to build nicer and more performant client applications that run in the browser and only need to interact with the server for a subset of the user interactions with the application. In my opinion Angular and React are both great options for developing client applications for the browser. However, for this project, I think the React library is a better fit. The client-side application really only needs to support a fairly simple user workflow and thus the Angular framework (which provides a comprehensive framework in contrast to a library) seems like it might be overkill.

And, for the last few years, the AWS Amplify project has supported building and continuously deploying React client applications, according to changes made to a source code repository that it can access, and allowing the hosting of these applications to be free-tier eligible. As an enabler for this, GitHub allows us to create and maintain git source code repositories for free. Therefore using a git repository at github.com and establishing an Amplify project to pull from it provide us with a free or low cost way to deploy our React application and make it available to browsers worldwide. As long as only you or only you and your family are using this client application, there's a good chance that it will remain free.

Problem

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, 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.

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).

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 application, along with a link to a page with more detail. Also, an accompanying YouTube video that shows how to deploy the React application 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 src/App.js file. However, I have stripped out some of the "noise" to allow you to focus in on the components provided by the Amplify project to support this first factor. You can look at the details related to this first factor of authentication here.

In this stripped down version, you can see that the AmplifyAuthenticator can be used to "wrap" the rest of the application (RosettaMain), which means that the first factor of authentication must be completed before moving on. And, you can see that you can customize 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 AmplifyAuthenticator, 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 AmplifyAuthenticator 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).

--

src/App.js

--

import Amplify from "aws-amplify";
import {
  AmplifyAuthenticator,
  AmplifyForgotPassword,
  AmplifySignIn,
  AmplifySignOut,
  AmplifySignUp,
} from "@aws-amplify/ui-react";

import awsconfig from "./config/aws-config";
import RosettaMain from './component/RosettaMain.js';

Amplify.configure(awsconfig);

function App(props) {
  return (
    <AmplifyAuthenticator usernameAlias="email" >
      <AmplifyForgotPassword usernameAlias="email" headerText="Reset your Rosetta Salt password" slot="forgot-password" />
      <AmplifySignIn usernameAlias="email" headerText="Sign in to your Rosetta Salt account" slot="sign-in" />
      <AmplifySignUp usernameAlias="email" headerText="Create a new Rosetta Salt account" slot="sign-up" />
      
      <div className="App">
        <RosettaMain />
        <AmplifySignOut />        
      </div>
    </AmplifyAuthenticator>
  );
}

--

To support the second factor, I used the Material UI 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 application in this way, I was able keep the React 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.

--

src/component/RequestTokenRequest.js

--

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

function RequestTokenRequest(props) {
  const handleSubmitRequestToken = () => {
    props.serverTokenRefreshUserThenRequestToken();
  }
  
  return (
    <React.Fragment>
      <Grid container spacing={3}>
      
        <Grid item xs={12}>
        </Grid>
        <Grid item xs={12}>
          <Typography variant="body1" gutterBottom>
            Congratulations, you have been authenticated by AWS Cognito (phase one).
            Now, request a Rosetta token to authorize your use of this application (phase two).
            If you are licensed for it, you will be sent an email with the information needed to claim it.
          </Typography>
        </Grid>
        
        <Grid item xs={6}>
          <Button
            color='primary' 
            onClick={(event) => handleSubmitRequestToken()}
          >
            OK
          </Button>
        </Grid>
        
      </Grid>
    </React.Fragment>
  );
}

const mapStateToProps = (state /*, ownProps*/) => {
  return {
  }
}

const mapDispatchToProps = {
  serverTokenRefreshUserThenRequestToken
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(RequestTokenRequest)

--

src/component/RequestTokenClaim.js

--

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

function RequestTokenClaim(props) {
  const areInputsValid = () => {
    // guard clause - invalid claim value
    let claimValue = props.requestTokenInfo.claimValue;
    if (!claimValue) {
      return false;
    }
    
    return true;
  }
  
  const handleSubmitClaimToken = () => {
    props.serverTokenRefreshUserThenClaimToken();
  }
  
  return (
    <React.Fragment>
      <Grid container spacing={3}>
      
        <Grid item xs={12}>
        </Grid>
        <Grid item xs={12}>
          <Typography variant="body1" gutterBottom>
            Provide the claim value you received in an email to claim the token.
            Note that you only have a few minutes to complete this claim process.
          </Typography>
        </Grid>
        
        <Grid item xs={6}>
          <Button 
            color='primary' 
            disabled={!areInputsValid()}
            onClick={(event) => handleSubmitClaimToken()}
          >
            OK
          </Button>
        </Grid>
        
      </Grid>
    </React.Fragment>
  );
}

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

const mapDispatchToProps = {
  serverTokenRefreshUserThenClaimToken,
}

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.

--

src/component/ShowSites.js

--

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

function TargetSites(props) {
  const handleSelectSite = (siteSpec, event) => {
    props.targetSiteSelectTargetAndPreloadSiteSpec(siteSpec);
  }

  return (
   <React.Fragment>
    <Grid container spacing={3}>
    
      <Grid item xs={12}>
        <Typography variant="h6" gutterBottom>
          Choose target site
        </Typography>
      </Grid>
    
      <Grid item xs={12}>
        {props.siteSpecs.length > 0 ? (
          <Typography variant="body1" gutterBottom>
            Choose a target site and then click the next button to copy its information. 
            Alternatively, refresh the target sites, add new sites, or delete the selected site from the toolbar.
          </Typography>
        ) : (
          <Typography variant="body1" gutterBottom>
            Add a new site by clicking that action from the toolbar.
          </Typography>
        )}
      </Grid>
        
      <Grid item xs={12}>
        <List className={classes.list}>
        {
          props.siteSpecs.map((siteSpec) =>
            <ListItem 
                button 
                key={siteSpec.siteId} 
                selected={siteSpec.siteId === props.selectedTargetSiteSpec.siteId}
                onClick={(event) => handleSelectSite(siteSpec, event)}
            >
              <ListItemText primary={siteSpec.siteName} />
            </ListItem>
          )
        }
        </List>
      </Grid>

    </Grid>
   </React.Fragment>
 );
}

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

const mapDispatchToProps = { 
  targetSiteSelectTargetAndPreloadSiteSpec,
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TargetSites)

--

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.