DEV Community

Cover image for Single Sign-On (SSO) with Zoho in Vue3
Odumosu Oluwashina
Odumosu Oluwashina

Posted on

Single Sign-On (SSO) with Zoho in Vue3

Hi fellas,

So I was recently working on a project where I had to use OIDC πŸ”for authentication on the app. Apparently, it was a tad bit more difficult than I expected, mostly due to lack of proper documentation especially for the Vue 3 framework.

You know how you feel like a boss 😎 when you're finally able to figure things out and make it work? Yup. That was soo me. My head was through the roof but I felt like I went through so much.

So if you ever, find yourself where I started, with little to no proper documentation, I hope this helps you feel like a boss in less time and with less stress.

A little introduction

Authentication and security are essential aspects of web development, and OIDC (OpenID Connect) has emerged as a popular authentication protocol due to its simplicity and robustness.

In this article, we will explore how to integrate OIDC into a Vue.js (version 3) application using Zoho as the identity provider. Zoho, a renowned cloud-based software suite with amazing products, offers a secure and reliable OIDC-based authentication service that enables developers to implement seamless user authentication in their Vue.js applications.

What is OIDC?

OIDC which stands for Open-Identity Connect, is an extension of OAuth 2.0, designed to provide a standardized way for users to log in and authenticate with web applications. It allows for the secure transmission of user identity information between the identity provider (IdP) and the client application, ensuring a seamless and consistent user experience across different platforms.

Single Sign-On Authentication

Simply put, it is an open standard that allows users to authenticate to applications using their existing identity providers. This means that users can sign in to your application using their Zoho credentials, without having to create a new account.

What is Vue?

Should you even be reading this if you don't know what Vue is?πŸ˜… Hey, don't get me wrong, you just need to learn about a tool before you start using it so you can maximize your effort. You can check the Vue article to learn about Vue and then come back to implement this. I'll definitely be rooting for you.

How to use OIDC in Vue

Before integrating OIDC into a Vue.js application, you need to set up Zoho as the identity provider:

  • Create a Zoho Developer Account: Visit the Zoho Developer Console and sign in or sign up for an account.

  • Register Your Application: In the Zoho Developer Console, create a new "Client" to register your application. You will receive a Client ID and Client Secret that you'll use later in the Vue.js application.

  • Define Redirect URLs: Specify the allowed redirect URLs where Zoho can send authentication responses after successful login. This should eventually resolve to this -
    redirect_uri=https://your.url.com/signin-oidc&response_type=code&prompt=consent&scope=openid profile AaaServer.profile.Read email&code_challenge=xxxx-xxx-xxxx-xxxxxx-xxx&code_challenge_method=S256&response_mode=form_post&state=xxxx-xxx-xxxx-xxxxxx-xxx&x-client-SKU=XX_XXX-1.0&x-client-ver=6.21.0.0

Let's get to coding!

To begin, we would need to set up an application using vue and oidc. We are going to install a library known as vue-oidc-client. This is a vue wrapper around oidc-client-js to help it work well with Vue.

Integrating OIDC into Vue.js

Now, let's dive into integrating OIDC into your Vue.js application:

  • Setting Up Vue.js Project: Start by creating a new Vue.js project using Vue CLI or any other preferred method. See here on how to.

  • Install Required Dependencies: To handle OIDC authentication, you'll need some packages like oidc-client and vue-oidc-client. Install them using npm or yarn:

npm install oidc-client vue-oidc-client

yarn add oidc-client vue-oidc-client
Enter fullscreen mode Exit fullscreen mode
  • Next, create a .env file for your credentials or secret
VITE_APP_CLIENT_ID=whatever_your_client_id_is
VITE_APP_BASE_URL=whatever_your_base_url_is
VITE_APP_OIDC_AUTHORITY=whatever_your_zoho_oidc_endpoint_is
Enter fullscreen mode Exit fullscreen mode
  • Create a config file to hold the settings config.ts
export const client_id:string = import meta.env.VITE_APP_CLIENT_ID;
export const base_url:string = import meta.env.VITE_APP_BASE_URL;
export const oidc_authority:string = import meta.env.VITE_APP_OIDC_AUTHORITY;
Enter fullscreen mode Exit fullscreen mode
  • Implement the oidc service idsrvAuth.ts
import { User } from 'oidc-client'
import { createOidcAuth, SignInType, LogLevel } from 'vue-oidc-client/vue3'
import { client_id, base_url, oidc_authority } from './config'

const appRootUrl:string = `${base_url}/`

// OR
// const loco = window.location
// const appRootUrl = `${loco.protocol}${loco.host}/`
// const appRootUrl = 'localhost:8000/'
const provider:string = 'Zoho'

const authCallbackPath:string = 'index.html?auth-callback=1';
const logoutCallbackPath:string = '/index.html?logout-callback=1';
const prompt:string = 'login'
const redirectUri:string = `${base_url}/${authCallbackPath}`; // remove / for local
const scopes:string = 'offline_access';
const logoutRedirectUri:string = `${oidc_authority}${logoutCallbackPath}`;

const idsrvAuth = createOidcAuth(
  'main',
  SignInType.Popup, // SignInType.Window on local
  appRootUrl,
  {
    authority: oidc_authority,
    client_id: client_id,
    redirect_uri: redirectUri,
    post_logout_redirect_uri: 'https://your-client-url/',
    response_type: 'code',
    filterProtocolClaims: true,
    loadUserInfo: true,
    scope: scope,
    extraQueryParams: { prompt: prompt, provider: provider }
  },
  console,
  LogLevel.Debug
)

// handle events
idsrvAuth.events.addAccessTokenExpiring(function() {
  // eslint-disable-next-line no-console
  console.log('access token expiring')
})

idsrvAuth.events.addAccessTokenExpired(function() {
  // eslint-disable-next-line no-console
  console.log('access token expired')
})

idsrvAuth.events.addSilentRenewError(function(err: Error) {
  // eslint-disable-next-line no-console
  console.error('silent renew error', err)
})

idsrvAuth.events.addUserLoaded(function(user: User) {
  // eslint-disable-next-line no-console
  console.log('user loaded', user)
// save token to storage for use later
  localStorage.setItem('token', user?.access_token)
})

idsrvAuth.events.addUserUnloaded(function() {
  // eslint-disable-next-line no-console
  console.log('user unloaded')
})

idsrvAuth.events.addUserSignedOut(function() {
  // eslint-disable-next-line no-console
  console.log('user signed out')
})

idsrvAuth.events.addUserSessionChanged(function() {
  // eslint-disable-next-line no-console
  console.log('user session changed')
})

export default idsrvAuth
Enter fullscreen mode Exit fullscreen mode
  • Edit your main.ts file
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import idSrvAuth from './idSrvAuth'
...

try {
    const start:any = idSrvAuth.startup();
    if (start) {
        const app:any = createApp(App).use(router);

        app.config.globalProperties.$oidc = idSrvAuth

        app.config.globalProperties.filtersLimit = (value:any, size:any) => {
            if (!value) return '';
            value = value.toString();

            if (value.length <= size) {
            return value;
            }
            return value.substr(0, size) + '...';
        }

        app.mount('#app')
    }
}
catch(err:any) {
    console.log(`Startup was not okay: ${err}`)
}
Enter fullscreen mode Exit fullscreen mode
  • Next up, update your router with the idsrvAuth service
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import idsrvAuth from '../idSrvAuth';

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Login',
    component: () => import('../views/auth/Login.vue'),
    meta: { requiresAuth: false },
  },
  {
    path: '/dashboard',
    name: 'Layout',
    component: () => import('../views/dashboard/Layout.vue'),
    meta: { authName: idsrvAuth.authName },
    children: [
      {
        path: '',
        name: 'Dashboard',
        component: () => import('../views/dashboard/Index.vue'),
        meta: {
          requiresAuth: true,
        },
      },
      {
        path: 'profile',
        name: 'Profile',
        component: () => import('../views/dashboard/Profile.vue'),
        meta: { requiresAuth: true },
      },
    ]
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'Error',
    component: () => import('../components/NotFound.vue'),
    meta: { requiresAuth: false },
  },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

router.beforeEach(async (to, from) => {
  const token:any = localStorage.getItem('token')
  if (to.meta.requiresAuth) {
    if (!token) {
      router.push({ name: 'Login' })
    }
  }
})

idsrvAuth.useRouter(router);

export default router;
Enter fullscreen mode Exit fullscreen mode
  • Finally, you can use in your form like this loginForm.vue
<script lang="ts">
export default {
  name: 'LoginForm',
}
</script>

<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import SvgIcons from '../SvgIcons.vue';
import spinner from '../spinner.vue'

const route = useRouter();

const loginWithZoho:any = async () => {
    route.push('/dashboard')
}
</script>

<template>
    <div class="relative main w-full bg-white">
        <form class="text-sm grid">
            <div class="grid gap-1 mb-4">
                <label for="username" class="font-semibold">
                    Username
                </label>
                <input type="text" name="username" v-model="data.username" id="username" @blur="checkError" placeholder="Enter username" :class="[errors.username ? 'border text-red border-red' : '']" class="p-4 rounded-md bg-gray-50 text-xs focus:outline-none">
                <p class="text-[10px] text-red">
                    {{ errors.username ? errors.usertext : '' }}
                </p>
            </div>
            <div class="grid gap-1 mt-4 mb-2">
                <label for="password" class="font-semibold">
                    Password
                </label>
                <input type="password" @blur="checkError" name="password" id="password" placeholder="Enter password" v-model="data.password" :class="[errors.password ? 'border text-red border-red' : '']" class="p-4 rounded-md bg-gray-50 text-xs focus:outline-none">
                <p class="text-[10px] text-red">
                    {{ errors.password ? errors.passwordtext : '' }}
                </p>
            </div>
            <div class="extras flex justify-between items-center text-[10px] text-blue-600 mt-2 mb-8">
                <div class="forgot">
                    <button @click.prevent="forgotPassword">
                        Forgot password?
                    </button>
                </div>
                <div class="remember flex">
                    <div class="flex items-center">
                        <input id="list-check" @click.prevent="check" type="checkbox" class="opacity-0 absolute h-5 w-5" />
                        <div class="bg-primary text-primary border-2 w-5 h-5 flex flex-shrink-0 justify-center items-center mr-2 focus-within:border-primary">
                        <SvgIcons v-if="rememberChecked" class="text-white" name="tick" />
                        </div>
                    </div>
                    <label for="remember">
                        Remember me
                    </label>
                </div>
            </div>
            <div class="grid">
                <button @click.prevent="submit" :disabled="isDisabled" :class="[isDisabled ? 'bg-grey' : 'bg-primary']" class="p-4 font-bold flex justify-center border text-white rounded-md">
                    <span class="px-4" :class="[isLoading ? '' : 'hidden']">
                        <spinner />
                    </span>
                    Login
                </button>
            </div>
        </form>
        <div class="flex items-center justify-center mt-8 gap-3">
            <hr class="bg-primary w-full h-[2px]"/>
            <p>OR</p>
            <hr class="bg-primary w-full h-[2px]"/>
        </div>
        <div class="my-5">
            <button @click="loginWithZoho" class="bg-[#CE2232] flex w-full font-semibold bg-opacity-70 hover:bg-opacity-100 justify-center text-white py-3 px-5 rounded">
                Sign in with Zoho
            </button>
        </div>
    </div>
</template>

<style scoped>
</style>
Enter fullscreen mode Exit fullscreen mode

When the user clicks the button, the loginWithZoho() method will redirect the user to the Zoho OAuth 2.0 authorization endpoint. If the user authenticates successfully, they will be redirected back to your application, which will then redirect them to the /dashboard route.

Conclusion

In this article, you saw how to use OIDC with the router in Vue to create a secure and user-friendly authentication flow for your application. We used Zoho as our identity provider, but the same principles can be applied to any OIDC-compliant provider.

By integrating OIDC into your Vue.js application using Zoho as the identity provider, you can achieve a secure and seamless authentication experience for your users. OIDC's standardized approach and Zoho's reliable authentication service provide a robust foundation for any web application, ensuring user data remains protected while enabling easy access across multiple platforms.

Remember to explore further customization options, such as handling user roles, accessing user information, and handling token expiration, to tailor the OIDC integration to your application's specific requirements. Happy coding!

Resources

You can find the wiki docs here -

Reach me

Top comments (0)