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:


_10
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:


_10
import { CapacitorConfig } from '@capacitor/cli';
_10
_10
const config: CapacitorConfig = {
_10
appId: 'io.ionic.gettingstartedacvue',
_10
appName: 'getting-started-ac-vue',
_10
webDir: 'dist',
_10
bundledWebRuntime: false
_10
};
_10
_10
export default config;

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


_10
npm run build
_10
ionic cap add android
_10
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:


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

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:


_10
npm install @ionic-enterprise/auth
_10
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:


_21
import { IonicAuthOptions } from '@ionic-enterprise/auth';
_21
import { isPlatform } from '@ionic/vue';
_21
_21
export default () => {
_21
const isNative = isPlatform('hybrid');
_21
_21
const config: IonicAuthOptions = {
_21
authConfig: '',
_21
clientID: '',
_21
discoveryUrl: '',
_21
scope: 'openid offline_access email profile',
_21
audience: '',
_21
redirectUri: isNative ? '' : '',
_21
logoutUrl: isNative ? '' : '',
_21
platform: isNative ? 'capacitor' : 'web',
_21
iosWebView: isNative ? 'private' : undefined,
_21
androidToolbarColor: isNative ? '#337ab7' : undefined,
_21
};
_21
_21
return { config };
_21
};

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:


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

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:


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

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:


_18
import { IonicAuth } from '@ionic-enterprise/auth';
_18
import useAuthConfig from './auth-config';
_18
_18
class AuthenticationService extends IonicAuth {
_18
constructor() {
_18
const { config } = useAuthConfig();
_18
super(config);
_18
}
_18
}
_18
_18
const authService = new AuthenticationService();
_18
_18
export default () => {
_18
return {
_18
login: (): Promise<void> => authService.login(),
_18
logout: (): Promise<void> => authService.logout(),
_18
};
_18
};

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. :


_10
<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:


_14
<script lang="ts">
_14
import { IonButton, IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
_14
import useAuth from '@/use/auth';
_14
_14
export default {
_14
name: 'Tab1',
_14
components: { IonButton, IonHeader, IonToolbar, IonTitle, IonContent, IonPage },
_14
setup() {
_14
return {
_14
...useAuth(),
_14
};
_14
},
_14
};
_14
</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:


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

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:


_10
<ion-button v-if="authenticated === false" @click="loginClicked">Login</ion-button>
_10
<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:


_30
setup() {
_30
const authenticated = ref<boolean | undefined>();
_30
const { login, logout, isAuthenticated } = useAuth();
_30
_30
const checkAuth = async () => {
_30
authenticated.value = await isAuthenticated();
_30
};
_30
_30
const loginClicked = async () => {
_30
try {
_30
await login();
_30
checkAuth();
_30
} catch (err) {
_30
console.log('Error logging in:', err);
_30
}
_30
};
_30
_30
const logoutClicked = async () => {
_30
await logout();
_30
checkAuth();
_30
};
_30
_30
checkAuth();
_30
_30
return {
_30
authenticated,
_30
loginClicked,
_30
logoutClicked,
_30
};
_30
},

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.


_10
import useAuth from '@/use/auth';
_10
_10
const { isAuthenticated } = useAuth();

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


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

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


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

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


_10
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:


_16
// imports and whatnot up here...
_16
...
_16
_16
export default () => {
_16
const getUserName = async (): Promise<string | undefined> => {
_16
const token = await authService.getIdToken();
_16
return token && token.name;
_16
};
_16
_16
return {
_16
getUserName,
_16
isAuthenticated: (): Promise<boolean> => authService.isAuthenticated(),
_16
login: (): Promise<void> => authService.login(),
_16
logout: (): Promise<void> => authService.logout(),
_16
};
_16
};

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.


_10
npm i @ionic-enterprise/identity-vault
_10
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.


_10
import { isPlatform } from '@ionic/vue';
_10
import { BrowserVault, IdentityVaultConfig, Vault } from '@ionic-enterprise/identity-vault';
_10
_10
export default () => {
_10
const createVault = (config: IdentityVaultConfig): Vault | BrowserVault =>
_10
isPlatform('hybrid') ? new Vault(config) : new BrowserVault(config);
_10
_10
return { createVault };
_10
};

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:


_15
import { DeviceSecurityType, VaultType } from '@ionic-enterprise/identity-vault';
_15
import useVaultFactory from './vault-factory';
_15
_15
const { createVault } = useVaultFactory();
_15
const vault = createVault({
_15
key: 'io.ionic.gettingstartedacvue',
_15
type: VaultType.SecureStorage,
_15
deviceSecurityType: DeviceSecurityType.None,
_15
lockAfterBackgrounded: 5000,
_15
shouldClearVaultAfterTooManyFailedAttempts: true,
_15
customPasscodeInvalidUnlockAttempts: 2,
_15
unlockVaultOnLoad: false,
_15
});
_15
_15
export default () => ({ vault });

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


_15
import { IonicAuthOptions } from '@ionic-enterprise/auth';
_15
import { isPlatform } from '@ionic/vue';
_15
import useVault from '@/use/vault';
_15
_15
export default () => {
_15
const { vault } = useVault();
_15
const isNative = isPlatform('hybrid');
_15
_15
const config: IonicAuthOptions = {
_15
...
_15
tokenStorageProvider: vault,
_15
};
_15
_15
return { config };
_15
};

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.