React Rosetta Token

How can we obtain a Rosetta token during the second factor of authentication?

These code snippets demonstrate how the React application obtains a Rosetta JWT that can then be used to interact with the API endpoints. First, the RequestTokenRequest component and its supporting "props" give the user the opportunity to request the Rosetta JWT from the server. Then, the RequestTokenClaim component and its supporting "props" give the user the opportunity to claim the Rosetta JWT, after they have received the claim code in their email.

--

src/component/RequestTokenRequest.js

--

import React from 'react';

import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';

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={12}>
          <TextField
            autoFocus
            disabled
            id="requestTokenDialogUserId"
            name="requestTokenDialogUserId"
            label="Token Email"
            fullWidth
            type="email"
            value={props.requestTokenUserid}
          />
        </Grid>
        {props.serverTokenExpiration && (
          <Grid item xs={12}>
            <TextField
              autoComplete="server-token-expiration"
              disabled
              id="serverTokenExpiration"
              name="serverTokenExpiration"
              label="Token Expiration"
              fullWidth
              type="text"
              value={props.serverTokenExpiration}
            />
          </Grid>
        )}
        
        {props.appAmplifyUserError && (
          <Grid item xs={12}>
            <TextField
              autoComplete="amplify-user-error"
              disabled
              id="amplifyUserError"
              name="amplifyUserError"
              label="Amplify User Error"
              fullWidth
              type="text"
              value={props.appAmplifyUserError}
            />
          </Grid>
        )}
        {props.requestTokenError && (
          <Grid item xs={12}>
            <TextField
              autoComplete="server-token-request-error"
              disabled
              id="serverTokenRequestError"
              name="serverTokenRequestError"
              label="Request Token Error"
              fullWidth
              type="text"
              value={props.requestTokenError}
            />
          </Grid>
        )}
        
        <Grid item xs={6}>
          <Button
            color='primary' 
            onClick={(event) => handleSubmitRequestToken()}
          >
            OK
          </Button>
        </Grid>
        
      </Grid>
    </React.Fragment>
  );
}

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

const mapDispatchToProps = {
  serverTokenRefreshUserThenRequestToken
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(RequestTokenRequest)

--

src/component/RequestTokenClaim.js

--

import React from 'react';

import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';

import { connect } from 'react-redux'
import {
  serverTokenClaimTokenCancel,
  serverTokenRefreshUserThenClaimToken,
  serverTokenSetRequestTokenInfo
} 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 handleCancelClaimToken = () => {
    props.serverTokenClaimTokenCancel();
  }
  
  const handleSubmitClaimToken = () => {
    props.serverTokenRefreshUserThenClaimToken();
  }
  
  const handleRequestTokenClaimValueChange = (event) => {
    let newRequestTokenInfo = {
      ...props.requestTokenInfo,
      'claimValue': event.target.value.trim()
    }
    props.serverTokenSetRequestTokenInfo(newRequestTokenInfo)
  }
  
  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={12}>
          <TextField
            disabled
            id="claimTokenRequestIdentifier"
            name="claimTokenRequestIdentifier"
            label="Request Identifier"
            fullWidth
            type="text"
            value={props.claimTokenRequestId}
          />
        </Grid>
        <Grid item xs={12}>
          <TextField
            disabled
            id="claimTokenDeviceIdentifier"
            name="claimTokenDeviceIdentifier"
            label="Device Identifier"
            fullWidth
            type="text"
            value={props.deviceId}
          />
        </Grid>
        <Grid item xs={12}>
          <TextField
            autoFocus
            id="claimTokenClaimValue"
            name="claimTokenClaimValue"
            label="Claim Value (from Email)"
            fullWidth
            onChange={(event) => handleRequestTokenClaimValueChange(event)}
            type="text"
            value={props.requestTokenInfo.claimValue}
          />
        </Grid>
        
        {props.appAmplifyUserError && (
          <Grid item xs={12}>
            <TextField
              autoComplete="amplify-user-error"
              disabled
              id="amplifyUserError"
              name="amplifyUserError"
              label="Amplify User Error"
              fullWidth
              type="text"
              value={props.appAmplifyUserError}
            />
          </Grid>
        )}
        {props.claimTokenError && (
          <Grid item xs={12}>
            <TextField
              autoComplete="server-token-claim-error"
              disabled
              id="serverTokenClaimError"
              name="serverTokenClaimError"
              label="Claim Token Error"
              fullWidth
              type="text"
              value={props.claimTokenError}
            />
          </Grid>
        )}
        {props.installTokenError && (
          <Grid item xs={12}>
            <TextField
              autoComplete="server-token-install-error"
              disabled
              id="serverTokenInstallError"
              name="serverTokenInstallError"
              label="Install Token Error"
              fullWidth
              type="text"
              value={props.installTokenError}
            />
          </Grid>
        )}
        
        <Grid item xs={6}>
          <Button 
            color='primary' 
            disabled={!areInputsValid()}
            onClick={(event) => handleSubmitClaimToken()}
          >
            OK
          </Button>
        </Grid>
        <Grid item xs={6}>
          <Button onClick={(event) => handleCancelClaimToken()}>
            Cancel
          </Button>
        </Grid>
        
      </Grid>
    </React.Fragment>
  );
}

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

const mapDispatchToProps = {
  serverTokenClaimTokenCancel,
  serverTokenRefreshUserThenClaimToken,
  serverTokenSetRequestTokenInfo
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(RequestTokenClaim)

--

src/redux/actions.js

--

import {
  OVERALL_APP_GET_USER_ERROR,
  OVERALL_APP_GET_USER_STARTED,
  OVERALL_APP_GET_USER_SUCCESS,
  SERVER_TOKEN_CLAIM_TOKEN_CANCEL,
  SERVER_TOKEN_CLAIM_TOKEN_ERROR,
  SERVER_TOKEN_CLAIM_TOKEN_STARTED,
  SERVER_TOKEN_CLAIM_TOKEN_SUCCESS,
  SERVER_TOKEN_REQUEST_TOKEN_ERROR,
  SERVER_TOKEN_REQUEST_TOKEN_STARTED,
  SERVER_TOKEN_REQUEST_TOKEN_SUCCESS,
  SERVER_TOKEN_SET_REQUEST_TOKEN_INFO,
} from './actionTypes'

import { Auth } from 'aws-amplify';
import axios from 'axios'

export function commonCreateAxiosClientWithAmplifyToken(state) {
  const axiosClient = axios.create();
  axiosClient.defaults.baseURL = state.serverTokenReducer.serverBaseUri;
  axiosClient.defaults.headers.common['Authorization'] = "Bearer " + state.overallAppReducer.appAmplifyUserIdJwtToken;
  axiosClient.defaults.headers.common['X-Device-Id'] = state.serverTokenReducer.deviceId;
  return axiosClient;
}

export function overallAppGetUserError(errorResponse) {
  return { type: OVERALL_APP_GET_USER_ERROR, errorResponse }
}

export function overallAppGetUserStarted() {
  return { type: OVERALL_APP_GET_USER_STARTED }
}

export function overallAppGetUserSuccess(appAmplifyUser) {
  return { type: OVERALL_APP_GET_USER_SUCCESS, appAmplifyUser }
}

export function serverTokenClaimToken() {
  return (dispatch, getState) => {
    dispatch(serverTokenClaimTokenStarted());
    const axiosClient = commonCreateAxiosClientWithAmplifyToken(getState());
    const reducer = getState().serverTokenReducer;
    const tokenClaimInfo = {
      'deviceId': reducer.requestTokenInfo.deviceId,
      'claimValue': reducer.requestTokenInfo.claimValue,
    }
    axiosClient
      .patch("/api/token-requests/" + reducer.claimTokenRequestId, tokenClaimInfo)
      .then(res => { dispatch(serverTokenClaimTokenSuccess(res.data)) })
      .catch(err => { dispatch(serverTokenClaimTokenError(err.response)) });
  }
}

export function serverTokenRefreshUserThenClaimToken() {
  return (dispatch, getState) => {
    dispatch(overallAppGetUserStarted());
    Auth.currentAuthenticatedUser( { bypassCache: false } )
    .then(user => {
      dispatch(overallAppGetUserSuccess(user));
      dispatch(serverTokenClaimToken());
    })
    .catch(err => { dispatch(overallAppGetUserError(err.response)); });
  }
}

export function serverTokenRequestToken() {
  return (dispatch, getState) => {
    dispatch(serverTokenRequestTokenStarted());
    const axiosClient = commonCreateAxiosClientWithAmplifyToken(getState());
    const appReducer = getState().overallAppReducer;
    const tokenReducer = getState().serverTokenReducer;
    const tokenRequestInfo = {
      'deviceId': tokenReducer.requestTokenInfo.deviceId,
      'userId': appReducer.appAmplifyUserIdJwtEmail,
    }
    axiosClient
      .post("/api/token-requests", tokenRequestInfo)
      .then(res => { dispatch(serverTokenRequestTokenSuccess(res.data)) })
      .catch(err => { dispatch(serverTokenRequestTokenError(err.response)) });
  }
}

export function serverTokenRefreshUserThenRequestToken() {
  return (dispatch, getState) => {
    dispatch(overallAppGetUserStarted());
    Auth.currentAuthenticatedUser( { bypassCache: false } )
    .then(user => {
      dispatch(overallAppGetUserSuccess(user));
      dispatch(serverTokenRequestToken());
    })
    .catch(err => { dispatch(overallAppGetUserError(err.response)); });
  }
}

export function serverTokenClaimTokenCancel() {
  return { type: SERVER_TOKEN_CLAIM_TOKEN_CANCEL }
}

export function serverTokenClaimTokenError(errorResponse) {
  return { type: SERVER_TOKEN_CLAIM_TOKEN_ERROR, errorResponse }
}

export function serverTokenClaimTokenStarted() {
  return { type: SERVER_TOKEN_CLAIM_TOKEN_STARTED }
}

export function serverTokenClaimTokenSuccess(bearerToken) {
  return { type: SERVER_TOKEN_CLAIM_TOKEN_SUCCESS, bearerToken }
}

export function serverTokenRequestTokenError(errorResponse) {
  return { type: SERVER_TOKEN_REQUEST_TOKEN_ERROR, errorResponse }
}

export function serverTokenRequestTokenStarted() {
  return { type: SERVER_TOKEN_REQUEST_TOKEN_STARTED }
}

export function serverTokenRequestTokenSuccess(tokenRequestId) {
  return { type: SERVER_TOKEN_REQUEST_TOKEN_SUCCESS, tokenRequestId }
}

export function serverTokenSetRequestTokenInfo(requestTokenInfo) {
  return { type: SERVER_TOKEN_SET_REQUEST_TOKEN_INFO, requestTokenInfo }
}

--

src/redux/reducers.js

--

import {
  SERVER_TOKEN_CLAIM_TOKEN_CANCEL,
  SERVER_TOKEN_CLAIM_TOKEN_ERROR,
  SERVER_TOKEN_CLAIM_TOKEN_STARTED,
  SERVER_TOKEN_CLAIM_TOKEN_SUCCESS,
  SERVER_TOKEN_REQUEST_TOKEN_ERROR,
  SERVER_TOKEN_REQUEST_TOKEN_STARTED,
  SERVER_TOKEN_REQUEST_TOKEN_SUCCESS,
  SERVER_TOKEN_SET_REQUEST_TOKEN_INFO,
} from './actionTypes'

export function commonGetErrorMessage(errorResponse) {
    let errorMessage = "Unknown error";
    if (errorResponse && errorResponse.data && errorResponse.data.Message) {
      errorMessage = errorResponse.data.Message
    }
    return errorMessage;
}

function serverTokenInstallToken(tokenValue) {
  // guard clause - invalid token value
  if (tokenValue == null) {
    return 'Invalid token value (should be in JWT format (header.payload.signature)';
  }
  
  // guard clause - invalid token length
  let tokenString = tokenValue.toString();
  if (tokenString.length > 1024) {
    return 'Invalid token value (should be in JWT format (header.payload.signature)';
  }
  
  // guard clause - invalid token structure
  let tokenParts = tokenString.split('.');
  if (tokenParts.length !== 3) {
    return 'Invalid token value (should be in JWT format (header.payload.signature)';
  }

  try {
    let payloadBase64 = tokenParts[1];
    let payloadString = atob(payloadBase64);
    let payloadJson = JSON.parse(payloadString);

    let tokenId = payloadJson['identifier'];
    let tokenExpirationSeconds = parseFloat(payloadJson['expiration']);
    let tokenExpirationDate = new Date();
    tokenExpirationDate.setTime(tokenExpirationSeconds * 1000);
    let tokenExpirationString = tokenExpirationDate.toString();
    
    localStorage.serverTokenValue = tokenValue;
    localStorage.serverTokenIdentifier = tokenId;
    localStorage.serverTokenExpiration = tokenExpirationString;
    localStorage.serverTokenExpirationSeconds = tokenExpirationSeconds;
    
    return '';
  } catch (error) {
    return 'Unable to extract payload from JWT (should be in JWT format (header.payload.signature)';
  }
}

const serverTokenInitialState = {
  claimTokenError: '',
  claimTokenRequestId: '',
  
  deviceId: serverTokenPersistentDeviceId || '',
  
  installTokenError: '',
  
  requestTokenError: '',
  requestTokenInfo: {
    deviceId: serverTokenPersistentDeviceId || '',
    durationHours: 10,
    claimValue: '',
  },
  
  serverBaseUri: "https://api.rosettasalt.org",
  
  serverTokenIdentifier: localStorage.serverTokenIdentifier || '',
  serverTokenExpiration: localStorage.serverTokenExpiration || '',
  serverTokenExpirationSeconds: localStorage.serverTokenExpirationSeconds || 0,
  serverTokenExpired: isTokenExpired(localStorage.serverTokenExpirationSeconds || 0),
  serverTokenValue: localStorage.serverTokenValue || '',
}

export function serverTokenReducer(state = serverTokenInitialState, action) {
  let errorMessage = "";
  switch (action.type) {
    case SERVER_TOKEN_CLAIM_TOKEN_CANCEL:
      return { 
        ...state, 
        claimTokenRequestId: '',
      }
    case SERVER_TOKEN_CLAIM_TOKEN_ERROR:
      errorMessage = commonGetErrorMessage(action.errorResponse);
      console.log("Claim token error: " + errorMessage)
      return { 
        ...state, 
        claimTokenError: errorMessage
      }
    case SERVER_TOKEN_CLAIM_TOKEN_STARTED:
      return { 
        ...state, 
        claimTokenError: '',
        installTokenError: '',
      }
    case SERVER_TOKEN_CLAIM_TOKEN_SUCCESS:
      let claimInstallError = serverTokenInstallToken(action.bearerToken);
      return { 
        ...state, 
        installTokenError: claimInstallError,
        serverTokenIdentifier: localStorage.serverTokenIdentifier || '',
        serverTokenExpiration: localStorage.serverTokenExpiration || '',
        serverTokenExpirationSeconds: localStorage.serverTokenExpirationSeconds || 0,
        serverTokenValue: localStorage.serverTokenValue || '',
      }
    case SERVER_TOKEN_REQUEST_TOKEN_ERROR:
      errorMessage = commonGetErrorMessage(action.errorResponse);
      console.log("Request token error: " + errorMessage);
      return { 
        ...state, 
        requestTokenError: errorMessage
      }
    case SERVER_TOKEN_REQUEST_TOKEN_STARTED:
      let newRequestTokenInfo = {
        deviceId: serverTokenPersistentDeviceId || '',
        durationHours: 10,
        claimValue: '',
      };
      return { 
        ...state, 
        claimTokenError: '',
        requestTokenError: '',
        requestTokenInfo: newRequestTokenInfo
      }
    case SERVER_TOKEN_REQUEST_TOKEN_SUCCESS:
      return { 
        ...state, 
        claimTokenRequestId: action.tokenRequestId,
      }
    case SERVER_TOKEN_SET_REQUEST_TOKEN_INFO:
      return { 
        ...state, 
        requestTokenInfo: action.requestTokenInfo
      }
    default:
      return state
  }
}

--