Lightweight auth library using the oidc-client-ts library for Vue single page applications (SPA). Support for composables and higher-order components (HOC)
This library implements an auth context provider by making use of the
oidc-client-ts library. Its configuration is tight coupled to that library.
The
User
and
UserManager
is hold in this context, which is accessible from the
Vue application. Additionally it intercepts the auth redirects by looking at
the query/fragment parameters and acts accordingly. You still need to setup a
redirect uri, which must point to your application, but you do not need to
create that route.
To renew the access token, the
automatic silent renew
feature of oidc-client-ts can be used.
Using npm
npm install oidc-client-ts vue-oidc-context --saveUsing pnpm
pnpm add oidc-client-ts vue-oidc-contextConfigure the library by wrapping your application in AuthProvider:
// App.vue
<script setup lang="ts">
import AuthProvider from "vue-oidc-context";
const oidcConfig = {
authority: "authority",
client_id: "clientId",
redirect_uri: window.location.origin
};
</script>
<template>
<AuthProvider
:authority="oidcConfig.authority"
:client_id="oidcConfig.client_id"
:redirect_uri="oidcConfig.redirect_uri"
>
<router-view/>
</AuthProvider>
</template>Use the useAuth component in your components to access authentication state
(isLoading, isAuthenticated and user) and userManagerContext with methods
(signinRedirect and signOutRedirect) and removeUser method:
// Home.vue
<script setup lang="ts">
import { useAuth } from "vue-oidc-context";
import { ref, watch } from "vue";
import { hasAuthParams } from "vue-oidc-context";
const auth = useAuth();
const hasTriedSignin = ref(false)
</script>
<template>
<div v-if="auth.activeNavigator === 'signinSilent'">
Signing you in...
</div>
<div v-if="auth.activeNavigator === 'signoutRedirect'">
Signing you out...
</div>
<div v-if="auth.isLoading">Loading...</div>
<div v-if="auth.error">Oops... {{ state.error.message }}</div>
<div v-if="auth.isAuthenticated">
Hello {{ state.user?.profile.sub }}{{ " " }}
<button @click="() => auth.removeUser()">Log out</button>
</div>
<div v-if="!auth.isLoading && !auth.isAuthenticated">
<button @click="() => auth.signinRedirect()">Log in</button>
</div>
</template>You must provide an implementation of onSigninCallback to oidcConfig to remove the payload from the URL upon successful login. Otherwise if you refresh the page and the payload is still there, signinSilent - which handles renewing your token - won't work.
A working implementation is already in the code here.
As a child of AuthProvider with a user containing an access token:
// Posts.vue
<script setup lang="ts">
import { useAuth } from "vue-oidc-context";
import { ref } from "vue";
const auth = useAuth()
const posts = ref()
const getPosts = async () => {
try {
const token = auth.value.user?.access_token;
const response = await fetch("https://api.example.com/posts", {
headers: {
Authorization: `Bearer ${token}`,
},
});
posts.value = await response.json()
} catch (e) {
console.error(e)
}
}
</script>
<template>
<div v-if="!posts.length">Loading...</div>
<ul>
<li
v-for="(post, index) in posts"
:key="index"
>
{{post}}
</li>
</ul>
</template>Secure a route component by using the withAuthenticationRequired higher-order component. If a user attempts
to access this route without authentication, they will be redirected to the login page.
// router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import Home from "../sections/Home.vue";
import { withAuthenticationRequired } from "vue-oidc-context";
const routes = [
//...
{
path: '/home',
name: 'home',
component: withAuthenticationRequired(Home)
},
//...
]
const router = createRouter({
history: createWebHistory(''),
routes
})
export default routerDefine Callback.vue component if you want remove code and state query params.
<script setup lang="ts">
import { watch } from "vue";
import { useAuth } from "vue-oidc-context";
const auth = useAuth();
watch(auth, () => {
if (!auth.value.isLoading && auth.value.isAuthenticated) {
window.location.replace(window.location.origin + '/home')
}
})
</script>
<template>
<div>...Loading</div>
</template>Append Callback.vue in routes
// router/index.ts
import { createRouter, createWebHistory } from "vue-router";
import Home from "../sections/Home.vue";
import { withAuthenticationRequired } from "vue-oidc-context";
const routes = [
//...
{
path: '/home',
name: 'home',
component: withAuthenticationRequired(Home)
},
{
name: 'callback',
path: '/callback',
component: Callback
},
//...
]
const router = createRouter({
history: createWebHistory(''),
routes
})
export default routerThe underlying UserManagerEvents instance can be imperatively managed with the useAuth composable.
// Home.vue
<script setup lang="ts">
import { useAuth } from "vue-oidc-context";
import { ref, watch } from "vue";
import { hasAuthParams } from "vue-oidc-context";
const auth = useAuth();
const hasTriedSignin = ref(false)
watch([auth.value.events, auth.value.signinSilent],() => {
auth.value.events.addAccessTokenExpiring(() => {
if (alert("You're about to be signed out due to inactivity. Press continue to stay signed in.")) {
auth.value.signinSilent();
}
})
})
</script>
<template>
<button @click="() => auth.signinRedirect()">Log in</button>
</template>Automatically sign-in and silently reestablish your previous session, if you close the tab and reopen the application.
// App.vue
<script setup lang="ts">
const oidcConfig: AuthProviderProps = {
//...
userStore: new WebStorageStateStore({store: window.localStorage}),
}
</script>// Home.vue
<script setup lang="ts">
import { useAuth } from "vue-oidc-context";
import { ref, watch } from "vue";
import { hasAuthParams } from "vue-oidc-context";
const auth = useAuth();
const hasTriedSignin = ref(false)
watch([auth, hasTriedSignin],() => {
if (!hasAuthParams() && !auth.value.isAuthenticated && !auth.value.activeNavigator
&& !auth.value.isLoading && !hasTriedSignin.value
) {
auth.signinRedirect()
hasTriedSignin.value = true
}
})
</script>
<template>
<div v-if="auth.activeNavigator === 'signinSilent'">
Signing you in...
</div>
<div v-if="auth.activeNavigator === 'signoutRedirect'">
Signing you out...
</div>
<div v-if="auth.isLoading">Loading...</div>
<div v-if="auth.error">Oops... {{ state.error.message }}</div>
<div v-if="auth.isAuthenticated">
Hello {{ state.user?.profile.sub }}{{ " " }}
<button @click="() => auth.removeUser()">Log out</button>
</div>
<div v-if="!auth.isLoading && !auth.isAuthenticated">
<button @click="() => auth.signinRedirect()">Log in</button>
</div>
</template>