Securing a Vue.js app using OpenID Connect Code Flow with PKCE and IdentityServer4

This article shows how to setup a Vue.js SPA application to authenticate and authorize using OpenID Connect Code flow with PKCE. This is good solution when implementing SPA apps requesting data from APIs on separate domains. The oidc-client-js npm package is used to implement the client side authentication logic and validation logic. IdentityServer4 and ASP.NET Core Identity are used to implement the secure token server.

Code https://github.com/damienbod/IdentityServer4VueJs

Other posts in this series

Building and securing an ASP.NET Core API with a hosted Vue.js UI

IdentityServer4 Client configuration

The secure token server was implemented using IdentityServer4 with ASP.NET Core Identity and an Entity Framework Core database. A client configuration was added for the Vue.js application. This configures the code flow with PKCE and supports the callback and the silent-renew redirects.

new Client
{
	ClientName = "vuejs_code_client",
	ClientId = "vuejs_code_client",
	AccessTokenType = AccessTokenType.Reference,
	// RequireConsent = false,
	AccessTokenLifetime = 330,// 330 seconds, default 60 minutes
	IdentityTokenLifetime = 300,

	RequireClientSecret = false,
	AllowedGrantTypes = GrantTypes.Code,
	RequirePkce = true,

	AllowAccessTokensViaBrowser = true,
	RedirectUris = new List<string>
	{
		"https://localhost:44357",
		"https://localhost:44357/callback.html",
		"https://localhost:44357/silent-renew.html"
	},
	PostLogoutRedirectUris = new List<string>
	{
		"https://localhost:44357/",
		"https://localhost:44357"
	},
	AllowedCorsOrigins = new List<string>
	{
		"https://localhost:44357"
	},
	AllowedScopes = new List<string>
	{
		"openid",
		"dataEventRecords",
		"dataeventrecordsscope",
		"role",
		"profile",
		"email"
	}

Vue.js Client setup

The Vue.js client is implemented using Vue.js CLI to create a new typescript application. The STS is configured to run with HTTPS. This needs to be configured in the Vue.js app.

This can be done in the package.json file. The serve script was changed to support HTTPS. The oidc-client and the axios npm packages were added to the solution as well as the standard Vue.js npm packages.

axios is used for the API requests which uses the access token after a successful login.

{
  "name": "vue-js-oidc-client",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve --https --port 44357",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "axios": "^0.18.0",
    "oidc-client": "^1.6.1",
    "vue": "^2.5.21",
    "vue-class-component": "^6.0.0",
    "vue-property-decorator": "^7.0.0",
    "vue-router": "^3.0.1"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "^3.3.0",
    "@vue/cli-plugin-typescript": "^3.3.0",
    "@vue/cli-service": "^3.3.0",
    "typescript": "^3.0.0",
    "vue-template-compiler": "^2.5.21"
  }
}

The authentication was implemented following the blog from Jerrie Pelser and the samples from the oidc-client github repo.

An AuthService typescript class was implemented with the oidc-client settings. The code flow is configured here, as well as the silent renew. Then some functions were implemented for login, logout and getting an access token.

import { UserManager, WebStorageStateStore, User } from 'oidc-client';

export default class AuthService {
    private userManager: UserManager;

    constructor() {
        const STS_DOMAIN: string = 'https://localhost:44356';

        const settings: any = {
            userStore: new WebStorageStateStore({ store: window.localStorage }),
            authority: STS_DOMAIN,
            client_id: 'vuejs_code_client',
            redirect_uri: 'https://localhost:44357/callback.html',
            automaticSilentRenew: true,
            silent_redirect_uri: 'https://localhost:44357/silent-renew.html',
            response_type: 'code',
            scope: 'openid profile dataEventRecords',
            post_logout_redirect_uri: 'https://localhost:44357/',
            filterProtocolClaims: true,
        };

        this.userManager = new UserManager(settings);
    }

    public getUser(): Promise<User> {
        return this.userManager.getUser();
    }

    public login(): Promise<void> {
        return this.userManager.signinRedirect();
    }

    public logout(): Promise<void> {
        return this.userManager.signoutRedirect();
    }

    public getAccessToken(): Promise<string> {
        return this.userManager.getUser().then((data: any) => {
            return data.access_token;
        });
    }
}

The AuthService was then used in the Home.vue file just like Jerrie Pelser did it. Thanks for this blog. An API call was added to the component which gets the access token from the oidc lib, and requests data from a secure API. The data is then displayed in the UI.

<template>
    <div class="home">
        <img alt="Vue logo" src="../assets/logo.png">
        <div class="home">
            <p v-if="isLoggedIn">User: {{ username }}</p>
            <button class="btn" @click="login" v-if="!isLoggedIn">Login</button>
            <button class="btn" @click="logout" v-if="isLoggedIn">Logout</button>
            <button class="btn" @click="getProtectedApiData" v-if="isLoggedIn">Get API data</button>
        </div>

        <div v-if="dataEventRecordsItems && dataEventRecordsItems.length">
            <div v-for="dataEventRecordsItem of dataEventRecordsItems">
                <p><em>Id:</em> {{dataEventRecordsItem.Id}} <em>Details:</em> {{dataEventRecordsItem.Name}}  - {{dataEventRecordsItem.Description}} - {{dataEventRecordsItem.Timestamp}}</p>
            </div>
            <br />
        </div>

    </div>
</template>
<script lang="ts">
    import { Component, Vue } from 'vue-property-decorator';
    import AuthService from '@/services/auth.service';

    import axios from 'axios';

    const auth = new AuthService();

    @Component({
        components: {
        },
    })

    export default class Home extends Vue {
        public currentUser: string = '';
        public accessTokenExpired: boolean | undefined = false;
        public isLoggedIn: boolean = false;

        public dataEventRecordsItems: [] = [];

        get username(): string {
            return this.currentUser;
        }

        public login() {
            auth.login();
        }

        public logout() {
            auth.logout();
        }

        public mounted() {
            auth.getUser().then((user) => {
                this.currentUser = user.profile.name;
                this.accessTokenExpired = user.expired;

                this.isLoggedIn = (user !== null && !user.expired);
            });
        }

        public getProtectedApiData() {

            auth.getAccessToken().then((userToken: string) => {
                axios.defaults.headers.common['Authorization'] = `Bearer ${userToken}`;

                axios.get('https://localhost:44355/api/DataEventRecords/')
                    .then((response: any) => {
                        this.dataEventRecordsItems = response.data;
                    })
                    .catch((error: any) => {
                        alert(error);
                    });
            });
        }
    }
</script>
<style>

    .btn {
        color: #42b983;
        font-weight: bold;
        background-color: #007bff;
        border-color: #007bff;
        display: inline-block;
        font-weight: 400;
        text-align: center;
        vertical-align: middle;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
        background-color: transparent;
        border: 1px solid #42b983;
        padding: .375rem .75rem;
        margin: 10px;
        font-size: 1rem;
        line-height: 1.5;
        border-radius: .25rem;
        transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
    }

</style>

Two html files are required in the root web folder. In Vue.js, this is the public folder. A callback.html file is required to handle the code redirect.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Waiting...</title>
</head>
<body>
    <script src="js/oidc-client.min.js"></script>
    <script>

        var mgr = new Oidc.UserManager({ response_mode: 'query', userStore: new Oidc.WebStorageStateStore() }).signinRedirectCallback().then(function (user) {
            console.log("signin response success", user);
            window.location.href = '../';
        }).catch(function (err) {
            console.log(err);
            });

    </script>
</body>
</html>

The silent-renew.html is used for the silent renew in the iframe.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Waiting...</title>
</head>
<body>
    <script src="js/oidc-client.min.js"></script>
    <script>

        var mgr = new Oidc.UserManager().signinSilentCallback();
    </script>
</body>
</html>

Both of these html files require a reference to the oidc-client.min.js which is copied to the js folder.

When the application is run, the user can login and get the secure data. Start the two dotnet core applications from Visual Studio and start the Vue.js application from the cmd using ‘npm run serve’

When the application starts login:

Give your consent:

request the API data:

And view the data:

Silent renew will also work using the iframe which has to be allowed on the server. This can be viewed in the F12 network tab in Chrome.

It is pretty easy to setup a secure Vue.js application using OIDC code Flow with PKCE.

Links

https://cli.vuejs.org

https://www.jerriepelser.com/blog/using-auth0-with-vue-oidc-client-js/

https://github.com/joaojosefilho/vuejsOidcClient

https://www.scottbrady91.com/Angular/Migrating-oidc-client-js-to-use-the-OpenID-Connect-Authorization-Code-Flow-and-PKCE

https://github.com/IdentityModel/oidc-client-js/

https://tools.ietf.org/html/rfc7636

https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest

5 comments

  1. […] Securing a Vue.js app using OpenID Connect Code Flow with PKCE and IdentityServer4 (Damien Bowden) […]

  2. What about using Azure Active Directory as the STS?

    1. Azure AD should work now that it supports OpenID Connect, but you will not have complete control of your tokens, and the applications become directly dependent on Azure AD and it’s breaking changes.

      Greetings Damien

  3. […] Securing a Vue.js app using OpenID Connect Code Flow with PKCE and IdentityServer4 […]

Leave a Reply to Vue.js – Securing Vue app with IDENTITY SERVER 4 – Bhavin Patel Cancel 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.

%d bloggers like this: