AWS

Integrating Amazon Cognito With Single Page Application (Vue.js)

In this article, we will look at authenticating Single page application (built using Vue.js) with Amazon Cognito using OAuth protocol. In our previous article we integrated a server side application with Amazon Cognito.

Scaffolding a Single Page Application

We will use vue-cli to create an empty Vuejs application. Vue CLI can be installed by following the instructions here.

Let’s create an empty application called aws-cognito-spa-demo by issuing the following command:

vue create aws-cognito-spa-demo

You will be prompted to choose the plugins

Select the “default” preset

After the application has been created, you can navigate into that directory and issue a command to run the application

cd aws-cognito-spa-demo
npm instal
npm run serve

You will have the application running at http://localhost:8080

Installing Additional Dependencies

We will install the required node packages which we will use for the application:

npm install --save amazon-cognito-auth-js
npm install --save amazon-cognito-identity-js
npm install --save vue-router
npm install --save axios

Creating New App Client in Amazon Cognito

We will create a new App client called test-spa-client from the Amazon Cognito console as shown below:

New app client for SPA integration

Update the settings for the created client by navigating to “App Client Settings” by providing values for Callback URL, Logout URL, Allowed OAUth flow and OAuth scopes:

We use the Implicit Grant as the OAuth flow for SPA applications.

Creating Environment Variables

We will store the Amazon Cognito related settings in the property files and Vue CLI will make them available among the environment variables during the application’s runtime. More about defining environment variables in Vue JS applications can be found here.

We will store generic application settings like cognito redirect URI, signout URI in .env file and some local settings in .env.local. The .env.*.local and .env.local files are ignored from git. So you dont commit local settings to version control.

# In .env
VUE_APP_COGNITO_REDIRECT_URI=http://localhost:8080/login/oauth2/code/cognito
VUE_APP_COGNITO_REDIRECT_URI_SIGNOUT=http://localhost:8080/logout
VUE_APP_APP_URL=http://localhost:8080

Then the following in .env.local:

VUE_APP_COGNITO_USERPOOL_ID=<cognito userpool id>
VUE_APP_COGNITO_APP_DOMAIN=<cognito app domain>
VUE_APP_COGNITO_CLIENT_ID=<app client id>

Creating User Info Store

We will use a global JSON object for storing the logged in user information. This is an alternate approach to using Vuex. Let’s create the JSON object in src/app/user-info-store.js:

var state = {
  cognitoInfo: {},
  loggedIn: false,
  loadingState: true,
  errorLoadingState: false
}

function setLoggedIn(newValue) {
  state.loggedIn = newValue;
}

function setLoggedOut() {
  state.loggedIn = false;
  state.cognitoInfo = {};
}

function setCognitoInfo(newValue){
  state.cognitoInfo = newValue;
}

export default {
  state: state,
  setLoggedIn: setLoggedIn,
  setLoggedOut: setLoggedOut,
  setCognitoInfo: setCognitoInfo
}

Wrapper for Amazon Cognito API

Let us create a wrapper src/app/auth.js for Amazon Cognito API which will facilitate operations like building the CognitoAuth object, login, logout:

/* eslint-disable */
import {CognitoAuth, StorageHelper} from 'amazon-cognito-auth-js';
import IndexRouter from '../router/index';
import UserInfoStore from './user-info-store';
import UserInfoApi from './user-info-api';


const CLIENT_ID = process.env.VUE_APP_COGNITO_CLIENT_ID;
const APP_DOMAIN = process.env.VUE_APP_COGNITO_APP_DOMAIN;
const REDIRECT_URI = process.env.VUE_APP_COGNITO_REDIRECT_URI;
const USERPOOL_ID = process.env.VUE_APP_COGNITO_USERPOOL_ID;
const REDIRECT_URI_SIGNOUT = process.env.VUE_APP_COGNITO_REDIRECT_URI_SIGNOUT;
const APP_URL = process.env.VUE_APP_APP_URL;

var authData = {
    ClientId : CLIENT_ID, // Your client id here
    AppWebDomain : APP_DOMAIN,
    TokenScopesArray : ['openid', 'email'],
    RedirectUriSignIn : REDIRECT_URI,
    RedirectUriSignOut : REDIRECT_URI_SIGNOUT,
    UserPoolId : USERPOOL_ID,
}

var auth = new CognitoAuth(authData);
auth.userhandler = {
    onSuccess: function(result) {
        console.log("On Success result", result);
        UserInfoStore.setLoggedIn(true);
        UserInfoApi.getUserInfo().then(response => {
            IndexRouter.push('/');
        });
        
        
    },
    onFailure: function(err) {
        UserInfoStore.setLoggedOut();
        IndexRouter.go({ path: '/error', query: { message: 'Login failed due to ' + err } });
    }
};

function getUserInfoStorageKey(){
    var keyPrefix = 'CognitoIdentityServiceProvider.' + auth.getClientId();
    var tokenUserName = auth.signInUserSession.getAccessToken().getUsername();
    var userInfoKey = keyPrefix + '.' + tokenUserName + '.userInfo';
    return userInfoKey;
}

var storageHelper = new StorageHelper();
var storage = storageHelper.getStorage();
export default{
    auth: auth,
    login(){
        auth.getSession();
    },
    logout(){
        if (auth.isUserSignedIn()) {
            var userInfoKey = this.getUserInfoStorageKey();
            auth.signOut();

            storage.removeItem(userInfoKey);
        }
    },
    getUserInfoStorageKey,

}

Getting User Info From Amazon Cognito

After authentication, we can use the access token to obtain information about the user logged in. For this we will have to make a GET request to the End point: https://<app domain>/oauth2/userInfo. We have created an utility method getUserInfo() in src/app/user-info.js as shown below:

import axios from 'axios';
import auth from './auth';


export default{  
    getUserInfo(){
        var jwtToken = auth.auth.getSignInUserSession().getAccessToken().jwtToken;
        const USERINFO_URL = 'https://'+auth.auth.getAppWebDomain() + '/oauth2/userInfo';
        var requestData = {
            headers: {
                'Authorization': 'Bearer '+ jwtToken
            }
        }
        return axios.get(USERINFO_URL, requestData).then(response => { 
            return response.data;
        });
    }
}

This API has been used in the Cognito wrapper written in section above.

Creating Vue Components

Lets create some Vue components for:

  • showing the logged in user information
  • showing log out success
  • error handling component

We will be using Vue Router for mapping the URL path to Vue components. The components definitions are shown below:

Home component

<template>
    <div class="row">
        <div class="col">
            <h3>Welcome, </h3>
            <div class="alert alert-info">
                {{userInfo}}
            </div>
            <router-link to="/logout">
                Logout
            </router-link>
        </div>
    </div>
</template>
<script>
import UserInfoStore from '../app/user-info-store';
export default {
    name: 'Home',
    data: function() {
        return{
            userInfo: UserInfoStore.state.cognitoInfo
        }
    }
}
</script>
<style>
</style>

LogoutSuccess component:

<template>
<div class="row">
    <div class="col">
        <h2>Logged Out successfully</h2>
        <router-link to="/login">Login</router-link>
    </div>
</div>
</template>
<script>
export default {
    mounted: function(){
        
    }
}
</script>

Error component:

<template>
    <div class="alert alert-danger">
        {{message}}
    </div>
</template>
<script>
export default {
    data: function(){
        return {
            message: ""
        }
    },
    mounted(){
        this.message = this.$route.query.message;
    }
}
</script>

Setting Up Router

As mentioned in previous section, we will be using Vue Router to map URL path to Vue components. We will setup the router configuration in router/index.jsas shown below:

/* eslint-disable */
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import auth from '../app/auth';
import LogoutSuccess from '@/components/LogoutSuccess';
import UserInfoStore from '../app/user-info-store';
import UserInfoApi from '../app/user-info-api';
import ErrorComponent from '@/components/Error';

Vue.use(Router)

function requireAuth(to, from, next) {
  
  if (!auth.auth.isUserSignedIn()) {
      UserInfoStore.setLoggedIn(false);
      next({
      path: '/login',
      query: { redirect: to.fullPath }
      });
  } else {
    UserInfoApi.getUserInfo().then(response => {
      UserInfoStore.setLoggedIn(true);
      UserInfoStore.setCognitoInfo(response);
      next();
    });
      
  }
}

export default new Router({
  mode: 'history',
  base: '/',
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home,
      beforeEnter: requireAuth
    },
    {
      path: '/login', beforeEnter(to, from, next){
        auth.auth.getSession();
      }
    },
    {
      path: '/login/oauth2/code/cognito', beforeEnter(to, from, next){
        var currUrl = window.location.href;
        
        //console.log(currUrl);
        auth.auth.parseCognitoWebResponse(currUrl);
        //next();
      }
    },
    {
      path: '/logout', component: LogoutSuccess,  beforeEnter(to, from, next){
        auth.logout();
        next();
      }

    },
    {
      path: '/error', component: ErrorComponent
    }
  ]
})

We are making use of beforeEnter property of routes object to add any pre-requisites required for rendering the component. And in this property we do the check if the user is logged in or not using the Cognito wrapper we had created. So for paths which require to be protected we can define the beforeEnter property.

The default application created has a App.vue component which will be our root component. We make use of the <router-view/> tag to indicate that the HTML here will be based on the component to which the path gets resolved to in the router configuration

So our version of App.vue looks like:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <div class="contents">
      <router-view/>
    </div>
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

We then update the src/main.jsto refer to the directory which contains the router configuration as shown below:

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  router
}).$mount('#app')

Running the Application

You can run the application by issuing the command: npm run serve. Navigating to localhost:8080 will take you to Cognito Login screen:

Enter the username and password of the user you had registered in the user pool or you can even signup for a new user. After login you will be redirected back to Vue JS app:

Authenticated User information

The Logout link will log the user out.

The complete code can be found in the Github repo here.

Categories: AWS, cognito, vuejs

Tagged as: , ,

3 replies »

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.