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
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:
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
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..env
# 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]
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.js
as 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.js
to 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:
The Logout link will log the user out.
The complete code can be found in the Github repo here.