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
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:
_10ionic start getting-started-ac-angular tabs --type=angular-standalone
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
:
_12import { CapacitorConfig } from '@capacitor/cli';_12_12const config: CapacitorConfig = {_12 appId: 'io.ionic.gettingstartedacangular',_12 appName: 'getting-started-ac-angular',_12 webDir: 'www',_12 server: {_12 androidScheme: 'https',_12 },_12};_12_12export default config;
Next, build the application, then install and create the platforms:
_10npm run build_10ionic cap add android_10ionic 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.
_10ionic 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:
_10npm 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:
_23import { Injectable } from '@angular/core';_23import { ProviderOptions } from '@ionic-enterprise/auth';_23import { Platform } from '@ionic/angular';_23_23@Injectable({_23 providedIn: 'root'_23})_23export 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:
_10this.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
.
_48import { Injectable } from '@angular/core';_48import { AuthConnect, ProviderOptions } from '@ionic-enterprise/auth';_48import { Platform } from '@ionic/angular';_48_48@Injectable({_48 providedIn: 'root'_48})_48export 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
:
_13import { Injectable } from '@angular/core';_13import { Auth0Provider, AuthConnect, ProviderOptions } from '@ionic-enterprise/auth';_13import { Platform } from '@ionic/angular';_13..._13@Injectable({_13 providedIn: 'root'_13})_13export 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
:
_20import { Injectable } from '@angular/core';_20import { Auth0Provider, AuthConnect, AuthResult, ProviderOptions } from '@ionic-enterprise/auth';_20import { Platform } from '@ionic/angular';_20_20@Injectable({_20 providedIn: 'root'_20})_20export 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.
_10public 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:
_21import { Component } from '@angular/core';_21import { AuthService } from '../services/auth/auth.service';_21_21@Component({_21 selector: 'app-tab1',_21 templateUrl: 'tab1.page.html',_21 styleUrls: ['tab1.page.scss']_21})_21export 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.
_48import { Injectable, NgZone } from '@angular/core';_48import { BehaviorSubject, Observable } from 'rxjs';_48..._48@Injectable({_48 providedIn: 'root'_48})_48export 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:
_21import { Component } from '@angular/core';_21import { Observable } from 'rxjs';_21import { AuthService } from '../services/auth/auth.service';_21..._21export 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:
_15public async getAccessToken(): Promise<string | undefined> {_15 await this.initialize();_15 const res = await this.getAuthResult();_15 return res?.accessToken; _15 }_15_15public 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.
_19public 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_19public 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.
_10npm 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.
_10ionic g service services/vault/vault
The following code should go in src/app/services/vault.service.ts
.
_28import { Injectable } from '@angular/core';_28import { Platform } from '@ionic/angular';_28import { BrowserVault, DeviceSecurityType, Vault, VaultType} from '@ionic-enterprise/identity-vault';_28import { AuthResult } from '@ionic-enterprise/auth';_28_28const 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_28const vaultKey = 'auth-result';_28_28@Injectable({_28 providedIn: 'root'_28})_28export 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
:
_10import { VaultService } from '../vault/vault.service';_10_10constructor(platform: Platform, private ngZone: NgZone, private vault: VaultService)
Create a new function called saveAuthResult()
:
_10private 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:
_13public 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:
_10public 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()
.
_14public async login(): Promise<void> {_14 await this.initialize();_14 const authResult = await AuthConnect.login(this.provider, authOptions);_14 await this.saveAuthResult(authResult);_14}_14_14public 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:
_10import { inject } from '@angular/core';_10import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';_10import { AuthService } from 'src/app/services/auth/auth.service';_10_10export 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.routes.ts
to indicate that they require authentication:
_10 {_10 path: 'tab2',_10 loadComponent: () => import('../tab2/tab2.page').then(m => m.Tab2Page),_10 canActivate: [authGuard]_10 },_10 {_10 path: 'tab3',_10 loadComponent: () => import('../tab3/tab3.page').then(m => m.Tab3Page),_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.
- Auth Connect
- Identity Vault - check out its Getting Started guide as well.
- Tea Taster with Auth Connect and Identity Vault