Skip to main content
Version: 6.0

Getting Started with Auth Connect in @ionic/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
note

The source code for the Ionic application created in this tutorial can be found here

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:


_12
import { CapacitorConfig } from '@capacitor/cli';
_12
_12
const config: CapacitorConfig = {
_12
appId: 'io.ionic.gettingstartedacvue',
_12
appName: 'getting-started-ac-vue',
_12
webDir: 'dist',
_12
server: {
_12
androidScheme: 'https',
_12
},
_12
};
_12
_12
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 sync is run with each build, add it to the build script in the package.json file as such:


_10
"scripts": {
_10
"build": "vue-tsc && vite build && cap sync",
_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

Configure Auth Connect

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


_15
import { ProviderOptions } from '@ionic-enterprise/auth';
_15
import { isPlatform } from '@ionic/vue';
_15
_15
const isNative = isPlatform('hybrid');
_15
_15
const options: ProviderOptions = {
_15
clientId: '',
_15
discoveryUrl: '',
_15
scope: 'openid offline_access',
_15
audience: '',
_15
redirectUri: isNative ? '' : '',
_15
logoutUrl: isNative ? '' : '',
_15
};
_15
_15
export const useAuth = () => ({});

Auth Connect Options

The options object is passed to the login() function when we establish the authentication session. As you can see, there are several items that we need to fill in. Specifically: audience, clientId, scope, discoveryUrl, redirectUri, and logoutUrl.

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

  • Application ID: yLasZNUGkZ19DGEjTmAITBfGXzqbvd00
  • Audience: https://io.ionic.demo.ac
  • Metadata Document URL: https://dev-2uspt-sz.us.auth0.com/.well-known/openid-configuration
  • Web Redirect (for development): http://localhost:8100/login
  • Native Redirect (for development): msauth://login
  • Additional Scopes: email picture profile

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


_10
const options: ProviderOptions = {
_10
audience: 'https://io.ionic.demo.ac',
_10
clientId: 'yLasZNUGkZ19DGEjTmAITBfGXzqbvd00',
_10
discoveryUrl: 'https://dev-2uspt-sz.us.auth0.com/.well-known/openid-configuration',
_10
logoutUrl: isNative ? 'msauth://login' : 'http://localhost:8100/login',
_10
redirectUri: isNative ? 'msauth://login' : 'http://localhost:8100/login',
_10
scope: 'openid offline_access email picture profile',
_10
};

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


_10
"scripts": {
_10
"build": "vue-tsc && vite build && cap sync",
_10
"dev": "vite --port=8100",
_10
"lint": "eslint",
_10
"prepare": "husky install",
_10
"preview": "vite preview",
_10
"test:e2e": "cypress run",
_10
"test:unit": "vitest"
_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.

Initialization

Before we can use any AuthConnect functions we need to make sure we have performed the initialization. Add the code to do this after the setting of the options value in src/composables/auth.ts.


_36
import { AuthConnect, ProviderOptions } from '@ionic-enterprise/auth';
_36
import { isPlatform } from '@ionic/vue';
_36
_36
const isNative = isPlatform('hybrid');
_36
_36
const options: ProviderOptions = {
_36
// see the options setting above
_36
};
_36
_36
const performInit = async (): Promise<void> => {
_36
await AuthConnect.setup({
_36
platform: isNative ? 'capacitor' : 'web',
_36
logLevel: 'DEBUG',
_36
ios: {
_36
webView: 'private',
_36
},
_36
web: {
_36
uiMode: 'popup',
_36
authFlow: 'implicit',
_36
},
_36
});
_36
};
_36
_36
let initializing: Promise<void>;
_36
const initialize = async (): Promise<void> => {
_36
if (!initializing) {
_36
initializing = new Promise((resolve) => {
_36
performInit().then(() => resolve());
_36
});
_36
}
_36
return initializing;
_36
};
_36
_36
initialize();
_36
_36
export const useAuth = () => ({});

This will get Auth Connect ready to use within our application. Notice that this is also where we supply any platform specific Auth Connect options. Right now, the logLevel is set to DEBUG since this is a demo application. In a production environment, we probably would set it to DEBUG in development and ERROR in production.

The initialize() function will be called from several locations to ensure the setup is complete before making any further AuthConnect calls.

The Provider

Auth Connect requires a provider object that specifies details pertaining to communicating with the OIDC service. Auth Connect offers several common providers out of the box: Auth0Provider, AzureProvider, CognitoProvider, OktaProvider, and OneLoginProvider. You can also create your own provider, though doing so is beyond the scope of this tutorial.

Since we are using Auth0, we will create an Auth0Provider:


_10
import { Auth0Provider, ProviderOptions } from '@ionic-enterprise/auth';
_10
import { isPlatform } from '@ionic/vue';
_10
...
_10
const provider = new Auth0Provider();
_10
...
_10
export const useAuth = () => ({});

Login and Logout

Login and logout are the two most fundamental operations in the authentication flow.

For the login(), we need to pass both the provider and the options we established above. The login() call resolves an AuthResult if the operation succeeds. The AuthResult contains the auth tokens as well as some other information. This object needs to be passed to almost all other Auth Connect functions. As such, it needs to be saved.

The login() call rejects with an error if the user cancels the login or if something else prevents the login to complete.

Add the following code to src/composables/auth.ts:


_13
import { Auth0Provider, AuthConnect, AuthResult, ProviderOptions } from '@ionic-enterprise/auth';
_13
import { isPlatform } from '@ionic/vue';
_13
...
_13
let authResult: AuthResult | null = null;
_13
...
_13
const login = async (): Promise<void> => {
_13
await initialize();
_13
authResult = await AuthConnect.login(provider, options);
_13
}
_13
...
_13
export const auth = () => ({
_13
login
_13
});

For the logout operation, we pass the provider and the authResult that was returned by the login() call.


_12
const logout = async (): Promise<void> => {
_12
await initialize();
_12
if (authResult) {
_12
await AuthConnect.logout(provider, authResult);
_12
authResult = null;
_12
}
_12
};
_12
...
_12
export const useAuth = () => ({
_12
login,
_12
logout,
_12
});

To test these new function, replace the ExploreContainer with "Login" and "Logout" buttons in the src/views/Tab1Page.vue file:


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

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


_10
<script setup lang="ts">
_10
import { IonButton, IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
_10
import { useAuth } from '@/composables/auth';
_10
_10
const { login, logout } = useAuth();
_10
</script>

If you are using our Auth0 provider, you can use the following credentials for the test:

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

You should be able to login and and logout successfully.

Configure the Native Projects

Build the application for a native device and try the login there as well. You should notice that this does not work on your device.

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 android/app/build.gradle and ios/App/App/Info.plist files as noted here. In the Info.plist file, 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.

A simple strategy to use is if we have an AuthResult then we are logged in, otherwise we are not. Add code to do that in src/composables/auth.ts. Ignore the extra complexity with the getAuthResult() function. We will expand on that as we go.


_14
const getAuthResult = async (): Promise<AuthResult | null> => {
_14
return authResult;
_14
}
_14
_14
const isAuthenticated = async (): Promise<boolean> => {
_14
await initialize();
_14
return !!(await getAuthResult());
_14
}
_14
...
_14
export const auth = () => ({
_14
isAuthenticated,
_14
login,
_14
logout,
_14
});

Use this in the Tab1Page 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" @click="logoutClicked">Logout</ion-button>
_10
<ion-button v-else @click="loginClicked">Login</ion-button>

Notice the newly added v-if and v-else conditions. Also notice the changes to 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 in the script setup node of the Tab1Page 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. Integrate this into the existing Tab1Page code.


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

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.

Get the Tokens

We can now log in and out, but what about getting at the tokens that our OIDC provider gave us? This information is stored as part of the AuthResult. Auth Connect also includes some methods that allow us to easily look at the contents of the tokens. For example:


_14
const getAccessToken = async (): Promise<string | undefined> => {
_14
await initialize();
_14
const res = await getAuthResult();
_14
return res?.accessToken;
_14
};
_14
_14
const getUserName = async (): Promise<string | undefined> => {
_14
await initialize();
_14
const res = await getAuthResult();
_14
if (res) {
_14
const data = (await AuthConnect.decodeToken(TokenType.id, res)) as { name: string };
_14
return data?.name;
_14
}
_14
};

Note: the format and data stored in the ID token may changed based on your provider and configuration. Check the documentation and configuration of your own provider for details.

Add these to src/composables/auth.ts and export them at the end of the file like we did the other functions.

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 use the getAccessToken() method and create in interceptor that adds the token.

We don't need an interceptor for this app, but as a challenge to you, update the Tab1Page to show the current user's name when they are logged in. You could also display the access token if you want (though you would never do that in a real app).

Refreshing the Authentication

In a typical OIDC implementation, access tokens are very short lived. In such a case, it is common to use a longer lived refresh token to obtain a new AuthResult.

Let's add a function to src/composables/auth.ts that does the refresh, and then modify getAuthResult() to call it when needed.


_20
const refreshAuth = async (authResult: AuthResult): Promise<AuthResult | null> => {
_20
let newAuthResult: AuthResult | null = null;
_20
_20
if (await AuthConnect.isRefreshTokenAvailable(authResult)) {
_20
try {
_20
newAuthResult = await AuthConnect.refreshSession(provider, authResult);
_20
} catch (err) {
_20
null;
_20
}
_20
}
_20
_20
return newAuthResult;
_20
};
_20
_20
const getAuthResult = async (): Promise<AuthResult | null> => {
_20
if (authResult && (await AuthConnect.isAccessTokenExpired(authResult))) {
_20
authResult = await refreshAuth(authResult);
_20
}
_20
return authResult;
_20
};

Now anything using getAuthResult() to get the current auth result will automatically handle a refresh if needed.

Store the Auth Result

Up until this point, we have been storing our AuthResult in a local state variable in src/composables/auth.ts. This has a couple of disadvantages:

  • Our tokens could show up in a stack trace.t
  • Our tokens do not survive a browser refresh or application restart.

There are several options we could use to store the AuthResult, but one that handles persistence as well as storing the data in a secure location on native devices is Identity Vault.

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.


_10
npm i @ionic-enterprise/identity-vault

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. The following code should go in src/composables/vault-factory.ts.


_10
import { isPlatform } from '@ionic/vue';
_10
import { BrowserVault, IdentityVaultConfig, Vault } from '@ionic-enterprise/identity-vault';
_10
_10
export const useVaultFactory = () => {
_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 secure vault on our devices, or a fallback vault that allows us to keep using our browser-based development flow.

Now that we have a factory in place to build our vaults, let's create some functions that allow us to manage our authentication result.

Create a file called src/composables/session-vault.ts with the following contents:


_34
import { AuthResult } from '@ionic-enterprise/auth';
_34
import { DeviceSecurityType, VaultType } from '@ionic-enterprise/identity-vault';
_34
import { useVaultFactory } from './vault-factory';
_34
_34
const key = 'auth-result';
_34
_34
const { createVault } = useVaultFactory();
_34
const vault = createVault({
_34
key: 'io.ionic.gettingstartedacvue',
_34
type: VaultType.SecureStorage,
_34
deviceSecurityType: DeviceSecurityType.None,
_34
lockAfterBackgrounded: 5000,
_34
shouldClearVaultAfterTooManyFailedAttempts: true,
_34
customPasscodeInvalidUnlockAttempts: 2,
_34
unlockVaultOnLoad: false,
_34
});
_34
_34
const clearSession = (): Promise<void> => {
_34
return vault.clear();
_34
};
_34
_34
const getSession = (): Promise<AuthResult | null> => {
_34
return vault.getValue<AuthResult>(key);
_34
};
_34
_34
const setSession = (value: AuthResult): Promise<void> => {
_34
return vault.setValue(key, value);
_34
};
_34
_34
export const useSessionVault = () => ({
_34
clearSession,
_34
getSession,
_34
setSession,
_34
});

Then modify src/composables/auth.ts to use the sessionVault functions. The goal is to no longer store the auth result in a session variable. Instead, we will use the session vault to store the result and retrieve it from the vault as needed.

Remove the let authResult: AuthResult | null; line and replace it with the following:


_10
import { useSessionVault } from './session-vault';
_10
_10
const { clearSession, getSession, setSession } = useSessionVault();

Create a new local function called saveAuthResult():


_10
const saveAuthResult = async (authResult: AuthResult | null): Promise<void> => {
_10
if (authResult) {
_10
await setSession(authResult);
_10
} else {
_10
await clearSession();
_10
}
_10
};

Modify refreshAuth to save the results of an attempted refresh:


_17
const refreshAuth = async (authResult: AuthResult): Promise<AuthResult | null> => {
_17
let newAuthResult: AuthResult | null;
_17
_17
if (await AuthConnect.isRefreshTokenAvailable(authResult)) {
_17
try {
_17
newAuthResult = await AuthConnect.refreshSession(provider, authResult);
_17
} catch (err) {
_17
// You could also log this, or otherwise mark the failure.
_17
// This app just makes the user redo their login since that is about
_17
// the only action a user could take.
_17
null;
_17
}
_17
saveAuthResult(newAuthResult);
_17
}
_17
_17
return newAuthResult;
_17
};

Modify getAuthResult() to obtain the auth result from the vault:


_10
const getAuthResult = async (): Promise<AuthResult | null> => {
_10
let authResult = await getSession();
_10
if (authResult && (await AuthConnect.isAccessTokenExpired(authResult))) {
_10
authResult = await refreshAuth(authResult);
_10
}
_10
return authResult;
_10
};

Finally, modify login() and logout() to both save the results of the operation accordingly:


_14
const login = async (): Promise<void> => {
_14
await initialize();
_14
const authResult = await AuthConnect.login(provider, options);
_14
await saveAuthResult(authResult);
_14
};
_14
_14
const logout = async (): Promise<void> => {
_14
await initialize();
_14
const authResult = await getAuthResult();
_14
if (authResult) {
_14
await AuthConnect.logout(provider, authResult);
_14
await saveAuthResult(null);
_14
}
_14
};

You should now be able to refresh the app and have a persistent session.

Guard the Routes

Let's pretend that Tab2Page and Tab3Page 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 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 '@/composables/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.

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.