Ionic 5 Tutorial: OAuth2 Login Example (Vue)

by Didin J., updated on Nov 28, 2020 Ionic 5 Tutorial: OAuth2 Login Example (Vue)

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

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

This tutorial 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. Terminal or Node Command Line
  7. IDE or Text Editor

Before start to the main steps, make sure you have installed the 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
v12.18.0
npm -v
6.14.7
yarn -v
1.22.5

Let's get started with the main steps!


Step #1: Create a New Ionic 5 Application

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

sudo npm install -g @ionic/cli

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

ionic start ionicVueOauth tabs --type=vue

Leave all questions as default then here's the Ionic 5 Vue.js project structure.

.
|-- babel.config.js
|-- capacitor.config.json
|-- cypress.json
|-- ionic.config.json
|-- jest.config.js
|-- node_modules
|-- package-lock.json
|-- package.json
|-- public
|   |-- assets
|   |   |-- icon
|   |   |   |-- favicon.png
|   |   |   `-- icon.png
|   |   `-- shapes.svg
|   `-- index.html
|-- src
|   |-- App.vue
|   |-- components
|   |   `-- ExploreContainer.vue
|   |-- main.ts
|   |-- router
|   |   `-- index.ts
|   |-- shims-vue.d.ts
|   |-- theme
|   |   `-- variables.css
|   `-- views
|       |-- Tab1.vue
|       |-- Tab2.vue
|       |-- Tab3.vue
|       `-- Tabs.vue
|-- tests
|   |-- e2e
|   |   |-- plugins
|   |   |   `-- index.js
|   |   |-- specs
|   |   |   `-- test.js
|   |   `-- support
|   |       |-- commands.js
|   |       `-- index.js
|   `-- unit
|       `-- example.spec.ts
`-- tsconfig.json

To working this Ionic 5 Vue.js app, open this project with your IDE or Text Editor. To run this application for the first time, type this command.

ionic serve -l

Install the @ionic/lab module if it's required to run as the lab in the browser. Here it is, the Ionic 5 and Vue.js application look like.

Ionic 5 Tutorial: OAuth2 Login Example (Vue) - Ionic Tabs

Stop the running Ionic 5 application by press CTRL+C.


Step #2: Install and Configure All Required Modules

To access the OAuth2 server via REST API, we will use the Axios module. For state management pattern, 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 stringify 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 working. Next, configure Vuex to working with Ionic-Vue and Typescript codes by open and edit src/shims-vue.d.ts then add this line at the end.

declare module 'vuex'

Next, for all configurations that use by 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 re-use API calls logic, we will create all the required services. For that, create the 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

We will put Axios and it's 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 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 Authorization token that 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 request.

    get(resource: string) {
        return axios.get(resource);
    },

    post(resource: string, data: any) {
        return axios.post(resource, data);
    },

    put(resource: string, data: any) {
        return axios.put(resource, data);
    },

    delete(resource: string) {
        return axios.delete(resource);
    },

    customRequest(data: AxiosRequestConfig) {
        return axios(data);
    },

Add the methods or function to intercept the HTTP request then show the Ionic/Vue loadingController.

    mountRequestInterceptor() {
        this._requestInterceptor = axios.interceptors.request.use(async config => {
            console.log("show loading");
            const loading = await loadingController.create({
                message: 'Please wait...'
            });
            await loading.present();

            return config;
        });
    },

Add the methods or function to intercept the HTTP response with 401 status. So, the captured 401 status will redirect to the login page by calling the signOut function. Otherwise, the 401 status will treat as the expired token that will trigger the refreshToken function. 

    mount401Interceptor() {
        this._401interceptor = axios.interceptors.response.use(
            response => {
                loadingController.dismiss().then(r => console.log(r));
                return response;
            },
            async error => {
                loadingController.dismiss().then(r => console.log(r));
                if (error.request.status === 401) {
                    if (error.config.url.includes("oauth/token")) {
                        await store.dispatch("auth/signOut");
                        throw error;
                    } else {
                        try {
                            await store.dispatch("auth/refreshToken");
                            return this.customRequest({
                                method: error.config.method,
                                url: error.config.url,
                                data: error.config.data
                            });
                        } catch (e) {
                            throw error;
                        }
                    }
                }
                throw error;
            }
        );
    },

Add the methods or function to eject the HTTP 401 response status interceptor.

    unmount401Interceptor() {
        axios.interceptors.response.eject(this._401interceptor);
    } 

Export above constant functions as a Typescript module.

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/auth.service.ts then add these required imports.

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

Create a class after the imports that handle the authentication error.

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;
    }
}

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 using URL encoded form and the content of the body form is a stringified JSON object. The response from the server are a user ID, access token, refresh token, and their expiration date. The access token and refresh token will save to the local storage. Also, the HTTP interceptor 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 refresh token. This function similar to the previous function except for the request body that just uses grant_type and refreshes token value.

    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 sign out or logout the logged-in user by clean 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 to the OAuth2 server. This time, the request body just using a plain or raw body with JSON format.

    signup: async function(email: any, password: any, name: any) {
        const signupData: AxiosRequestConfig = {
            method: "post",
            headers: { "Content-Type": "application/json" },
            url: "/oauth/signup",
            data: {
                email: email,
                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.

    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);
    }

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) {
            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();
}

The API service uses the variable from the .env file that previously created.


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 {AuthenticationError, AuthService} from "@/services/auth.service";
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((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, {email, password, name}: any) {
        try {
            await AuthService.signup(email, 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 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) {
            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 using the existing page as the secure page to the views. Before that, create the new Vue files inside the src/views.

touch src/views/Signin.vue
touch src/views/Signup.vue

Next, open and edit src/views/Signin.vue then add the Ionic-Vue template that contains a login form with username and password input text, sign-in button to submit the form, and 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>

Add a Vue script tag with Typescript language.

<script lang="ts">
</script>

Inside that script, the tag adds these imports of the required components in the template and in the scripts.

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';

Add the main exported Vue module that contains the name, components that use by template, setup the Vue router, the form data, instantiate the getters from auth store module, and the method to submit the form.

export default {
  name: 'SignIn',
  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()
      })
    }
  }
}

Next. open and edit src/views/Signup.vue then add these lines of 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: 'Signup',
  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>

Finally, modify the existing src/views/Tab1.vue then replace all Vue template and script to 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: 'Tab1',
  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>


Step #6: Run and Test Ionic 5 (Vue) OAuth2 Application

Before running the Ionic Vue app, run the PostgreSQL server and Express OAuth2 server in the separate Terminal tabs. After, everything running, 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 xcworkspace file from your XCode then run the iOS application to the simulator from the XCode. Next, to run the Ionic Vue apps to 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 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 to the Android device again. And here are the Ionic Vue Oauth2 apps look like.

Ionic 5 Tutorial: OAuth2 Login Example (Vue) - Demo

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

Thanks!

Loading…