Skip to main content
Version: 4.0

Getting Started with Auth Connect in @ionic/angular

In this tutorial we will walk through the basic setup and use of Ionic's Auth Connect in an @ionic/angular 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-angular tabs --type=angular

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


_10
import { CapacitorConfig } from '@capacitor/cli';
_10
_10
const config: CapacitorConfig = {
_10
appId: 'io.ionic.gettingstartedacangular',
_10
appName: 'getting-started-ac-angular',
_10
webDir: 'www',
_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

We should do a cap sync with each build and ensure that our application is served on port 8100 when we run the development server. Change the scripts in package.json to do this:


_10
"scripts": {
_10
...
_10
"start": "ng serve --port=8100",
_10
"build": "ng build && cap sync",
_10
...
_10
},

Finally, we're going to update our routes to better conform to what our OIDC provider requires. A typical app has a Login page with a route like /login and our OIDC provider expects that we do too. We will get around this by adding a blank login page, which will add /login as a route. We will never actually navigate to the page within our app.


_10
ionic g page login

Since this page may display for a short time in the OIDC provider popup tab, it is best to modify the HTML for it to only contain an ion-content tag. Open src/app/login/login.page.html and remove everything other than the empty ion-content.

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 service named src/app/services/auth/auth.service.ts by using ionic g service services/auth/auth and fill it with the following boilerplate content:


_23
import { Injectable } from '@angular/core';
_23
import { ProviderOptions } from '@ionic-enterprise/auth';
_23
import { Platform } from '@ionic/angular';
_23
_23
@Injectable({
_23
providedIn: 'root'
_23
})
_23
export class AuthService {
_23
private isNative;
_23
private authOptions: ProviderOptions;
_23
_23
constructor(platform: Platform) {
_23
this.isNative = platform.is('hybrid');
_23
this.authOptions = {
_23
audience: '',
_23
clientId: '',
_23
discoveryUrl: '',
_23
logoutUrl: '',
_23
redirectUri: '',
_23
scope: '',
_23
};
_23
}
_23
}

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
this.authOptions = {
_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: this.isNative ? 'msauth://login' : 'http://localhost:8100/login',
_10
redirectUri: this.isNative ? 'msauth://login' : 'http://localhost:8100/login',
_10
scope: 'openid offline_access email picture profile',
_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/app/services/auth.service.ts.


_48
import { Injectable } from '@angular/core';
_48
import { AuthConnect, ProviderOptions } from '@ionic-enterprise/auth';
_48
import { Platform } from '@ionic/angular';
_48
_48
@Injectable({
_48
providedIn: 'root'
_48
})
_48
export class AuthService {
_48
private initializing: Promise<void> | undefined;
_48
private isNative;
_48
private authOptions: ProviderOptions;
_48
_48
constructor(private platform: Platform) {
_48
this.isNative = platform.is('hybrid');
_48
this.authOptions = {
_48
audience: 'https://io.ionic.demo.ac',
_48
clientId: 'yLasZNUGkZ19DGEjTmAITBfGXzqbvd00',
_48
discoveryUrl: 'https://dev-2uspt-sz.us.auth0.com/.well-known/openid-configuration',
_48
logoutUrl: this.isNative ? 'msauth://login' : 'http://localhost:8100/login',
_48
redirectUri: this.isNative ? 'msauth://login' : 'http://localhost:8100/login',
_48
scope: 'openid offline_access email picture profile',
_48
};
_48
this.initialize();
_48
}
_48
_48
private setup(): Promise<void> {
_48
return AuthConnect.setup({
_48
platform: this.isNative ? 'capacitor' : 'web',
_48
logLevel: 'DEBUG',
_48
ios: {
_48
webView: 'private',
_48
},
_48
web: {
_48
uiMode: 'popup',
_48
authFlow: 'implicit',
_48
},
_48
});
_48
}
_48
_48
private initialize(): Promise<void> {
_48
if (!this.initializing) {
_48
this.initializing = new Promise( resolve => {
_48
this.setup().then(() => resolve());
_48
});
_48
}
_48
return this.initializing;
_48
}
_48
}

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 inside src/app/services/auth.service.ts:


_13
import { Injectable } from '@angular/core';
_13
import { Auth0Provider, AuthConnect, ProviderOptions } from '@ionic-enterprise/auth';
_13
import { Platform } from '@ionic/angular';
_13
...
_13
@Injectable({
_13
providedIn: 'root'
_13
})
_13
export class AuthService {
_13
private initializing: Promise<void> | undefined;
_13
private isNative;
_13
private provider = new Auth0Provider();
_13
...
_13
}

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/app/services/auth.service.ts:


_20
import { Injectable } from '@angular/core';
_20
import { Auth0Provider, AuthConnect, AuthResult, ProviderOptions } from '@ionic-enterprise/auth';
_20
import { Platform } from '@ionic/angular';
_20
_20
@Injectable({
_20
providedIn: 'root'
_20
})
_20
export class AuthService {
_20
private initializing: Promise<void> | undefined;
_20
private isNative;
_20
private provider = new Auth0Provider();
_20
▏ private authOptions: ProviderOptions;
_20
private authResult: AuthResult | null = null;
_20
_20
...
_20
public async login(): Promise<void> {
_20
await this.initialize();
_20
this.authResult = await AuthConnect.login(this.provider, this.authOptions);
_20
}
_20
}

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


_10
public async logout(): Promise<void> {
_10
await this.initialize();
_10
if (this.authResult) {
_10
await AuthConnect.logout(this.provider, this.authResult);
_10
this.authResult = null;
_10
}
_10
}

To test these new function, replace the ExploreContainer with "Login" and "Logout" buttons in the src/app/tab1/tab1.page.html file. :


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

Inside src/app/tab1/tab1.page.ts, inject our auth service, and expose the login and logout functions:


_21
import { Component } from '@angular/core';
_21
import { AuthService } from '../services/auth/auth.service';
_21
_21
@Component({
_21
selector: 'app-tab1',
_21
templateUrl: 'tab1.page.html',
_21
styleUrls: ['tab1.page.scss']
_21
})
_21
export class Tab1Page {
_21
_21
constructor(private auth: AuthService) {}
_21
_21
login() {
_21
this.auth.login();
_21
}
_21
_21
logout() {
_21
this.auth.logout();
_21
}
_21
_21
}

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/app/services/auth.service.ts. We are going to setup observables to notify listening pages of the change. Ignore the extra complexity with the getAuthResult() function. We will expand on that as we go.

We also need to be sure and trigger our authenticationChange$ when we successfully log in or out of the application.


_48
import { Injectable, NgZone } from '@angular/core';
_48
import { BehaviorSubject, Observable } from 'rxjs';
_48
...
_48
@Injectable({
_48
providedIn: 'root'
_48
})
_48
export class AuthService {
_48
...
_48
private authenticationChange: BehaviorSubject<boolean> = new BehaviorSubject(false);
_48
public authenticationChange$: Observable<boolean>;
_48
_48
constructor(platform: Platform, private ngZone: NgZone) {
_48
this.isNative = platform.is('hybrid');
_48
this.authOptions = { ... };
_48
this.initialize();
_48
this.authenticationChange$ = this.authenticationChange.asObservable();
_48
this.isAuthenticated().then( authenticated => this.onAuthChange(authenticated));
_48
}
_48
...
_48
public async login(): Promise<void> {
_48
await this.initialize();
_48
this.authResult = await AuthConnect.login(this.provider, this.authOptions);
_48
this.onAuthChange(await this.isAuthenticated());
_48
}
_48
_48
public async logout(): Promise<void> {
_48
await this.initialize();
_48
if (this.authResult) {
_48
await AuthConnect.logout(this.provider, this.authResult);
_48
this.authResult = null;
_48
this.onAuthChange(await this.isAuthenticated());
_48
}
_48
}
_48
...
_48
private async onAuthChange(isAuthenticated: boolean): Promise<void> {
_48
this.ngZone.run(() => {
_48
this.authenticationChange.next(isAuthenticated);
_48
})
_48
}
_48
public async getAuthResult(): Promise<AuthResult | null> {
_48
return this.authResult;
_48
}
_48
_48
public async isAuthenticated(): Promise<boolean> {
_48
await this.initialize();
_48
return !!(await this.getAuthResult());
_48
}
_48
}

Setup Tab1Page to listen to this observable by attaching to it in the constructor:


_21
import { Component } from '@angular/core';
_21
import { Observable } from 'rxjs';
_21
import { AuthService } from '../services/auth/auth.service';
_21
...
_21
export class Tab1Page {
_21
_21
public authenticationChange$: Observable<boolean>;
_21
_21
constructor(private auth: AuthService) {
_21
this.authenticationChange$ = auth.authenticationChange$;
_21
}
_21
_21
login() {
_21
this.auth.login();
_21
}
_21
_21
logout() {
_21
this.auth.logout();
_21
}
_21
_21
}

Use this in the layout to display only the Login or the Logout button depending on the current login status:


_10
<div *ngIf="authenticationChange$ | async; else elseBlock">
_10
<ion-button (click)="logout()">Logout</ion-button>
_10
</div>
_10
<ng-template #elseBlock>
_10
<ion-button (click)="login()">Login</ion-button>
_10
</ng-template>

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, in src/app/services/auth.service.ts add the following:


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

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

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/app/services/auth.service.ts that does the refresh, and then modify getAuthResult() to call it when needed.


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

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/app/services/auth.service.ts. This has a couple of disadvantages:

  • Our tokens could show up in a stack trace.
  • 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.


_10
ionic g service services/vault/vault

The following code should go in src/app/services/vault.service.ts.


_28
import { Injectable } from '@angular/core';
_28
import { Platform } from '@ionic/angular';
_28
import { BrowserVault, DeviceSecurityType, Vault, VaultType} from '@ionic-enterprise/identity-vault';
_28
import { AuthResult } from '@ionic-enterprise/auth';
_28
_28
const config = {
_28
key: 'io.ionic.gettingstartedacangular',
_28
type: VaultType.SecureStorage,
_28
deviceSecurityType: DeviceSecurityType.None,
_28
lockAfterBackgrounded: 5000,
_28
shouldClearVaultAfterTooManyFailedAttempts: true,
_28
customPasscodeInvalidUnlockAttempts: 2,
_28
unlockVaultOnLoad: false,
_28
}
_28
_28
const vaultKey = 'auth-result';
_28
_28
@Injectable({
_28
providedIn: 'root'
_28
})
_28
export class VaultService {
_28
_28
private vault: Vault | BrowserVault;
_28
_28
constructor(private platform: Platform) {
_28
this.vault = platform.is('hybrid') ? new Vault(config) : new BrowserVault(config);
_28
}
_28
}

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. Add the following methods to our service:


_11
public clear(): Promise<void> {
_11
return this.vault.clear();
_11
}
_11
_11
public getSession(): Promise<AuthResult | null> {
_11
return this.vault.getValue<AuthResult>(vaultKey);
_11
}
_11
_11
public setSession(value: AuthResult): Promise<void> {
_11
return this.vault.setValue(vaultKey, value);
_11
}

Then modify src/app/services/auth.service.ts to use the vault service. 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 private authResult: AuthResult | null = null; line and inject your service into the AuthService:


_10
import { VaultService } from '../vault/vault.service';
_10
_10
constructor(platform: Platform, private ngZone: NgZone, private vault: VaultService)

Create a new function called saveAuthResult():


_10
private async saveAuthResult(authResult: AuthResult | null): Promise<void> {
_10
if (authResult) {
_10
await this.vault.setSession(authResult);
_10
} else {
_10
await this.vault.clear();
_10
}
_10
this.onAuthChange(!!authResult);
_10
}

Modify refreshAuth to save the results of an attempted refresh:


_13
public async refreshAuth(authResult: AuthResult): Promise<AuthResult | null> {
_13
let newAuthResult: AuthResult | null = null;
_13
if (await AuthConnect.isRefreshTokenAvailable(authResult)) {
_13
try {
_13
newAuthResult = await AuthConnect.refreshSession(this.provider, authResult);
_13
} catch (err) {
_13
null;
_13
}
_13
this.saveAuthResult(newAuthResult);
_13
}
_13
_13
return newAuthResult;
_13
}

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


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

Finally, modify login() and logout() to both save the results of the operation accordingly. The call to onAuthChange() should also be removed from each is it is now centralized in saveAuthResult().


_14
public async login(): Promise<void> {
_14
await this.initialize();
_14
const authResult = await AuthConnect.login(this.provider, authOptions);
_14
await this.saveAuthResult(authResult);
_14
}
_14
_14
public async logout(): Promise<void> {
_14
await this.initialize();
_14
const authResult = await this.getAuthResult();
_14
if (authResult) {
_14
await AuthConnect.logout(this.provider, authResult);
_14
await this.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.

Create a guard called src/app/guards/auth.guard.ts and add the following code into it:


_10
import { inject } from '@angular/core';
_10
import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';
_10
import { AuthService } from 'src/app/services/auth/auth.service';
_10
_10
export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
_10
const authService = inject(AuthService);
_10
return authService.isAuthenticated()
_10
};

Then add some metadata to the tab2 and tab3 routes inside src/app/tabs/tabs-routing.module.ts to indicate that they require authentication:


_10
{
_10
path: 'tab2',
_10
loadChildren: () => import('../tab2/tab2.module').then(m => m.Tab2PageModule),
_10
canActivate: [authGuard]
_10
},
_10
{
_10
path: 'tab3',
_10
loadChildren: () => import('../tab3/tab3.module').then(m => m.Tab3PageModule),
_10
canActivate: [authGuard]
_10
},

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