Authorization with Vue 3 and Apollo

Intro

In this article we’re going to take a look at how to implement client-side authentication with Vue3 + Vite + Apollo + GraphQL. We will implement authentication using json web tokens and make it reactive so that our application can react to authentication state changes. As the focus of this article is on the frontend we will use a very basic GraphQL mock auth server that just satisfies our minimal requirements of signing users up and in and validating jwts.

Before you start make sure that you have NodeJS of version at least 14.18.0 installed as it is a requirement for projects running on vite@3.1.0 or newer.

Setup

Scaffold a new project using vite

Let’s start by creating an empty Vue 3 project

yarn create vite

Pick Vue as framework and typescript as variant. We can run our application using yarn dev command.

We will add 2 pages to our application. Home page will be available only to unauthenticated users and Protected page will be accessible only by logged in users. First let’s add vue-router

yarn add vue-router@4

Then create src/plugins/router.ts file.

import { createRouter, createWebHistory } from "vue-router";

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: "/",
      component: () => import("@/components/pages/Home.vue"),
    },
    {
      path: "/protected",
      component: () => import("@/components/pages/Protected.vue"),
    },
  ],
});

export default router;

And update src/main.ts and src/App.vue

import { createApp, provide, h } from "vue";
import "./style.css";
import App from "./App.vue";
import router from "@/plugins/router";

const app = createApp(App);
app.use(router);
app.mount("#app");
<template>
	<RouterView />
</template>

Home page will contain an authentication form with email and password inputs and 2 buttons: Sign Up and Sign In. Let’s prepare all the components for it. We’re going to use tailwindcss framework for styling, so let’s install it.

yarn add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Add tailwind.config.js file with the following content:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

And update src/style.css

@tailwind base;
@tailwind components;
@tailwind utilities;

Now we’re ready to create our BaseButton and TextInput components!

<script lang="ts" setup>
type TextInputProps = {
  modelValue: string;
};
type TextInputEmits = {
  (event: "update:modelValue", payload: string): void;
};

defineProps<TextInputProps>();
const emit = defineEmits<TextInputEmits>();

function onInput(e: Event) {
  emit("update:modelValue", (e.target as HTMLInputElement).value);
}
</script>

<template>
  <label class="block text-sm font-medium text-gray-900 dark:text-gray-300">
    <slot />
    <input
      class="mt-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
      v-bind="$attrs"
      :value="modelValue"
      @input="onInput"
    />
  </label>
</template>
<template>
  <button
    class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
    v-bind="$attrs"
  >
    <slot />
  </button>
</template>

Add these components to AuthForm

<script setup lang="ts">
import { reactive } from "vue";
import TextInput from "@/components/TextInput/TextInput.vue";
import BaseButton from "@/components/BaseButton/BaseButton.vue";

const form = reactive({
  input: {
    email: "",
    password: "",
  },
});
</script>

<template>
  <div class="flex flex-col items-center justify-center h-screen">
    <section class="p-4 bg-gray-100 rounded-lg border">
      <form class="grid gap-6">
        <div>
          <TextInput
            placeholder="john@doe.com"
            v-model="form.input.email"
            autofocus
          >
						Email:
          </TextInput>
        </div>
        <div>
          <TextInput v-model="form.input.password" type="password">
						Password:
          </TextInput>
        </div>
        <div>
          <BaseButton>Sign Up</BaseButton>
          <BaseButton>Sign In</BaseButton>
        </div>
      </form>
    </section>
  </div>
</template>

And use AuthForm on our homepage:

<script setup lang="ts">
import AuthForm from "@/components/AuthForm/AuthForm.vue";
</script>

<template>
  <div class="container mx-auto">
    <AuthForm />
  </div>
</template>

For  Protected page let’s use a placholder for now, we’re going to update it later, when we have our authentication set up.

<template>Protected page</template>

At this point, when you start your application you should see this simple form on the home page.

Untitled

Preparing auth server

In order to be able to authenticate users, we will need a GraphQL auth server. The following server enables 3 GraphQL operations:

  • query currentUser - it validates the jwt and returns User object
  • mutation signUp - creates a new user if it doesn’t exist and returns a signed jwt
  • mutation signIn - authenticates a user and returns a signed jwt

Additionally it runs a GraphQL sandbox on [<http://localhost:4000/graphql>](<http://localhost:4000/graphql>) url so you can test these queries and mutations.

Create a separate folder for our server application, install dependencies and add the following code to server.js

yarn init && yarn add express express-graphql express-jwt graphql jsonwebtoken
const express = require("express");
const { graphqlHTTP } = require("express-graphql");
const { buildSchema } = require("graphql");
const jwt = require("jsonwebtoken");
const cors = require("cors");

const NOT_SO_SECRET_JWT_KEY = "shhhhh";

const fakeDb = new Map();

const schema = buildSchema(`
  type User {
    id: ID
    email: String
  }
  
  type Token {
    token: String!
  }
  
  input AuthInput {
    email: String
    password: String
  }
  
  type Mutation {
    signUp(input: AuthInput): Token
    signIn(input: AuthInput): Token
  }
  
  type Query {
    currentUser: User
  }
`);

const rootValue = {
  signUp({ input }) {
    if (!input.email) throw new Error("Email is required");
    if (!input.password) throw new Error("Password is required");
    if (fakeDb.get(input.email)) throw new Error("User already exists");

    const newUser = {
      id: fakeDb.size + 1,
      email: input.email,
    };
    const token = jwt.sign({ email: newUser.email }, NOT_SO_SECRET_JWT_KEY);

    fakeDb.set(input.email, { ...newUser, ...input });

    return {
      token,
    };
  },
  signIn({ input }) {
    if (!input.email) throw new Error("Email is required");
    if (!input.password) throw new Error("Password is required");

    const user = fakeDb.get(input.email);

    if (!user) throw new Error("User not found");
    if (input.password !== user.password) throw new Error("Incorrect password");

    const token = jwt.sign(
      { id: user.id, email: user.email },
      NOT_SO_SECRET_JWT_KEY
    );
    return { token };
  },
  currentUser(args, request) {
    const token = request.headers.authorization.split(" ").pop();
    if (!token) return {};
    const userData = jwt.verify(token, NOT_SO_SECRET_JWT_KEY);
    const user = fakeDb.get(userData.email);

    if (!user) return {};

    return user;
  },
};

const app = express();
app.use(cors());
app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    rootValue,
    graphiql: true,
  })
);
app.listen(4000);
console.log("Running a GraphQL API server at localhost:4000/graphql");

Now we can run this server with

node server.js

Please take a note that this is just a mock server and it is not suitable for production.

Install apollo

To integrate with GraphQL backend we’re going to use Apollo client with vue-apollo implementation in particular. Let’s head back to our Vue3 project and install the required dependencies.

yarn add graphql graphql-tag @apollo/client @vue/apollo-composable
yarn add -D @rollup/plugin-graphql

Update vite.config.ts by adding GraphQL rollup plugin. This will allow us to write our GraphQL queries and mutations in separate files with .gql extension

import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import graphql from '@rollup/plugin-graphql';

// <https://vitejs.dev/config/>
export default defineConfig({
    plugins: [vue(), graphql()]
})

Create .env.local file. Vite will load env variables during development from this file so we can use it to store the URL of our GraphQL endpoint. Take a note that only variables with prefix VITE_ will be exposed to your client-side code.

VITE_GRAPHQL_URL=http://localhost:4000/graphql

Create src/plugins/apollo.ts file. We instantiate a new ApolloClient by providing 2 options to the constructor:

link - an instance of ApolloLink that will hold the URL of our GraphQL endpoint, cache - storage for all the data returned from GraphQL, in our case, it's an instance of InMemoryCache

import {
  ApolloClient,
  createHttpLink,
  InMemoryCache,
} from "@apollo/client/core";

const httpLink = createHttpLink({
  uri: import.meta.env.VITE_GRAPHQL_URL,
});
const cache = new InMemoryCache();
const apolloClient = new ApolloClient({
  link: httpLink,
  cache,
});

export default apolloClient;

And connect apolloClient in main.ts.

import { createApp, provide, h } from "vue";
import "./style.css";
import App from "./App.vue";
import { DefaultApolloClient } from "@vue/apollo-composable";
import apolloClient from "@/plugins/apollo";
import router from "@/plugins/router";

const app = createApp({
  setup() {
    provide(DefaultApolloClient, apolloClient);
  },
  render: () => h(App),
});
app.use(router);
app.mount("#app");

I also highly recommend installing Apollo Client Devtools extension. We’re not going to use it in this tutorial, but it will make it much easier to debug your GraphQL operations and see a snapshot of your Apollo cache.

Implementing authentication

Provide/inject pattern

To implement our authentication layer we will use provide/inject pattern. It allows to provide data in one component and then inject this data in any part of its components subtree. Let’s say we have the following structure:

<AuthProvider>
  <ChildComponent>
		<GrandChildComponent />
  </ChildComponent>
  <SiblingComponent>
	  <SiblingsChildComponent />
	</SiblingComponent>
</AuthProvider>

Using provide/inject we can access data from AuthProvider in any of its descendents without props drilling. The provided data will also be reactive.

AuthProvider

Let’s start by creating our src/providers/AuthProvider/AuthProvider.vue component

<script lang="ts" setup>
import { AUTH_PROVIDER_KEY } from "@/constants";

provide(AUTH_PROVIDER_KEY, {});
</script>

<template>
  <slot />
</template>

define  AUTH_PROVIDER_KEY in src/constants.ts and export

export const AUTH_PROVIDER_KEY = Symbol() as InjectionKey<{}>;

Provide function accepts 2 arguments: key (we can later inject the data using this same key) and value. Take a note at how we define our provider key in src/constants.ts ; Casting to InjectionKey will allow us to add correct types to the injected data.

To be able to inject auth data into our components, we’re adding a custom composable useAuth

import { inject } from "vue";
import { AUTH_PROVIDER_KEY } from "@/constants";

export function useAuth() {
  const store = inject(AUTH_PROVIDER_KEY);

  if (!store) {
    throw new Error("Auth store has not been instantiated");
  }

  return store;
}

If this composable is used outside of AuthProvider subtree then store will be undefined and we’ll throw an error in this case. We can now inject our auth store like this:

<script setup lang="ts">
import useAuth from "@/providers/AuthProvider/useAuth"

const authStore = useAuth() // authStore is fully typed here
</script>

Let’s also wrap our whole application with this provider. Update src/App.vue

<script lang="ts" setup>
import AuthProvider from "@/providers/AuthProvider/AuthProvider.vue";
</script>

<template>
  <AuthProvider>
    <RouterView />
  </AuthProvider>
</template>

Now our full component tree will have access to the data from AuthProvider

Prepare graphql utils

We’re ready to start integrating with our mock auth server. But to make working with GraphQL a little easier let’s install one additional util: GraphQL Code Generator, and 2 plugins for it: typescript and typescript-operations. This will allow us to generate typescript types based on our GraphQL schema.

Install dependencies:

yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations

Add codegen.yml file with the following settings

schema: '<http://localhost:4000/graphql>'
documents: 'src/**/*.gql'
generates:
  src/types/graphql.ts:
    plugins:
      - 'typescript'
      - 'typescript-operations'

To break down options used:

  • schema - it tells graphql-codegen where it should take GraphQL schema from. Here, we'll specify our GraphQL endpoint.
  • documents - a glob pattern specifying where our queries and mutations are. graphql-codegen will scan these files and create typescript types based on them
  • generates - location of the output types file and which plugins should be used

Since we still don’t have any client GraphQL operations, running graphql-codegen will not provide any meaningful output. We’ll get back to it in a minute.

SignUp Mutation

Let’s start by adding a mutation to sign up a new user. Add src/providers/AuthProvider/apollo/SignUp.gql. signUp mutation accepts email and password as input and returns a json web token if the user was successfully created

mutation SignUp($input: AuthInput!) {
    signUp(input: $input) {
        token
    }
}

Now run yarn graphql-codegen. You will find a new file src/types/graphql.ts with exported members SignUpMutation and SignUpMutationVariables, we can now use them to type our GraphQL operations. Let’s update our AuthProvider.vue

<script lang="ts" setup>
import { useMutation } from "@vue/apollo-composable";
import type {
  SignUpMutation,
  SignUpMutationVariables,
} from "@/types/graphql";
import SignUp from "./apollo/SignUp.gql";
import { provide } from "vue";
import { AUTH_PROVIDER_KEY } from "@/constants";

const { mutate: signUp } = useMutation<SignUpMutation, SignUpMutationVariables>(
  SignUp,
  {
    update(cache, { data }) {
      if (data?.signUp?.token) {
        localStorage.setItem(LOCALSTORAGE_TOKEN_KEY, data.signUp.token);
      }
    },
  }
);

provide(AUTH_PROVIDER_KEY, {
  signUp,
});
</script>

<template>
  <slot />
</template>

And update src/constants.ts with LOCALSTORAGE_TOKEN_KEY and a type for our signUp function

import { ComputedRef, InjectionKey, Ref } from "vue";
import { MutateFunction } from "@vue/apollo-composable";
import {
  SignUpMutation,
  SignUpMutationVariables,
} from "./types/graphql";

// Provide/inject keys
export const AUTH_PROVIDER_KEY = Symbol() as InjectionKey<{
  signUp: MutateFunction<SignUpMutation, SignUpMutationVariables>;
}>;

// app constants
export const LOCALSTORAGE_TOKEN_KEY = "token";

We pass additional options object with update function to useMutation composable. This update function will be called after the mutation is resolved and has access to the data returned from the mutation. After we receive the response, we save our token in the localStorage.

The standard way of using JSON web tokens is by attaching them to Authorization header, so we're going to attach this token to every subsequent GraphQL request.

In order to do this we need to update src/plugins/apollo.ts

import { setContext } from "@apollo/client/link/context";
import {
  ApolloClient,
  createHttpLink,
  InMemoryCache,
} from "@apollo/client/core";
import { LOCALSTORAGE_TOKEN_KEY } from "@/constants";

const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem(LOCALSTORAGE_TOKEN_KEY);

  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    },
  };
});

const httpLink = createHttpLink({
  uri: import.meta.env.VITE_GRAPHQL_URL,
});
const cache = new InMemoryCache();
const apolloClient = new ApolloClient({
  link: authLink.concat(httpLink),
  cache,
});

export default apolloClient;

We’ve created a series of ApolloLinks here. You can think about ApolloLinks chain as a series of middlewares. Before our GraphQL operation hit the server it passes through a series of intermediary handlers and each of them can modify the operation in a certain way. What our authLink does is, it looks for the token in localStorage and if it finds one, it updates authorization request header with it.

Let’s now use our signUp mutation in AuthForm.vue. We can also use SignUpMutationVariables to corectly type our form reactive variable.

<script setup lang="ts">
import { reactive } from "vue";
import { useAuth } from "@/providers/AuthProvider/useAuth";
import TextInput from "@/components/TextInput/TextInput.vue";
import BaseButton from "@/components/BaseButton/BaseButton.vue";
import { SignUpMutationVariables } from "@/types/graphql";

const { signUp } = useAuth();

const form = reactive<SignUpMutationVariables>({
  input: {
    email: "",
    password: "",
  },
});
</script>

<template>
  <div class="flex flex-col items-center justify-center h-screen">
    <section class="p-4 bg-gray-100 rounded-lg border">
      <form class="grid gap-6">
        <div>
          <TextInput
              placeholder="john@doe.com"
              v-model="form.input.email"
              autofocus
          >Email:
          </TextInput>
        </div>
        <div>
          <TextInput v-model="form.input.password" type="password"
          >Password:
          </TextInput>
        </div>
        <div>
          <BaseButton @click.prevent="signUp(form)">Sign Up</BaseButton>
          <BaseButton>Sign In</BaseButton>
        </div>
      </form>
    </section>
  </div>
</template>

Now when we click on Sign Up button we will send a gql mutation and get a token in the response

Untitled

We can also see that our token is correctly saved in localStorage

Untitled

CurrentUser query and SignIn mutation

Next, let’s add a new SignIn mutation. It is very similar to signUp as it also accepts an email and password and returns a token.

mutation SignIn($input: AuthInput!) {
    signIn(input: $input) {
        token
    }
}

Our server also knows how to validate jwt from Authorization header. If the token is valid it will return a user object, if it’s not, it will return an object with empty id and email, meaning that the user is not authenticated. Let’s add src/providers/AuthProvider/apollo/CurrentUser.gql query to fetch current user.

query CurrentUser {
    currentUser {
        id
        email
    }
}

Run yarn graphql-codegen again to update our types and then we can use these new operations in our AuthProvider

<script lang="ts" setup>
import { computed, provide } from "vue";
import { useMutation, useQuery } from "@vue/apollo-composable";
import type {
  CurrentUserQuery,
  CurrentUserQueryVariables,
  SignUpMutation,
  SignUpMutationVariables,
  SignInMutation,
  SignInMutationVariables,
} from "@/types/graphql";
import { AUTH_PROVIDER_KEY, LOCALSTORAGE_TOKEN_KEY } from "@/constants";
import CurrentUser from "./apollo/CurrentUser.gql";
import SignUp from "./apollo/SignUp.gql";
import SignIn from "./apollo/SignIn.gql";

const { result: user, loading: authLoading } = useQuery<
  CurrentUserQuery,
  CurrentUserQueryVariables
>(CurrentUser);

const { mutate: signUp } = useMutation<SignUpMutation, SignUpMutationVariables>(
  SignUp,
  {
    update(cache, { data }) {
      if (data?.signUp?.token) {
        localStorage.setItem(LOCALSTORAGE_TOKEN_KEY, data.signUp.token);
      }
    },
    refetchQueries: ["CurrentUser"],
  }
);

const { mutate: signIn } = useMutation<SignInMutation, SignInMutationVariables>(
  SignIn,
  {
    update(cache, { data }) {
      if (data?.signIn?.token) {
        localStorage.setItem(LOCALSTORAGE_TOKEN_KEY, data.signIn.token);
      }
    },
    refetchQueries: ["CurrentUser"],
  }
);

provide(AUTH_PROVIDER_KEY, {
  user: computed(() => user.value?.currentUser),
  isLoggedIn: computed(() => !!user.value?.currentUser?.id),
  authLoading,
  signUp,
  signIn,
});
</script>

<template>
  <slot />
</template>

And let’s also update authStore types in src/constants.ts

import { ComputedRef, InjectionKey, Ref } from "vue";
import { MutateFunction } from "@vue/apollo-composable";
import {
  CurrentUserQuery,
  SignInMutation,
  SignInMutationVariables,
  SignUpMutation,
  SignUpMutationVariables,
} from "./types/graphql";

// Provide/inject keys
export const AUTH_PROVIDER_KEY = Symbol() as InjectionKey<{
  user: ComputedRef<CurrentUserQuery["currentUser"] | undefined>;
  isLoggedIn: ComputedRef<boolean>;
  authLoading: Ref<boolean>;
  signUp: MutateFunction<SignUpMutation, SignUpMutationVariables>;
  signIn: MutateFunction<SignInMutation, SignInMutationVariables>;
}>;

// app constants
export const LOCALSTORAGE_TOKEN_KEY = "token";

Since AuthProvider is at the very root of our components tree, CurrentUser query will be invoked as soon as the application loads.

Notice that we’ve also added a new option to signUp and signIn mutations: refetchQueries. It tells the Apollo client which queries we need to refetch after a particular mutation. We’re passing “CurrentUser" query in both cases since we want to fetch user data immediately after we sign them in or sign them up.

We can now add the signIn mutation to our AuthForm. It’s as simple as just injecting signIn from our AuthProvider

<script setup lang="ts">
import { reactive } from "vue";
import { useAuth } from "@/providers/AuthProvider/useAuth";
import TextInput from "@/components/TextInput/TextInput.vue";
import BaseButton from "@/components/BaseButton/BaseButton.vue";

const { signUp, signIn } = useAuth();

const form = reactive({
  input: {
    email: "",
    password: "",
  },
});
</script>

<template>
  <div class="flex flex-col items-center justify-center h-screen">
    <section class="p-4 bg-gray-100 rounded-lg border">
      <form class="grid gap-6">
        <div>
          <TextInput
            placeholder="john@doe.com"
            v-model="form.input.email"
            autofocus
            >Email:
          </TextInput>
        </div>
        <div>
          <TextInput v-model="form.input.password" type="password"
            >Password:
          </TextInput>
        </div>
        <div>
          <BaseButton @click.prevent="signUp(form)">Sign Up</BaseButton>
          <BaseButton @click.prevent="signIn(form)">Sign In</BaseButton>
        </div>
      </form>
    </section>
  </div>
</template>

Remember, we wanted to close Protected page to logged out users and Home page to logged in users? Well, now we can do that!

Let’s update src/pages/Home.vue and src/pages/Protected.vue

<script setup lang="ts">
import { useRouter } from "vue-router";
import { useAuth } from "@/providers/AuthProvider/useAuth";
import AuthForm from "@/components/AuthForm/AuthForm.vue";
import { watchEffect } from "vue";

const router = useRouter();
const { isLoggedIn } = useAuth();

watchEffect(() => {
  if (isLoggedIn.value) router.push("/protected");
});
</script>

<template>
  <div class="container mx-auto">
    <AuthForm />
  </div>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import { useAuth } from "@/providers/AuthProvider/useAuth";
import { watchEffect } from "vue";

const router = useRouter();
const { user, isLoggedIn } = useAuth();

watchEffect(() => {
  if (!isLoggedIn.value) router.push("/");
});
</script>

<template>
  <section class="flex h-screen flex-col items-center justify-center">
    <h1 class="mb-2 text-2xl">Protected page</h1>
    <pre class="mb-2">{{ user }}</pre>
  </section>
</template>

The pages are watching isLoggedIn , a computed variable from AuthProvider and would redirect the user to the next page if the state of this variable changes according to the predefined condition. This means that now, our pages are properly hidden behind auth and also the users are correctly redirected after a successful log in/log out.

Untitled

Log out action

Let's not forget that we still need need to implement the logOut action. Let's update AuthProvider.vue component and types in constants.ts

<script lang="ts" setup>
import { useMutation, useQuery, useApolloClient } from "@vue/apollo-composable";
import type {
  CurrentUserQuery,
  CurrentUserQueryVariables,
  SignUpMutation,
  SignUpMutationVariables,
  SignInMutation,
  SignInMutationVariables,
} from "@/types/graphql";
import CurrentUser from "./apollo/CurrentUser.gql";
import SignUp from "./apollo/SignUp.gql";
import SignIn from "./apollo/SignIn.gql";
import { computed, provide } from "vue";
import { AUTH_PROVIDER_KEY, LOCALSTORAGE_TOKEN_KEY } from "@/constants";

const { resolveClient } = useApolloClient();

const { result: user, loading: authLoading } = useQuery<
  CurrentUserQuery,
  CurrentUserQueryVariables
>(CurrentUser);

const { mutate: signUp } = useMutation<SignUpMutation, SignUpMutationVariables>(
  SignUp,
  {
    update(cache, { data }) {
      if (data?.signUp?.token) {
        localStorage.setItem(LOCALSTORAGE_TOKEN_KEY, data.signUp.token);
      }
    },
    refetchQueries: ["CurrentUser"],
  }
);

const { mutate: signIn } = useMutation<SignInMutation, SignInMutationVariables>(
  SignIn,
  {
    update(cache, { data }) {
      if (data?.signIn?.token) {
        localStorage.setItem(LOCALSTORAGE_TOKEN_KEY, data.signIn.token);
      }
    },
    refetchQueries: ["CurrentUser"],
  }
);

async function logOut() {
  localStorage.removeItem(LOCALSTORAGE_TOKEN_KEY);
  const apolloClient = resolveClient();
  await apolloClient.clearStore();
  await apolloClient.resetStore();
}

provide(AUTH_PROVIDER_KEY, {
  user: computed(() => user.value?.currentUser),
  isLoggedIn: computed(() => !!user.value?.currentUser?.id),
  authLoading,
  signUp,
  signIn,
  logOut,
});
</script>

<template>
  <slot />
</template>
import { ComputedRef, InjectionKey, Ref } from "vue";
import { MutateFunction } from "@vue/apollo-composable";
import {
  CurrentUserQuery,
  SignInMutation,
  SignInMutationVariables,
  SignUpMutation,
  SignUpMutationVariables,
} from "./types/graphql";

// Provide/inject keys
export const AUTH_PROVIDER_KEY = Symbol() as InjectionKey<{
  user: ComputedRef<CurrentUserQuery["currentUser"] | undefined>;
  isLoggedIn: ComputedRef<boolean>;
  authLoading: Ref<boolean>;
  signUp: MutateFunction<SignUpMutation, SignUpMutationVariables>;
  signIn: MutateFunction<SignInMutation, SignInMutationVariables>;
  logOut: () => void;
}>;

// app constants
export const LOCALSTORAGE_TOKEN_KEY = "token";

Our logOut function does 2 things:

  1. removes the auth token from localStorage. After we remove the token all, subsequent requests to GraphQL endpoint will be sent without an Authorization header.
  2. clears the Apollo cache and refetches active queries. Once that is done, our user object will be cleared and the user will be redirected to the homepage with the auth form.

Now, the only thing left is to add this button to our Protected.vue page:

<script setup lang="ts">
import { useRouter } from "vue-router";
import { useAuth } from "@/providers/AuthProvider/useAuth";
import { watchEffect } from "vue";
import BaseButton from "@/components/BaseButton/BaseButton.vue";

const router = useRouter();
const { user, isLoggedIn, logOut } = useAuth();

watchEffect(() => {
  if (!isLoggedIn.value) router.push("/");
});
</script>

<template>
  <section class="flex h-screen flex-col items-center justify-center">
    <h1 class="mb-2 text-2xl">Protected page</h1>
    <pre class="mb-2">{{ user }}</pre>
    <BaseButton type="submit" @click="logOut">Log out</BaseButton>
  </section>
</template>

With these changes once the user clicks on the Log out button on the Protected page, they will be immediately redirected to the Home page.

And that's it, we have successfully implemented client-side authentication with Vue3 + Vite + Apollo + GraphQL!

Links

You can find the full code of this application in the following repositories:

Sergey Khval

Sergey Khval

Engineering Manager / Tech Lead at Bravado
Mofope Ojosipe-Isaac

Mofope Ojosipe-Isaac

Frontend Engineer at Bravado