Ionic Vue Tutorial: OAuth2 Login Example

by Didin J. on Mar 06, 2023 Ionic Vue Tutorial: OAuth2 Login Example

The comprehensive step-by-step Ionic Vue tutorial on building secure mobile apps that login or authenticate to the OAuth2 server

In this Ionic Vue tutorial, we will try to build secure Ionic Vue mobile apps that access or login into the OAuth2 server. This time, we are using Vue.js and Vuex to build and manage the logic for this mobile application. To access the REST API (OAuth2 server), we will use the Axios module. After all, these mobile apps will build for Android and iOS using Capacitor.

This tutorial is divided into several steps:

The following tools, frameworks, and modules are required for this tutorial:

  1. Node.js (with NPM or Yarn)
  2. Ionic CLI
  3. Vue.js and Vuex
  4. Axios
  5. Node-Express-MongoDB OAuth2 Server
  6. PostgreSQL Server
  7. Terminal or Node Command Line
  8. IDE or Text Editor

Before starting the main steps, make sure you have installed Node.js and can run NPM or Yarn. To check the installed Node.js (NPM or Yarn) type these commands from the Terminal or Node command line.

node -v
v18.14.2
npm -v
9.5.0
yarn -v
1.22.5

You can watch the video tutorial on our YouTube channel here.

Let's get started with the main steps!


Step #1: Create a New Ionic Vue Application

As we mentioned in the previous paragraph, we will use Ionic CLI to create an Ionic Vue project or application. Type this command to install the latest Ionic application.

npm install -g @ionic/cli

To create a new Ionic Vue and Tabs mode, type this command.

ionic start ionicVueOauth tabs --type=vue

Leave all questions as default then go to the newly created Ionic Vue project folder.

cd ionicVueOauth 

Now, you can open this project using your IDE.

code .

Or run this application for the first time by typing this command.

ionic serve

Open your Chrome, and go to  http://localhost:8100/. Then, open the Chrome developer tools by pressing Ctrl+Shift+I on Windows/Linux or Cmd+Opt+I on Mac. Click the Toggle Device Toolbar or Ctrl+Shift+M to show the device view.

Ionic Vue Tutorial: OAuth2 Login Example - ionic welcome

Stop the running Ionic Vue application by pressing CTRL+C.


Step #2: Install the Required Modules and Components

To access the OAuth2 server via REST API, we will use the Axios module. For state management patterns, we will use Vuex. Vuex will use to simplify the API call to the OAuth2 server. So, there is no direct call to the OAuth2 server from the Components. Also, we need a stringified JSON object by using QS. To install all of them, type this command.

npm install axios vuex qs --save

Make sure the Vuex version is using 4.0.0-0 and above to make this tutorial work. Next, configure Vuex to work with Ionic-Vue and Typescript codes by opening and editing src/shims-vue.d.ts and then adding this line at the end.

declare module 'vuex'

Next, for all configurations that use API and OAuth2 access, create a new file at the root of the project folder.

touch .env

Fill that file with this configuration variable.

VUE_APP_NAME="IonicVueOauth"
VUE_APP_ROOT_API="http://192.168.0.7:3000"
VUE_APP_CLIENT_ID="express-client"
VUE_APP_CLIENT_SECRET="express-secret"


Step #3: Create a Vue Service

To separate and reuse API call logic, we will create all the required services. For that, create a new folder and files.

mkdir src/services
touch src/services/api.service.ts
touch src/services/auth.service.ts
touch src/services/home.service.ts
touch src/services/token.service.ts
touch src/services/authentication.error.ts

We will put Axios and its functionality in the api.service.ts. So, the other services will use the same function to do requests to the RESP API. Except, token.service.ts is used to manage the OAuth access token and refresh token. Next, open and edit src/services/api.service.ts then add these imports.

import axios, { AxiosRequestConfig } from "axios";
import {store} from '@/store';
import {TokenService} from "@/services/token.service";
import {loadingController} from '@ionic/vue';

We will add the required imported class later. Next, add a constant variable of ApiService.

const ApiService = {
}

Inside the constant variable body, add these local variables that use for HTTP interceptor operation.

    _requestInterceptor: 0,
    _401interceptor: 0,

Add a method or function to initialize the Axios base REST API URL.

    init(baseURL: string | undefined) {
        axios.defaults.baseURL = baseURL;
    },

Add a method or function to intercept the HTTP request headers parameter for the Authorization token received after the successful login.

    setHeader() {
        axios.defaults.headers.common[
            "Authorization"
            ] = `Bearer ${TokenService.getToken()}`;
    },

Add a method or function to clear the HTTP request headers.

    removeHeader() {
        axios.defaults.headers.common = {};
    },

Add the methods or functions to implement the Axios GET, POST, PUT, DELETE, and custom requests.

export default ApiService;

Next, open and edit src/services/token.service.ts then add these constant variables to hold the access_token and refresh_token names.

const TOKEN_KEY = "access_token";
const REFRESH_TOKEN_KEY = "refresh_token";

Add a main constant variable that holds all the required functions or methods.

const TokenService = {
}

Inside that variable or main function body, add this CRUD (create, read, update, delete) function to populate the access token and refresh token. For this tutorial, the tokens will save to the localStorage. For production, you can use the Ionic secure storage native library.

    getToken() {
        return localStorage.getItem(TOKEN_KEY);
    },

    saveToken(accessToken: string) {
        localStorage.setItem(TOKEN_KEY, accessToken);
    },

    removeToken() {
        localStorage.removeItem(TOKEN_KEY);
    },

    getRefreshToken() {
        return localStorage.getItem(REFRESH_TOKEN_KEY);
    },

    saveRefreshToken(refreshToken: string) {
        localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
    },

    removeRefreshToken() {
        localStorage.removeItem(REFRESH_TOKEN_KEY);
    }

Export that main constant as a Typescript module.

export { TokenService };

Next, open and edit src/services/authentication.error.ts then add these Typescript codes.

class AuthenticationError extends Error {
    errorCode: any;
    constructor(errorCode: any, message: string | undefined) {
        super(message);
        this.name = this.constructor.name;
        if (message != null) {
            this.message = message;
        }
        this.errorCode = errorCode;
    }
}

export default AuthenticationError;

Next, open and edit src/services/auth.service.ts then add these required imports.

import ApiService from "./api.service";
import AuthenticationError from "./authentication.error";
import { TokenService } from "./token.service";
import { AxiosRequestConfig } from "axios";
import qs from "qs";

Create a main constant variable that contains all the required functions for authentication to the OAuth2 server.

const AuthService = {
}

Inside that main constant, add an asynchronous function to request the authentication token to the OAuth2 server. The request contains Authorization headers with the encrypted OAuth2 client id and secret values. The request body uses a URL-encoded form and the content of the body form is a stringified JSON object. The response from the server is a user ID, access token, refresh token, and expiration date. The access token and refresh token will save to the local storage. Also, the HTTP interceptor was activated to catch the 401 error status.

    signIn: async function(signInData: any) {
        const requestData: AxiosRequestConfig = {
            method: "post",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
                Authorization: 'Basic ' + btoa(process.env.VUE_APP_CLIENT_ID + ':' + process.env.VUE_APP_CLIENT_SECRET)
            },
            url: "/oauth/token",
            data: qs.stringify({
                "grant_type": "password",
                username: signInData.username,
                password: signInData.password
            })
        };

        try {
            const response = await ApiService.customRequest(requestData);
            TokenService.saveToken(response.data.access_token);
            TokenService.saveRefreshToken(response.data.refresh_token);
            ApiService.setHeader();

            ApiService.mount401Interceptor();

            return response.data.access_token;
        } catch (error) {
            this.catchError(error);
        }
    }, 

Add a function to request the new token by refreshing the token. This function is similar to the previous function except for the request body which just uses grant_type and refreshes the token value.

    refreshToken: async function() {
        const refreshToken = TokenService.getRefreshToken();

        const requestData: AxiosRequestConfig = {
            method: "post",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
                Authorization: 'Basic ' + btoa(process.env.VUE_APP_CLIENT_ID + ':' + process.env.VUE_APP_CLIENT_SECRET)
            },
            url: "/oauth/token",
            data: qs.stringify({
                "grant_type": "refresh_token",
                refreshToken: refreshToken
            })
        };

        try {
            const response = await ApiService.customRequest(requestData);

            TokenService.saveToken(response.data.access_token);
            TokenService.saveRefreshToken(response.data.refresh_token);
            ApiService.setHeader();

            return response.data.access_token;
        } catch (error:any) {
            throw new AuthenticationError(
                error.response.status,
                error.response.data.error_description
            );
        }
    },

Add a function to sign out or log out the logged-in user by cleaning up the tokens and request headers only.

    signOut() {
        TokenService.removeToken();
        TokenService.removeRefreshToken();
        ApiService.removeHeader();
        ApiService.unmount401Interceptor();
    },

Add a function to request POST data to create a new user for the OAuth2 server. This time, the request body just uses a plain or raw body with JSON format.

    signup: async function(username: any, password: any, name: any) {
        const signupData: AxiosRequestConfig = {
            method: "post",
            headers: { "Content-Type": "application/json" },
            url: "/oauth/signup",
            data: {
                username: username,
                password: password,
                name: name
            }
        };

        try {
            return await ApiService.customRequest(signupData);
        } catch (error) {
            this.catchError(error);
        }
    },

Add a function that calls from the previous functions when a request to the API failed.

    catchError: function(error: any) {
        let status;
        let description;

        if (error.response === undefined) {
            status = error.message;
            description = error.message;
        } else {
            status = error.response.status;
            description = error.response.data.error_description;
        }

        throw new AuthenticationError(status, description);
    }

Export that main constant as a Typescript module.

export default AuthService;

Next, open and edit src/services/home.service.ts then add these Vue-Typescript codes that contain the request to the secure API endpoint.

import ApiService from "./api.service";

class ResponseError extends Error {
    errorCode: any;
    errorMessage: any;
    constructor(errorCode: any, message: string | undefined) {
        super(message);
        this.name = this.constructor.name;
        if (message != null) {
            this.message = message;
        }
        this.errorCode = errorCode;
    }
}

const HomeService = {
    secretArea: async function() {
        try {
            return ApiService.get("/secret");
        } catch (error: any) {
            throw new ResponseError(
                error.status,
                error.error.message
            );
        }
    }
}

export { HomeService, ResponseError };

Next, add or register the above services to the main.ts after the app's constant.

ApiService.init(process.env.VUE_APP_ROOT_API);

if (TokenService.getToken()) {
  ApiService.setHeader();
  ApiService.mountRequestInterceptor();
  ApiService.mount401Interceptor();
}

Don't forget to add these imports.

import ApiService from './services/api.service';
import { TokenService } from './services/token.service';


Step #4: Create Vuex Store

To manage the state and calls to the services, create a folder and files like this.

mkdir src/store
touch src/store/auth.store.ts
touch src/store/home.store.ts
touch src/store/index.ts

Next, open and edit src/store/auth.store.ts then add these imports.

import AuthService from "@/services/auth.service";
import AuthenticationError from "@/services/authentication.error";
import { TokenService } from "@/services/token.service";

Add a constant variable that contains the key-value pair of the state objects.

const state = {
    authenticating: false,
    accessToken: TokenService.getToken(),
    authenticationErrorCode: 0,
    authenticationError: "",
    refreshTokenPromise: null
};

Add a constant variable to get the required states. This time, we just return the authenticating, authentication error code, and error message.

const getters = {
    authenticationErrorCode: (state: { authenticationErrorCode: any }) => {
        return state.authenticationErrorCode;
    },

    authenticationError: (state: { authenticationError: any }) => {
        return state.authenticationError;
    },

    authenticating: (state: { authenticating: any }) => {
        return state.authenticating;
    }
};

Add the action constant variable that contains the operations to handle the states of login, register, refresh token, and logout. Also, call the commit of the state mutations. 

const actions = {
    async signIn(context: any, signInData: any) {
        context.commit("signInRequest");
        return new Promise((resolve, reject) => {
            AuthService.signIn(signInData).then(res => {
                context.commit("signInSuccess", res);
                resolve(res);
            }).catch(err => {
                if (err instanceof AuthenticationError) {
                    context.commit("signInError", {
                        errorCode: err.errorCode,
                        errorMessage: err.message
                    });
                    reject(err.message);
                }
            });
        });
    },

    signOut(context: any) {
        context.commit("signOutRequest");
        return new Promise<void>((resolve) => {
            AuthService.signOut();
            resolve();
        });
    },

    refreshToken(context: any, state: { refreshTokenPromise: any }) {
        if (!state.refreshTokenPromise) {
            const p = AuthService.refreshToken();
            context.commit("refreshTokenPromise", p);

            p.then(
                response => {
                    context.commit("refreshTokenPromise", null);
                    context.commit("loginSuccess", response);
                },
                error => {
                    context.commit("refreshTokenPromise", error);
                }
            );
        }

        return state.refreshTokenPromise;
    },

    async signup(context: any, {username, password, name}: any) {
        try {
            await AuthService.signup(username, password, name);
            context.commit("processSuccess");
            return true;
        } catch (e) {
            if (e instanceof AuthenticationError) {
                context.commit("signInError", {
                    errorCode: e.errorCode,
                    errorMessage: e.message
                });
            }
            return false;
        }
    },

    setAuthenticatingStatus(context: any, status: any) {
        context.commit("setAuthenticatingStatus", status);
    },
};

Add a constant variable that defines all commits that were previously called from the action.

const mutations = {
    signInRequest(state: {
        authenticating: boolean;
        authenticationError: string;
        authenticationErrorCode: number;
    }) {
        state.authenticating = true;
        state.authenticationError = "";
        state.authenticationErrorCode = 0;
    },

    signInSuccess(state: {
        accessToken: any;
        authenticating: boolean;
    }, accessToken: any) {
        state.accessToken = accessToken;
        state.authenticating = false;
    },

    signInError(state: {
        authenticating: boolean;
        authenticationErrorCode: any;
        authenticationError: any;
    }, {errorCode, errorMessage}: any) {
        state.authenticating = false;
        state.authenticationErrorCode = errorCode;
        state.authenticationError = errorMessage;
    },

    signOutRequest(state: { authenticating: boolean }) {
        state.authenticating = false;
    },

    refreshTokenPromise(state: { refreshTokenPromise: any }, promise: any) {
        state.refreshTokenPromise = promise;
    },

    processSuccess(state: { authenticating: boolean }) {
        state.authenticating = false;
    },

    setAuthenticatingStatus(state: { authenticating: any }, status: any) {
        state.authenticating = status;
    }
};

Export the main constant as the auth module that performs state, getters, actions, and mutations.

export const auth = {
    namespaced: true,
    state,
    getters,
    actions,
    mutations
};

Next, open and edit src/store/home.store.ts then add these lines of Vuex codes to handle the state of secure API only.

import { HomeService, ResponseError } from "@/services/home.service";

const state = {
    responseData: "",
    responseErrorCode: 0,
    responseError: "",
};

const getters = {
    responseErrorCode: (state: { responseErrorCode: any }) => {
        return state.responseErrorCode;
    },
    responseError: (state: { responseError: any }) => {
        return state.responseError;
    }
};

const actions = {
    async loadSecretArea(context: any) {
        context.commit("dataRequest");
        try {
            const resp = await HomeService.secretArea();
            context.commit("dataSuccess", resp);
            return resp;
        } catch (e: any) {
            if (e instanceof ResponseError) {
                context.commit("dataError", {
                    errorMessage: e.errorMessage,
                    responseErrorCode: e.errorCode
                });
            }
            return e.message;
        }
    }
};

const mutations = {
    dataRequest(state: {
        responseError: string;
        responseErrorCode: number;
    }) {
        state.responseError = "";
        state.responseErrorCode = 0;
    },
    dataSuccess(state: { responseData: string }, payload: any) {
        state.responseData = payload;
    },
    dataError(state: {
        responseError: any;
        responseErrorCode: any;
        }, {errorCode, errorMessage}: any) {
        state.responseError = errorMessage;
        state.responseErrorCode = errorCode;
    }
}

export const home = {
    namespaced: true,
    state,
    getters,
    actions,
    mutations
};

Next, open and edit src/store/index.ts then add the imports and register the previously created store modules.

import { createStore } from 'vuex';
import { auth } from "./auth.store";
import { home } from "./home.store";

export const store = createStore({
    state: {},
    mutations: {},
    actions: {},
    modules: {
        auth,
        home
    },
})

Finally, register that store to the main.ts by adding it to the app constant.

const app = createApp(App)
    .use(IonicVue)
    .use(router)
    .use(store);


Step #5: Implementing Login, Register, and Secure Page

Now, we will implement the login, register, and use the existing page as the secure page for the views. Before that, create the new Vue files inside the src/views.

touch src/views/SigninPage.vue
touch src/views/SignupPage.vue

Next, open and edit src/views/SigninPage.vue then add the Ionic-Vue template that contains a login form with username and password input text, a sign-in button to submit the form, and a signup button to go to the signup page.

<template>
  <ion-page>
    <form @submit.prevent="handleLogin">
      <ion-card>
        <ion-item>
          <h3>Please Sign In!</h3>
        </ion-item>
        <ion-item>
          <ion-label position="floating">Username</ion-label>
          <ion-input v-model="form.username" id="username" required></ion-input>
        </ion-item>

        <ion-item>
          <ion-label position="floating">Password</ion-label>
          <ion-input type="password" v-model="form.password" id="password" required></ion-input>
        </ion-item>

        <ion-item>
          <ion-button type="submit" shape="round">
            Sign In
            <ion-icon slot="end" :icon="logIn"></ion-icon>
          </ion-button>
        </ion-item>
        <ion-item>
          <p>Or</p>
        </ion-item>
        <ion-item>
          <ion-button type="button" shape="round" router-link="/signup">
            Sign Up
            <ion-icon slot="end" :icon="personAdd"></ion-icon>
          </ion-button>
        </ion-item>
      </ion-card>
    </form>
  </ion-page>
</template>

<script lang="ts">
import { IonPage, IonCard, IonItem, IonLabel, IonButton, IonInput, alertController, IonIcon } from '@ionic/vue'
import { logIn, personAdd } from 'ionicons/icons';
import { mapActions, mapGetters } from "vuex"
import { useRouter } from 'vue-router';

export default {
  name: 'SigninPage',
  components: { IonPage, IonCard, IonItem, IonLabel, IonButton, IonInput, IonIcon },
  setup() {
    const router = useRouter();
    return {
      router,
      logIn,
      personAdd
    };
  },
  data() {
    return {
      form: {
        username: "",
        password: ""
      }
    };
  },
  computed: {
    ...mapGetters("auth", [
      "authenticating",
      "authenticationError",
      "authenticationErrorCode"
    ])
  },
  methods: {
    ...mapActions("auth", ["signIn"]),
    async handleLogin() {
      this.signIn(this.form).then(() => {
        this.form.username = ""
        this.form.password = ""
        this.router.push("/tabs/tab1")
      }).catch(async (err: any) => {
        const errorAlert = await alertController
            .create({
              header: 'Failed',
              subHeader: 'Sign in Failed',
              message: err,
              buttons: ['OK'],
            });
        await errorAlert.present()
      })
    }
  }
}
</script>

Next. open and edit src/views/SignupPage.vue then adds these lines of the Vue template and Vue-Typescript script to handle the Signup form.

<template>
  <ion-page>
    <form @submit.prevent="handleSignup">
      <ion-card>
        <ion-item>
          <h3>Please Sign Up!</h3>
        </ion-item>
        <ion-item>
          <ion-label position="floating">Username</ion-label>
          <ion-input v-model="form.username" id="username" required></ion-input>
        </ion-item>

        <ion-item>
          <ion-label position="floating">Password</ion-label>
          <ion-input type="password" v-model="form.password" id="email" required></ion-input>
        </ion-item>

        <ion-item>
          <ion-label position="floating">Name</ion-label>
          <ion-input v-model="form.name" id="name" required></ion-input>
        </ion-item>

        <ion-item>
          <ion-button type="submit" shape="round">
            Sign Up
            <ion-icon slot="end" :icon="personAdd"></ion-icon>
          </ion-button>
        </ion-item>
        <ion-item>
          <p>Already have an Account?</p>
        </ion-item>
        <ion-item>
          <ion-button type="button" shape="round" router-link="/signin">
            Sign In
            <ion-icon slot="end" :icon="logIn"></ion-icon>
          </ion-button>
        </ion-item>
      </ion-card>
    </form>
  </ion-page>
</template>

<script lang="ts">
import { IonPage, IonCard, IonItem, IonLabel, IonButton, IonInput, alertController, IonIcon } from '@ionic/vue'
import { logIn, personAdd } from 'ionicons/icons';
import { mapActions, mapGetters } from "vuex"

export default {
  name: 'SignupPage',
  components: { IonPage, IonCard, IonItem, IonLabel, IonButton, IonInput, IonIcon },
  setup() {
    return {
      logIn,
      personAdd
    };
  },
  data() {
    return {
      form: {
        username: "",
        password: "",
        name: ""
      }
    };
  },
  computed: {
    ...mapGetters("auth", ["authenticationError", "authenticationErrorCode"])
  },
  methods: {
    ...mapActions("auth", ["signup"]),
    async handleSignup() {
      if (
        this.form.name &&
        this.form.username &&
        this.form.password
      ) {
        const registerUser = {
          name: this.form.name,
          username: this.form.username,
          password: this.form.password
        };
        this.signup(registerUser).then(async() => {
          const alert = await alertController
            .create({
              header: 'Success',
              subHeader: 'Signup Success',
              message: 'Your username signup successfully.',
              buttons: ['OK'],
            });
          this.form.name = ""
          this.form.username = ""
          this.form.password = ""
          await alert.present()
        }).catch((err: any) => {
          console.log(err)
        })
      } else {
        const errorAlert = await alertController
            .create({
              header: 'Failed',
              subHeader: 'Signup Failed',
              message: 'You are not fill the form completely.',
              buttons: ['OK'],
            });
        await errorAlert.present
      }
    },

  }
}
</script>

Next, modify the existing src/views/Tab1.vue then replaces all Vue templates and scripts with this.

<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Tab 1</ion-title>
        <ion-buttons slot="primary">
          <ion-button color="secondary" @click="handleSignOut">
            <ion-icon slot="icon-only" :icon="logOut"></ion-icon>
          </ion-button>
        </ion-buttons>
      </ion-toolbar>
    </ion-header>
    <ion-content :fullscreen="true">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">Tab 1</ion-title>
        </ion-toolbar>
      </ion-header>
      <ion-title>{{msg}}</ion-title>
      <ExploreContainer name="Tab 1 page" />
    </ion-content>
  </ion-page>
</template>

<script lang="ts">
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonIcon, IonButtons, IonButton } from '@ionic/vue';
import { logOut } from 'ionicons/icons';
import ExploreContainer from '@/components/ExploreContainer.vue';
import {mapActions} from "vuex";
import { useRouter } from 'vue-router';

export default  {
  name: 'Tab1Page',
  components: { ExploreContainer, IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonIcon, IonButtons, IonButton },
  data() {
    return {
      msg: ""
    }
  },
  setup() {
    const router = useRouter();
    return {
      router,
      logOut
    };
  },
  methods: {
    ...mapActions("auth", ["signOut"]),
    ...mapActions("home", ["loadSecretArea"]),
    async handleSignOut() {
      await this.signOut().then(() => {
        this.router.push("/login");
      });
    },
    async loadHomeData() {
      await this.loadSecretArea().then((res: any) => {
        this.msg = res.data;
      });
    },
    ionViewWillEnter() {
      this.loadHomeData();
    }
  }
}
</script>

Next, register the SigninPage and SignupPage to the Vue router src/router/index.ts and action to redirect the apps to the SigninPage if there's no token found. So, the whole src/router/index.ts will be like this.

import { createRouter, createWebHistory } from '@ionic/vue-router';
import { RouteRecordRaw } from 'vue-router';
import TabsPage from '../views/TabsPage.vue'
import SigninPage from "@/views/SigninPage.vue";
import SignupPage from "@/views/SignupPage.vue";
import { TokenService } from '@/services/token.service';

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    redirect: '/tabs/tab1'
  },
  {
    path: '/tabs/',
    component: TabsPage,
    children: [
      {
        path: '',
        redirect: '/tabs/tab1'
      },
      {
        path: 'tab1',
        component: () => import('@/views/Tab1Page.vue')
      },
      {
        path: 'tab2',
        component: () => import('@/views/Tab2Page.vue')
      },
      {
        path: 'tab3',
        component: () => import('@/views/Tab3Page.vue')
      },
      {
        path: '/login',
        component: SigninPage,
        meta: {
          public: true,
          onlyWhenLoggedOut: true
        }
      },
      {
        path: '/signup',
        component: SignupPage,
        meta: {
          public: true,
          onlyWhenLoggedOut: true
        }
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

router.beforeEach((to, from, next) => {
  const isPublic = to.matched.some(record => record.meta.public);
  const onlyWhenLoggedOut = to.matched.some(
      record => record.meta.onlyWhenLoggedOut
  );
  const loggedIn = !!TokenService.getToken();

  if (!isPublic && !loggedIn) {
    return next({
      path: "/login",
      query: { redirect: to.fullPath }
    });
  }

  if (loggedIn && onlyWhenLoggedOut) {
    return next("/tabs/tab1");
  }

  next();
});

export default router

 

Step #6: Run and Test Ionic Vue OAuth2 Application

Before running the Ionic Vue app, run the PostgreSQL server and Express OAuth2 server in the separate Terminal tabs. For more information about how to run this stand-alone OAuth2 server check this article. After, everything runs, back to the current terminal tab then run this command to run the Ionic Vue app to the iOS simulator.

ionic capacitor run ios

The XCode will start after building the iOS, or you can manually open the xc-workspace file from your XCode and then run the iOS application to the simulator from the XCode. Next, to run the Ionic Vue apps on the Android device, type this command.

ionic capacitor run android

The android studio will open then connect your Android device to your computer then run the Android app from the Android studio. If there's a network error when accessing the API from the device then open the android/app/src/main/AndroidManifest.xml then add this parameter to the <application> tag.

    <application
        ...
        android:usesCleartextTraffic="true">

Then run again the Android apps on the Android device again. And here are the Ionic Vue Oauth2 apps look like.

Ionic Vue Tutorial: OAuth2 Login Example - demo

That it's. the Ionic Vue Tutorial: OAuth2 Login Example. You can get the full source code from our GitHub.

We know that building beautifully designed Ionic apps from scratch can be frustrating and very time-consuming. Check Ionic 6 - Full Starter App and save development and design time. Android, iOS, and PWA, 100+ Screens and Components, the most complete and advanced Ionic Template.

That is just the basic. If you need more deep learning about Ionic, Angular, and Typescript, you can take the following cheap course:

Thanks!