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

Update

I have reviewed this post based on the comments and I have fixed ambiguity in the same. Please refer to the sample code and remember to replace the values in the `.env.local` (https://github.com/sanaulla123/samples/blob/master/aws-cognito-spa-demo/.env.local) file with those which are available in your user pool and app client settings. 

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 a User Pool

In Amazon Cognito, the user pool is a directory of users that are used for authentication by your application. These can be users created in the user pool or authenticating via integration with other authentication providers like Facebook, Google, and the likes. 

When you open the Amazon Cognito service page, you will be shown the below:

From here you have to click on “Manage User Pools” to view the user pools you have already created and to create new user pools. You will see the user pools as well as a button to create new user pool as shown below:

Clicking on the “Create a user pool” will take you to the page where you can provide the user pool name and then keep the default settings to set up your new user pool.

Creating New App Client in Amazon Cognito

App clients are created from the user pool detail page you created before. 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 the file, and some local settings in .env.local. The .env.*.local and .env.local files are ignored from git. So you don’t 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:

#In .env.local file
VUE_APP_COGNITO_USERPOOL_ID=<cognito userpool id> [This is the name of the user pool. In my case test-spa-client] VUE_APP_COGNITO_APP_DOMAIN=<cognito app domain> [this can be found from the Domain name settings as shown in below image. Enter here without https://] VUE_APP_COGNITO_CLIENT_ID=<app client id>[this can be found from the app client setting page as shown in the image below]
Amazon Cognito Domain Name

 

Amazon Cognito 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 Endpoint: https://<app domain>/oauth2/userInfo. We have created a utility method getUserInfo() in src/app/user-info-api.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 the section above.

Creating Vue Components

Let’s 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 the previous section, we will be using Vue Router to map URL path to Vue components. We will set up 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.

23 thoughts on “Integrating Amazon Cognito With Single Page Application (Vue.js)”

    • this is very late but I felt the need to put it here.
      The file user-info.js should be named user-info-api.js

      Reply
  1. It seems to me like the call to auth.getSession() is taking me to an invalid URL. I’m seeing “https//” without a colon in my browser.

    Reply
    • In your .env.local file, you have this entry:

      VUE_APP_COGNITO_APP_DOMAIN=

      When you add the URL here, leave out “https://” as auth.getSession() will add it for you. This is the format the URL should be in.

      VUE_APP_COGNITO_APP_DOMAIN=example.com

      Reply
  2. why do i get this back in the console…
    Uncaught Error: The parameters: App client Id, App web domain, the redirect URL when you are signed in and the redirect URL when you are signed out are required.

    Reply
  3. Uncaught Error: The parameters: App client Id, App web domain, the redirect URL when you are signed in and the redirect URL when you are signed out are required.

    this isn’t how i expected the tut to end 🙁

    Reply
    • Following up on this, I got the same error when I mistakenly had .env and .env.local in /src rather than the root.

      Reply
    • I experienced this issue as well and it was because my .env file was INSIDE of the `src` directory.
      Both – the .env and .env.local files need to be outside the src directory.

      |- public
      |- src
      |- .env
      |- .env.local

      If you are confused, you can check out the git repo shared in the post to see where the .env file is placed. Hope this helps incase you are new to the vue-cli and getting started!

      Reply
    • Sorry for that. The file .env.local was not committed as it contained the app settings of my Cognito User pool. But I have masked the changes and uploaded the file. Those properties have to be defined in the file .env.local in your app root directory

      Reply
  4. Hey – have you tried deploying this?
    Have this working great in local host, but once deployed I get stuck with the following issues:
    If I have the following Callback URL
    https:///login/oauth2/code/cognito

    I get a 404 error on the redirect as it’s a direct redirect from Cognito and won’t find the router:
    https://router.vuejs.org/guide/essentials/history-mode.html#example-server-configurations

    Seems like this is something that needs to be handled at the server level:
    https://stackoverflow.com/questions/36399319/vue-router-return-404-when-revisit-to-the-url

    Any suggestions? Trying to put this up in an S3 bucket so don’t have a ton of config options.

    Reply
  5. I would like to know, how I can call this app in ec2 instance, not by ip but by ec2-34-221-67-52.us-west-2.compute.amazonaws.com, when I call by ip it’s ok , but when I call by public dns the app return invalid host header

    Reply
  6. Its a really nice example. Works perfectly when I run locally (as localhost). The redirect works from cognito (http://localhost:8080/login/oauth2/code/cognito).

    However, it fails when I deploy on my hosted domain. The redirect fails (post login) with a 403 – /login/oauth2/code/cognito

    If I change it to point to “index.html”, the redirect works. However, the “app.js” code does not get execute so I don’t have access to the token etc.

    Not sure what i’m doing wrong. Any help would be appreciated. thanks.

    Reply
  7. Nice example!! I’m using it on an internal website of my work.

    I would like to know if, is there a way to get the cognito usergroup on the login and store it, just like the username?
    Currently, I get it using auth.auth.getSignInUserSession().getIdToken().payload[“cognito:groups”][0] in my vue file, but that’s slow and throws error about rendering before the data is loaded, and have to refresh the page.

    Regards!!

    Reply

Leave a Reply

Discover more from Experiences Unlimited

Subscribe now to keep reading and get access to the full archive.

Continue reading