Skip to main content
Version: 3.x

Auth Connect Vue

In this tutorial we will walk through the basic setup and use of Ionic's Auth Connect in an @ionic/vue application.

In this tutorial, you will learn how to:

  • Install and configure Auth Connect
  • Perform Login and Logout operations
  • Check if the user is authenticated
  • Obtain the tokens from Auth Connect
  • Integrate Identity Vault with Auth Connect

Generate the Application

The first step to take is to generate the application:

ionic start getting-started-ac-vue tabs --type=vue

Now that the application has been generated, let's also add the iOS and Android platforms.

Open the capacitor.config.ts file and change the appId to something unique like io.ionic.gettingstartedacvue:

import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
appId: 'io.ionic.gettingstartedacvue',
appName: 'getting-started-ac-vue',
webDir: 'dist',
bundledWebRuntime: false
};

export default config;

Next, build the application, then install and create the platforms:

npm run build
ionic cap add android
ionic cap add ios

Finally, in order to ensure that a cap copy is run with each build, add it to the build script in the package.json file as such:

  "scripts": {
"build": "vue-cli-service build && cap copy",
...
},

Install Auth Connect

In order to install Auth Connect, you will need to use ionic enterprise register to register your product key. This will create a .npmrc file containing the product key.

If you have already performed that step for your production application, you can just copy the .npmrc file from your production project. Since this application is for learning purposes only, you don't need to obtain another key.

You can now install Auth Connect and sync the platforms:

npm install @ionic-enterprise/auth
ionic cap sync

Configure Auth Connect

Our next step is to configure Auth Connect. Create a file named src/use/auth-config.ts and fill it with the following boilerplate content:

import { IonicAuthOptions } from '@ionic-enterprise/auth';
import { isPlatform } from '@ionic/vue';

export default () => {
const isNative = isPlatform('hybrid');

const config: IonicAuthOptions = {
authConfig: '',
clientID: '',
discoveryUrl: '',
scope: 'openid offline_access email profile',
audience: '',
redirectUri: isNative ? '' : '',
logoutUrl: isNative ? '' : '',
platform: isNative ? 'capacitor' : 'web',
iosWebView: isNative ? 'private' : undefined,
androidToolbarColor: isNative ? '#337ab7' : undefined,
};

return { config };
};

As you can see, there are several items that we need to fill in. Specifically: authConfig, clientID, discoveryUrl, redirectUri, and logoutUrl.

The audience value can remain blank as it is not used by Azure, but may be used by other providers such as Auth0.

Additionally, Azure requires a custom API scope that we will need to add to the scope parameter.

Obtaining this information likely takes a little coordination with whoever administers our backend services. In our case, we have a team that administers our Azure services and they have given us the following information:

  • Application ID: ed8cb65d-7bb2-4107-bc36-557fb680b994
  • Metadata Document URL: https://dtjacdemo.b2clogin.com/dtjacdemo.onmicrosoft.com/B2C_1_acdemo2/v2.0/.well-known/openid-configuration
  • Web Redirect (for development): http://localhost:8100
  • Native Redirect (for development): msauth://com.ionic.acprovider/O5m5Gtd2Xt8UNkW3wk7DWyKGfv8%3D
  • Custom API Scope: https://dtjacdemo.onmicrosoft.com/ed8cb65d-7bb2-4107-bc36-557fb680b994/demo.read

Translating that into our configuration object, we now have this:

const config: IonicAuthOptions = {
authConfig: 'azure',
clientID: 'ed8cb65d-7bb2-4107-bc36-557fb680b994',
discoveryUrl:
'https://dtjacdemo.b2clogin.com/dtjacdemo.onmicrosoft.com/B2C_1_acdemo2/v2.0/.well-known/openid-configuration',
scope:
'openid offline_access email profile https://dtjacdemo.onmicrosoft.com/ed8cb65d-7bb2-4107-bc36-557fb680b994/demo.read',
audience: '',
redirectUri: isNative ? 'msauth://com.ionic.acprovider/O5m5Gtd2Xt8UNkW3wk7DWyKGfv8%3D' : 'http://localhost:8100',
logoutUrl: isNative ? 'msauth://com.ionic.acprovider/O5m5Gtd2Xt8UNkW3wk7DWyKGfv8%3D' : 'http://localhost:8100',
platform: isNative ? 'capacitor' : 'web',
iosWebView: isNative ? 'private' : undefined,
androidToolbarColor: isNative ? '#4424eb' : undefined,
};

The web redirect for development is on port 8100. Vue uses port 8080 by default, so we will need to make a minor change to our package.json file as well:

  "scripts": {
"build": "vue-cli-service build && cap copy",
"lint": "vue-cli-service lint",
"serve": "vue-cli-service serve --port=8100",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e"
},

Note: you can use your own configuration for this tutorial as well. However, we suggest that you start with our configuration, get the application working, and then try your own configuration after that.

Create the Auth Connect Service

Now that we have Auth Connect configured, let's instantiate an instance of it using our configuration.

Create a file named src/use/auth.ts with the following contents:

import { IonicAuth } from '@ionic-enterprise/auth';
import useAuthConfig from './auth-config';

class AuthenticationService extends IonicAuth {
constructor() {
const { config } = useAuthConfig();
super(config);
}
}

const authService = new AuthenticationService();

export default () => {
return {
login: (): Promise<void> => authService.login(),
logout: (): Promise<void> => authService.logout(),
};
};

This creates an instance of Auth Connect using our configuration and exposes the two operations we would like to perform at this time: login and logout.

To test this out, we will replace the ExploreContainer with "Login" and "Logout" buttons in the src/views/Tab1.vue file. :

<ion-button @click="login">Login</ion-button> <ion-button @click="logout">Logout</ion-button>

Within the script area, import the our useAuth and expose the login and logout functions:

<script lang="ts">
import { IonButton, IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
import useAuth from '@/use/auth';

export default {
name: 'Tab1',
components: { IonButton, IonHeader, IonToolbar, IonTitle, IonContent, IonPage },
setup() {
return {
...useAuth(),
};
},
};
</script>

Test that with the following credentials:

  • Email Address: test@ionic.io
  • Password: Ion54321

You should be able to login and and logout successfully. Open the devtools and look in the Application tab. You should see keys being created when the user logs in and being removed when the user logs out.

Configure the Native Projects

Build the application for a native device and try the login there as well. This currently behaves as follows:

  1. Clicking "Login" takes you to the login page.
  2. Entering the email and password and clicking "Sign in" does the sign in process (so far as we can tell).
  3. Azure does not redirect back to the application once the login completes.

If you inspect the login page via the devtools, you will notice a message in the console similar to the this: Navigation is unreachable: msauth://com.ionic.acprovider/O5m5Gtd2Xt8UNkW3wk7DWyKGfv8%3D?state=...

The problem is that we need to let the native device know which application(s) are allowed to handle navigation to the msauth:// scheme. To do this, we need to modify our AndroidManifest.xml and Info.plist files as noted here. Use msauth in place of $AUTH_URL_SCHEME.

Determine Current Auth Status

Right now, the user is shown both the login and logout buttons, and you don't really know if the user is logged in or not. Let's change that.

Auth Connect includes a method called isAuthenticated(). This method resolves true if a valid access token exists, and false otherwise. If the current access token has expired, this method will attempt to refresh the token before determining if it should resolve true or false.

Let's add a line to our return value in src/use/auth.ts:

export default () => {
return {
isAuthenticated: (): Promise<boolean> => authService.isAuthenticated(),
login: (): Promise<void> => authService.login(),
logout: (): Promise<void> => authService.logout(),
};
};

We will use this in the Tab1 page to display only the Login or the Logout button, depending on the current login status. First, update the bindings on the buttons:

<ion-button v-if="authenticated === false" @click="loginClicked">Login</ion-button>
<ion-button v-if="authenticated" @click="logoutClicked">Logout</ion-button>

Notice that we added the v-if conditions and also changed the @click event bindings. The reason for this is that our click logic is going to do a little more work than before.

What we want to do is:

  • upon creating the page, check the current auth status
  • after performing a login or logout operation, refresh the auth status

Here is one way to code all of that:

  setup() {
const authenticated = ref<boolean | undefined>();
const { login, logout, isAuthenticated } = useAuth();

const checkAuth = async () => {
authenticated.value = await isAuthenticated();
};

const loginClicked = async () => {
try {
await login();
checkAuth();
} catch (err) {
console.log('Error logging in:', err);
}
};

const logoutClicked = async () => {
await logout();
checkAuth();
};

checkAuth();

return {
authenticated,
loginClicked,
logoutClicked,
};
},

Notice the try ... catch in loginClicked(). The login() will throw an error if the user fails to log in. Production applications should have some kind of handling here, but our sample can get away with simply logging the fact.

At this point, you should see the Login button if you are not logged in and the Logout button if you are. Furthermore, you should see the proper button after refreshing your browser or after successfully logging in or out.

Guarding the Routes

Let's pretend that Tab2 and Tab3 had super secret information that only logged in users could see (they don't, of course, but we can pretend). We would not want users getting there if they were not currently authenticated.

We can use our newly exposed isAuthenticated() function to build a guard for those routes.

Open src/router/index.ts. At the top of the file, import useAuth.

import useAuth from '@/use/auth';

const { isAuthenticated } = useAuth();

Then add some metadata to the tab2 and tab3 routes to indicate that they require authentication:

      {
path: 'tab2',
component: () => import('@/views/Tab2.vue'),
meta: { requiresAuth: true },
},
{
path: 'tab3',
component: () => import('@/views/Tab3.vue'),
meta: { requiresAuth: true },
},

Create a guard function (you will need to add more import statements):

const checkAuthStatus = async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) => {
if (to.matched.some((r) => r.meta.requiresAuth)) {
if (!(await isAuthenticated())) {
return next('/');
}
}
next();
};

Finally, after the router is created, but before it is exported, add the guard:

router.beforeEach(checkAuthStatus);

Now if you are not logged in and try to click on tabs 2 or 3, the application will not navigate and you will stay on tab 1. Furthermore, if you try to manually load http://localhost:8100/tabs/tab2 (or tab3), you will be redirected to tab1.

Get the Tokens

We can now log in and out, but what about getting at the tokens that our OIDC provider gave us? There are a handful of methods available that get us wht we need:

You can use these wherever you need to supply a specific token. For example, if you are accessing a backend API that requires you to include a bearer token (and you probably are if you are using Auth Connect), then you can expose the getAccessToken() method and create in interceptor that adds the token.

Since we don't have a backend API that will need the access token, let's instead modify src/use/auth.ts to grab the user's name from the ID token. Here is the code in context. Add the parts you need:

// imports and whatnot up here...
...

export default () => {
const getUserName = async (): Promise<string | undefined> => {
const token = await authService.getIdToken();
return token && token.name;
};

return {
getUserName,
isAuthenticated: (): Promise<boolean> => authService.isAuthenticated(),
login: (): Promise<void> => authService.login(),
logout: (): Promise<void> => authService.logout(),
};
};

As a challenge to you, update the Tab1 page to show the current user's name when they are logged in.

Use a Token Storage Provider

Up until now, we haven't really been worrying about where the tokens are being stored. By default, Auth Connect is just using localstorage. This has two disadvantages: it is not secure, and on mobile devices the OS can wipe out localstorage any time it feels like it needs a little extra space.

The default token storage provider is OK for development purposes, but it is not a good option for a production application. To rectify this, there are two solid options:

Using Identity Vault is the clear winner here in all categories: security, ease of use, and ease of maintenance.

As such, for our application we will install identity vault and use it in "secure storage" mode to store the tokens. The first step is to install the product and sync the platforms.

npm i @ionic-enterprise/identity-vault
ionic cap sync

Next we will create a factory that builds either the actual vault if we are on a device or a browser based "vault" that is suitable for development if we are in the browser.

import { isPlatform } from '@ionic/vue';
import { BrowserVault, IdentityVaultConfig, Vault } from '@ionic-enterprise/identity-vault';

export default () => {
const createVault = (config: IdentityVaultConfig): Vault | BrowserVault =>
isPlatform('hybrid') ? new Vault(config) : new BrowserVault(config);

return { createVault };
};

This provides us with a vault that is usable on our devices, or a fallback vault that will allow us to keep using our browser-based development flow.

Now that we have a factory in place to build our vaults, let's instantiate a vault and include it in our Auth Connect configuration.

Create a file called src/use/vault.ts with the following contents:

import { DeviceSecurityType, VaultType } from '@ionic-enterprise/identity-vault';
import useVaultFactory from './vault-factory';

const { createVault } = useVaultFactory();
const vault = createVault({
key: 'io.ionic.gettingstartedacvue',
type: VaultType.SecureStorage,
deviceSecurityType: DeviceSecurityType.None,
lockAfterBackgrounded: 5000,
shouldClearVaultAfterTooManyFailedAttempts: true,
customPasscodeInvalidUnlockAttempts: 2,
unlockVaultOnLoad: false,
});

export default () => ({ vault });

Then modify src/use/auth-config.ts to include the tokenStorageProvider:

import { IonicAuthOptions } from '@ionic-enterprise/auth';
import { isPlatform } from '@ionic/vue';
import useVault from '@/use/vault';

export default () => {
const { vault } = useVault();
const isNative = isPlatform('hybrid');

const config: IonicAuthOptions = {
...
tokenStorageProvider: vault,
};

return { config };
};

Now when you run the application on a device, the device's secure key storage mechanisms are used to store the key rather than localstorage. The browser is still using localstorage, but the browser implementation is just there for developer convenience.

Conclusion

At this point, you should have a good idea of how Auth Connect and Identity Vault work together to provide a complete and secure authentication solution. There is still more functionality that can be implemented. Be sure to check out our other documentation and demos to see how to expand on this to offer expanded functionality such as Biometric based authentication.