Vue.js Tutorial

This tutorial demonstrates how to add user login, logout, and profile to a Vue 3 application.

This tutorial shows how to use PlusAuth with Vue 3 Single Page Application. If you do not have a PlusAuth account, register from here.

This tutorial follows plusauth-vue-starter sample project on Github. You can download and follow the tutorial via the sample project.

Create PlusAuth Client

After you sign up or log in to PlusAuth, you need to create a client to get the necessary configuration keys in the dashboard. Go to Clients and create a client with the type of Single Page Application

Configure Client

Get Client Properties

You will need your Client Id for interacting with PlusAuth. You can retrieve it from the created client's details.

Configure Redirect and Logout URIs

When PlusAuth authenticates a user, it needs a URI to redirect back with access and id token. That URI must be in your client's Redirect URI list. If your application uses a redirect URI which is not white-listed in your PlusAuth Client, you will receive an error.

The same thing applies to the logout URIs. After the user logs out, you need a URI to be redirected.

If you are following the sample project, the Redirect URL you need to add to the Redirect URIs fields are http://localhost:5173/callback and http://localhost:5173/silent-renew.html. The Logout URL you need to add to the Post Logout Redirect URIs field is http://localhost:5173/.

Create a Vue Application

Follow Quick Start Guide from Vue.js official document site and create a project. Make sure you have answered to Add Vue Router for Single Page Application development? prompt as Yes. Other prompts are not required for this tutorial.

Here is the overview of the commands:

Scaffolding project in ./... Done.

# Install the CLI
npm create vue@latest
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add an End-to-End Testing Solution? … No / Cypress / Playwright
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes

Scaffolding project in ./<your-project-name>...
Done.

Install OIDC Client

For interacting with PlusAuth it is advised to use an OpenID Connect library. In this tutorial we will be using oidc-client-js but you could use any OpenID Connect library.

Install oidc-client-js with the following command

npm install @plusauth/oidc-client-js
oidc-client-js is an OpenID Connect (OIDC) and OAuth2 library for browser based JavaScript applications. You can find source code on Github and the API documentation here.

Configure Vue Application to use PlusAuth

We will be using dotenv files for maintaining providing some constant values. Vite supports them, so no need for extra configuration.

Create the .env file

Create the .env file at the root of your project with the following and modify values accordingly.

# .env
VITE_OIDC_ISSUER=https://<YOUR_PLUSAUTH_TENANT_NAME>.plusauth.com/
VITE_OIDC_CLIENT_ID=<YOUR_PLUSAUTH_CLIENT_ID>
If you are following the sample project, rename .env.example to .env and replace the values accordingly.

Configure OIDC Client

We need to initialize our OIDC Client library to handle authentication-related operations. Create auth.js in src folder. Configure oidc-client-js as following:

import { OIDCClient } from '@plusauth/oidc-client-js'

const Auth = new OIDCClient({
  issuer: import.meta.env.VITE_OIDC_ISSUER,
  client_id: import.meta.env.VITE_CLIENT_ID,
  redirect_uri: 'http://localhost:5173/callback',
  silent_redirect_uri: 'http://localhost:5173/silent-renew',
  post_logout_redirect_uri: 'http://localhost:5173/',
  response_mode: 'fragment',
  response_type: 'id_token token',
  scope: 'openid profile',
  checkSession: true,
  autoSilentRenew: true,
  requestUserInfo: true,
})

export { Auth }

You may have noticed that the values defined in the Configure Client section are used here. If you have used different values make sure to update this file accordingly.

Configure Vue Application

Let's start by defining our application's entry point file. Go to file named main.js in the src folder. Import the auth file we have created above and make the following changes.

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { Auth } from './auth'
const app = createApp(App)

app.use(router)

// Make auth object global to access from anywhere
app.config.globalProperties.$auth = Auth 
Auth.initialize()     .then(() => {})     .catch(console.error)     .finally(() => {       app.mount('#app')
    }) 

Configure Router

Now let's define our application's router. We are going to define routes of our views. requiresAuth flag in the metadata of routes will ensure those routes are accessible only by authenticaed users.

Create router.js in src folder as following:

import * as VueRouter from 'vue-router'
import { Auth } from "./auth.js";
import Home from './components/Home.vue'
import Profile from './components/Profile.vue'
import Unauthorized from './components/Unauthorized.vue'
import AuthCallback from './components/AuthCallback.vue'
import SilentRenew from './components/SilentRenew.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/callback', // Authentication redirect uri
    name: 'AuthCallback',
    component: AuthCallback
  },
  {
    path: '/silent-renew', // Token silent renew uri
    name: 'SilentRenew',
    component: SilentRenew
  },
  {
    path: '/profile',
    name: 'profile',
    meta: {requiresAuth: true},
    component: Profile
  },
  {
    path: '/unauthorized', // Redirect to page if user not authorized
    name: 'unauthorized',
    component: Unauthorized
  },
  {
    path: '/:pathMatch(.*)*', // redirect to home if unhandled route entered
    redirect: '/'
  }
]

const Router = VueRouter.createRouter({
  history: VueRouter.createWebHistory('/'),
  routes
})

// Check user if logged in for routes that requires auth
Router.beforeEach(async (to, from, next) => {
  const loggedIn = await Auth.isLoggedIn(true)
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!loggedIn) {
      return next({
        path: '/unauthorized'
      })
    }
  }
  return next()
})

export { Router }

Implement login, user profile, and logout

Until now, we have defined our authentication helper and routes. It is time to create the pages and interact with auth helper.

Create Main Vue Component

Let's create a simple layout for our application. Add Header component and router-view to App.vue.

<template>
    <div id="app">
        <Header></Header>
        <router-view />
    </div>
</template>
<script>
import Header from './components/Header'
export default {
    name: 'App',
    components: { Header }
}
</script>
<style>
body {
    padding-top: 5rem;
}
</style>

Create Header Component

Create Header.vue under src/components folder. It will be a basic header. If a user is authenticated, it will show the user's identifier and a Logout button. If not, a Login button will be there to initiate login.

<template>
  <header>
    <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
      <a class="navbar-brand" href="/">Plusauth Starter</a>
      <div class="collapse navbar-collapse" id="navbarsExampleDefault">
        <ul class="navbar-nav mr-auto"></ul>
        <template v-if="user">
          <li class="nav-item navbar-nav">
            <router-link class="nav-link" to="/profile">{{
              `Logged in as: ${userDisplayName}`
            }}</router-link>
          </li>
          <button class="btn btn-link" @click="$auth.logout()">Logout</button>
        </template>
        <li v-else class="nav-item navbar-nav">
          <button class="btn btn-link" @click="$auth.login()">Login</button>
        </li>
      </div>
    </nav>
  </header>
</template>

<script>
export default {
  data() {
    return {
      user: null
    }
  },
  computed: {
    userDisplayName() {
      if (!this.user) {
        return null
      } else {
        if (!this.user.given_name || !this.user.family_name) {
          return this.user.username || this.user.email || this.user.sub
        }
        return `${this.user.given_name} ${this.user.family_name}`
      }
    }
  },
  async created() {
    this.$auth.on('user_login', ({ user }) => {
      this.user = user
    })
    this.$auth.on('user_logout', () => {
      this.user = null
    })
    this.user = await this.$auth.getUser()
  }
}
</script>

Create AuthCallback

To handle authorization results after a successful login, we need a simple page and let the library handle the authentication result. Create AuthCallback.vue under src/components folder.

<template>
  <div></div>
</template>
<script>
export default {
  name: 'AuthCallback',
  async mounted() {
    try {
      await this.$auth.loginCallback(window.location.href)
      this.$router.replace('/')
    } catch (e) {
      console.error(e)
    }
  }
}
</script>

Create SilentRenew

Access tokens retrieved from PlusAuth have a life span. oidc-client-js automatically provides access_token renewal without too much hassle. Before your access token expires, it will receive a new one in the background so that your users will have a flawless app experience without signing in again.

Create SilentRenew.vue under src/components folder as following:

<template> </template>
<script>
import { OIDCClient } from '@plusauth/oidc-client-js'
export default {
  name: 'SilentRenew',
  async mounted() {
    await new OIDCClient({
      issuer: process.env.VITE_OIDC_ISSUER
    }).loginCallback()
  }
}
</script>

Create Views

HomePage

Create Home.vue under src/components.

<template>
  <div class="jumbotron">
    <div class="container">
      <h1 class="display-3">Hello, world!</h1>
      <p>
        This is a template for a simple login/register system. It includes the
        OpenID Connect Implicit Flow. To view Profile page please login.
      </p>
      <p>
        <router-link v-if="user" class="btn btn-success btn-lg" to="/profile">
          View Profile &raquo;
        </router-link>
        <button v-else class="btn btn-primary btn-lg" @click="$auth.login()">
          Login/Register &raquo;
        </button>
      </p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Home',
  data() {
    return {
      user: null
    }
  },
  async created() {
    this.$auth.on('user_login', ({ user }) => {
      this.user = user
    })
    this.$auth.on('user_logout', () => {
      this.user = null
    })
    this.user = await this.$auth.getUser()
  }
}
</script>

Profile Page

Create Profile.vue under src/components.

<template>
  <div class="container" v-if="user">
    <h3>Welcome {{ user.username }} !</h3>
    <pre>User object: {{ JSON.stringify(user, null, 2) }} </pre>
  </div>
</template>

<script>
export default {
  name: 'Profile',
  data() {
    return {
      user: null
    }
  },
  async created() {
    this.$auth.on('user_login', ({ user }) => {
      this.user = user
    })
    this.$auth.on('user_logout', () => {
      this.user = null
    })
    this.user = await this.$auth.getUser()
  }
}
</script>

Add Unauthorized Page

We will display a page whenever a user tries to access a protected route without signing in.

Create Unauthorized.vue under src/components.

<template>
  <div class="container">
    <p>You must log in to view the page</p>
    <button class="btn btn-primary" @click="$auth.login()">Log in</button>
  </div>
</template>

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

See it in action

That's it. Start your app and point your browser to http://localhost:5173. Follow the Log In link to log in or sign up to your PlusAuth tenant. Upon successful login or signup, you should be redirected back to the application.