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 --save
Using pnpm
pnpm add oidc-client-ts vue-oidc-context
Configure 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 router
Define 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 router
The 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>