Setting up AWS Cognito and React Native to enable Federated SSO: Part 2

Setting up AWS Cognito and React Native to enable Federated SSO: Part 2
|

Part Two: The Frontend

 

Introduction

 

Previously we looked at how to set up Amazon Cognito User Pools to work with federated Single Sign On (SSO). This is important for tracking data and users consistently across applications. However, this feature is currently only available through the Hosted UI provided to us by Amazon.

The problem we now face is that the redirection to the UI is ugly and provides an unsuitable user experience. Next, we attempt to improve the user experience without compromising on the functionality.

 

Basic App Setup

 

At The Distance we implement a React-Navigation authenticated stack as part of The Core framework. This allows us to control user flow and authentication. This, however, is out of scope for this blog and as such we are going to use a simpler method.

 

Installation

 

I am going to presume that you have React Native set up. If not, head on over to the React-Native Website.

Let’s generate a new project. Head over to the directory you want to generate the app in from the terminal and run:

$ react-native init CognitoExample

This will generate a project with the default React Native boilerplate. Create a new folder named ‘/Screens’ inside the ‘/src’ folder. In this folder we need a AuthenticatedScreen.js, SignInScreen.android.js  and a SignInScreen.ios.js file. We will handle our sign up differently for both platforms.

Next create a new folder named ‘/Utils’ also inside ‘/src’. Create a new file named awsConfiguration.js.

Before we continue building our screens, we need to add our dependencies. In the project root folder run:

$ yarn add aws-amplify amazon-cognito-identity-js react-native-inappbrowser-reborn amazon-cognito-auth-js

$ react-native link react-native-inappbrowser-reborn

I’m not going to focus on styling in this guide, I’ll leave that up to you, but I will add some colour. First up, start with the AWSConfiguration.

Here we will need some of the details from Part One of this blog. 

import { StorageHelper } from "@aws-amplify/core";

const storage = new StorageHelper().getStorage();

const awsConfiguration = {
  Auth: {
    region: ”eu-west-2”,
    userPoolId: ”eu-west-2_HTBSYrv6l”,
    userPoolWebClientId: ”7snsn7e3t77h0uennvevq1tgb8”,
    storage
  }
};

export default awsConfiguration;

Above is my User Pool configuration. Note here that we manually set up our storage object. This is important so that when we use amazon-cognito-identity-js later, not aws-amplify, our users are stored in the correct place. We will configure Amplify later.

Next, set up our SignInScreen screens. This file will be the default file for our app when not authenticated. 

 

Building the screens

 

Recently Apple has employed a stance on social login that WKWebView is not secure enough. Instead we must use SafariViewController which is available to us through react-native-InAppBrowser-reborn.

First, the imports and some basic formatting:

import React from ‘react’;
Import { View, SafeAreaView, Text, TouchableOpacity, Linking } from ‘react-native’;
import InAppBrowser from 'react-native-inappbrowser-reborn’;
Import { Auth } from ‘aws-amplify’;
import { CognitoAuth } from "amazon-cognito-auth-js";

const settings = {
  container: {
    style: {
      flex: 1,
      backgroundColor: '#631D76',
      justifyContent: 'center',
      alignItems: 'center'
    }
  },
  button: {
    style: {
      borderWidth: 2,
      borderColor: '#832161',
      borderRadius: 10,
      width: '80%',
      height: 50,
      alignItems: 'center',
      justifyContent: 'center',
      backgroundColor: '#832161',
      shadowColor: '#000',
      shadowOffset: {
        width: 0,
        height: 2
      },
      shadowOpacity: 0.25,
      shadowRadius: 3.84,

      elevation: 5
    }
  },
  buttonText: {
    style: {
      color: 'white'
    }
  },
  title: {
    style: {
      color: 'white',
      fontSize: 28,
      fontWeight: '600',
      textAlign: 'center',
      marginBottom: 150
    }
  }
};

class SignInScreen extends React.Component {
  render() {
    return (
      <SafeAreaView {...settings.container}>
        <View>
          <Text {...settings.title}>Cognito Authentication Example</Text>
        </View>
        <TouchableOpacity {...settings.button}>
          <Text {...settings.buttonText}>I'm a button</Text>
        </TouchableOpacity>
      </SafeAreaView>
    );
  }
}

export default SignInScreen;

Add the same to SignInScreen.android.js.

With the design of our SignInScreens done, lets add them to our App.js file and configure the screens and AWS to handle our authentication. First, clear out the App.js file and add the SignInScreen so it looks like this:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow
 */

import React from "react";
import SignInScreen from "./src/Screens/SignInScreen";
import awsConfiguration from './src/Utils/awsConfiguration';
import Amplify from 'aws-amplify';

const App = () => {
  return <SignInScreen />;
};

export default App;

 

Note that I added two new imports to the file. We must configure amplify to use our AWS configuration object. Above the return block add:

Amplify.configure(awsConfiguration);

Now I’m going to implement a simple switch for unauthenticated and authenticated users. Replace the App function to look like:

class App extends React.Component {
  state = {
    authenticated: false
  };

  render() {
    if (this.state.authenticated) {
      return <Authenticated />;
    }
    return <SignInScreen toAuthenticated={() => this.setState({ authenticated: true })} />;
   }
}

Here we’ve created a state field named authenticated and based on this property we are displaying one of two files. Because we currently only have a SignInScreen this is set to false. The other thing to note here is that we have passed a prop to SignInScreen which sets authenticated to true.

Let’s actually handle the authentication. As mentioned previously, Apple now requires us to use SafariViewController and in turn react-native-InAppBrowser-reborn. We will focus on iOS as Android can be achieved in a number of different manners.

In SignInScreen.ios.js add the code to handle redirection to the app. We will need an App URL Scheme. To set this up:

  • Edit the Info.plist
  • Add ‘URL types’ as a row
  • Add a row to ‘URL types’ called ‘URL Identifier’. This is usually the same as your bundle id
  • Add another row to ‘URL types’ called ‘URL Schemes’.
  • Add an item as the name of your scheme. E.g. myapp will be used as myapp://

First we need to handle the redirection. This is done in componentDidMount() by adding listeners for AppState changes.

componentDidMount() {
    // this handles the case where the app is closed and is launched via Universal Linking.
    Linking.getInitialURL()
      .then(url => {
        console.log("I am in the get initial url");
        if (url) {
          Alert.alert("GET INIT URL", `initial url  ${url}`);
          console.log("GET INIT URL", `initial url  ${url}`);
          // this.resetStackToProperRoute(url);
        }
      })
      .catch(e => {});

    // This listener handles the case where the app is woken up from the Universal or Deep Linking
    Linking.addEventListener("url", this.appWokeUp);
}

componentWillUnmount() {
    Linking.removeEventListener("url", this.appWokeUp);
  }

We now require a number of different functions to handle sign in and redirection from cognito. First of all, we are going to open the Hosted UI directly on Facebook’s log in screen. Add this code to the onPress of the TouchableOpacity.

Adding The Web Browser

 

() => InAppBrowser

   .open(`https://example-sso.auth.eu-west-2.amazoncognito.com/login?redirect_uri=http://localhost:3000&response_type=code&client_id=7snsn7e3t77h0uennvevq1tgb8&identity_provider=Facebook`)
   .catch(error => {console.log(error);})

 

This is the same URL we generated at the end of Part One however we have added the identity_provider=Facebook property to the end.

At this point the file should look like this:

import React from 'react';
import { View, SafeAreaView, Text, TouchableOpacity, Linking } from 'react-native';
import InAppBrowser from 'react-native-inappbrowser-reborn';
import { Auth } from 'aws-amplify';
import { CognitoAuth } from 'amazon-cognito-auth-js';

const settings = {
  container: {
    style: {
      flex: 1,
      backgroundColor: '#631D76',
      justifyContent: 'center',
      alignItems: 'center'
    }
  },
  button: {
    style: {
      borderWidth: 2,
      borderColor: '#832161',
      borderRadius: 10,
      width: '80%',
      height: 50,
      alignItems: 'center',
      justifyContent: 'center',
      backgroundColor: '#832161',
      shadowColor: '#000',
      shadowOffset: {
        width: 0,
        height: 2
      },
      shadowOpacity: 0.25,
      shadowRadius: 3.84,

      elevation: 5
  },
  onPress: () =>
      InAppBrowser.open(

`https://example-sso.auth.eu-west-2.amazoncognito.com/login?redirect_uri=http://localhost:3000&response_type=code&client_id=7snsn7e3t77h0uennvevq1tgb8&identity_provider=Facebook`
      ).catch(error => {
        console.log(error);
      })
  },
  buttonText: {
    style: {
      color: 'white'
    }
  },
  title: {
    style: {
      color: 'white',
      fontSize: 28,
      fontWeight: '600',
      textAlign: 'center',
      marginBottom: 150
    }
  }
};

class SignInScreen extends React.Component {
  render() {
    return (
      <SafeAreaView {...settings.container}>
        <View>
          <Text {...settings.title}>Cognito Authentication Example</Text>
        </View>
        <TouchableOpacity {...settings.button}>
          <Text {...settings.buttonText}>Login With Facebook</Text>
        </TouchableOpacity>
      </SafeAreaView>
    );
  }
}

export default SignInScreen;

To set up Cognito we need to Cognito Auth which is not provided to us by Amplify. Above our class under the imports add:

/* Social Login Setup */

CognitoAuth.prototype.createCORSRequest = function (method, url) {
    const xhr = new window.XMLHttpRequest();
    xhr.open(method, url, true);
    return xhr;
};

if (!global.atob) {
    global.atob = base64.decode;
}

const cognitoAuthParams = {
    ClientId: awsConfiguration.Auth.userPoolWebClientId,
    UserPoolId: awsConfiguration.Auth.userPoolId,
    AppWebDomain: "example-sso.auth.eu-west-2.amazoncognito.com",
    TokenScopesArray: [
        "email",
        "profile",
        "openid",
        "aws.cognito.signin.user.admin"
    ],
    RedirectUriSignIn: "myapp://",
    RedirectUriSignOut: "myapp://",
    Storage: awsConfiguration.Auth.storage
};

Then in the constructor of the class add:

this.cognitoAuthClient = new CognitoAuth(cognitoAuthParams);
    this.cognitoAuthClient.userhandler = {
      onSuccess: async result => {
        this.handleSignInSuccess();
      },
      onFailure: err => {
        console.log(err, "Sign in error");
      }
    };

Handling Redirection

 

Next add these functions:

1.

appWokeUp = event => {
   // this handles the use case where the app is running in the background and is activated by the listener...
   // Alert.alert("Linking Listener", `url  ${event.url}`);
   InAppBrowser.close();
   console.log(event);
   this.handleBrowserNavigation(event);
 };

2.

handleBrowserNavigation = webViewState => {
   console.log(webViewState);
   const formattedURL = webViewState.url.split(
     "myapp://?code"
   )[1];
   this.handleSocialSignIn(formattedURL);
   // if (webViewState.url.includes("?code")) {
   //   this.handleWebViewClose();
   // }
 };

3.

handleSocialSignIn = async callbackURL => {
   const connected = await this.isConnected();
   if (connected !== true) {
     this.handleError("Network");
     return;
   }
   // console.log(Auth._config);
   try {
     const AmplifyUser = await Auth.currentAuthenticatedUser();
     console.log("\nUSER ALREADY LOGGED IN");

     this.handleSignInSuccess();

     return AmplifyUser;
   } catch (e) {
     try {
       console.log(callbackURL);
       await this.cognitoAuthClient.parseCognitoWebResponse(callbackURL);
       try {
         await this.handleSignInSuccess();
       } catch (err) {
         if (this.state.retry === false) {
           this.setState({
             retry: true
           });
           this.handleSocialSignIn();
         } else {
           throw err;
         }
       }
     } catch (err) {
       if (this.state.retry === false) {
         this.setState({
           retry: true
         });
         this.handleSocialSignIn();
       }
       console.log(err, "HANDLESOCIALLOGIN ERROR");
       console.log(AsyncStorage.getAllKeys(), "Async");
       return false;
     }
   }
 };

4.

handleSignInSuccess = async () => {
   console.log("Sign in success");
   this.props.toAuthenticated()
}

 

Updating Cognito to match URL Scheme

And that’s it for the SignInScreen. The only thing we need to do is update our redirectURL’s in Cognito as below:

Now we have successfully updated and redirected the user to the Authentication page we get an error that the Authentication doesn’t exist. You can do whatever you like with this page. I suggest creating a simple component to test and then experiment with the data that can be returned from Auth in creating a profile page.

 

Article by Joseph Clough, React Developer at The Distance