Skip to main content
Version: 5.0

Getting Started with Auth Connect

Generate the Application

Before we explore the use of Auth Connect, we need to scaffold an application. In this section, we will generate an @ionic/react tabs based application, perform some basic configuration, and add the iOS and Android platforms.

Terminal

_10
ionic start getting-started-ac tabs --type=react

Use the Ionic CLI to generate the application.

Terminal

_10
ionic start getting-started-ac tabs --type=react
_10
cd getting-started-ac

Change directory into the newly generated project.

Terminal
capacitor.config.ts

_12
import { CapacitorConfig } from '@capacitor/cli';
_12
_12
const config: CapacitorConfig = {
_12
appId: 'io.ionic.gettingstartedac',
_12
appName: 'getting-started-ac',
_12
webDir: 'dist',
_12
server: {
_12
androidScheme: 'https',
_12
},
_12
};
_12
_12
export default config;

Change the appId to be something unique. The appId is used as the bundle ID / application ID. Therefore it should be a string that is unique to your organization and application. We will use io.ionic.gettingstartedac for this application.

It is best to do this before adding the iOS and Android platforms to ensure they are setup properly from the start.

Terminal
capacitor.config.ts

_10
ionic start getting-started-ac tabs --type=react
_10
cd getting-started-ac
_10
npm run build
_10
ionic cap add android
_10
ionic cap add ios

Build the application and install the platforms.

Terminal
capacitor.config.ts
package.json

_15
{
_15
"name": "getting-started-ac",
_15
"version": "0.0.1",
_15
"author": "Ionic Framework",
_15
"homepage": "https://ionicframework.com/",
_15
"scripts": {
_15
"dev": "vite",
_15
"build": "tsc && vite build && cap sync",
_15
"preview": "vite preview",
_15
"test.e2e": "cypress run",
_15
"test.unit": "vitest",
_15
"lint": "eslint"
_15
},
_15
...
_15
}

We should do a cap sync with each build. This ensures our native projects remain up to date. Change the scripts in package.json to do this.

Terminal
capacitor.config.ts
package.json
vite.config.ts

_16
import legacy from '@vitejs/plugin-legacy';
_16
import react from '@vitejs/plugin-react';
_16
import { defineConfig } from 'vite';
_16
_16
// https://vitejs.dev/config/
_16
export default defineConfig({
_16
plugins: [react(), legacy()],
_16
server: {
_16
port: 8100,
_16
},
_16
test: {
_16
globals: true,
_16
environment: 'jsdom',
_16
setupFiles: './src/setupTests.ts',
_16
},
_16
});

Modify the vite.config.ts file to ensure that the development server (npm run dev) uses port 8100.

Use the Ionic CLI to generate the application.

Change directory into the newly generated project.

Change the appId to be something unique. The appId is used as the bundle ID / application ID. Therefore it should be a string that is unique to your organization and application. We will use io.ionic.gettingstartedac for this application.

It is best to do this before adding the iOS and Android platforms to ensure they are setup properly from the start.

Build the application and install the platforms.

We should do a cap sync with each build. This ensures our native projects remain up to date. Change the scripts in package.json to do this.

Modify the vite.config.ts file to ensure that the development server (npm run dev) uses port 8100.

Terminal

_10
ionic start getting-started-ac tabs --type=react

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:

Terminal

_10
npm install @ionic-enterprise/auth
_10
npx cap sync

Create the "Session Store"

When using Auth Connect, the current session is defined by the current AuthResult which contains auth tokens as well as other session information. Auth Connect returns an AuthResult from the login() function. We will need a place to store that result. Let's create that now. Create a src/util directory, then create a file called src/util/session-store.ts.

Note: React allows for many different state management strategies. Your strategy may differ significantly. State management strategies are outside the scope of this tutorial. Use code that aligns with your chosen strategy.

src/utils/session-store.ts

_10
import { AuthResult } from '@ionic-enterprise/auth';
_10
_10
let session: AuthResult | null = null;

We know we will need to store the session, and that the session is defined by the AuthResult.

src/utils/session-store.ts

_11
import { AuthResult } from '@ionic-enterprise/auth';
_11
_11
let session: AuthResult | null = null;
_11
let listeners: any[] = [];
_11
_11
const subscribe = (listener: any) => {
_11
listeners = [...listeners, listener];
_11
return () => {
_11
listeners = listeners.filter((l) => l !== listener);
_11
};
_11
};

To sync the store with React, we will eventually use useSyncExternalHook. In preparation, we need some boilerplate code starting with a subscribe() method.

src/utils/session-store.ts

_15
import { AuthResult } from '@ionic-enterprise/auth';
_15
_15
let session: AuthResult | null = null;
_15
let listeners: any[] = [];
_15
_15
const subscribe = (listener: any) => {
_15
listeners = [...listeners, listener];
_15
return () => {
_15
listeners = listeners.filter((l) => l !== listener);
_15
};
_15
};
_15
_15
const getSnapshot = (): AuthResult | null => {
_15
return session;
_15
};

Add a getSnapshot() function that just returns the current session.

src/utils/session-store.ts

_21
import { AuthResult } from '@ionic-enterprise/auth';
_21
_21
let session: AuthResult | null = null;
_21
let listeners: any[] = [];
_21
_21
const subscribe = (listener: any) => {
_21
listeners = [...listeners, listener];
_21
return () => {
_21
listeners = listeners.filter((l) => l !== listener);
_21
};
_21
};
_21
_21
const getSnapshot = (): AuthResult | null => {
_21
return session;
_21
};
_21
_21
const emitChange = () => {
_21
for (let listener of listeners) {
_21
listener();
_21
}
_21
};

When the session changes, we need to inform any subscribed listeners.

src/utils/session-store.ts

_26
import { AuthResult } from '@ionic-enterprise/auth';
_26
_26
let session: AuthResult | null = null;
_26
let listeners: any[] = [];
_26
_26
const subscribe = (listener: any) => {
_26
listeners = [...listeners, listener];
_26
return () => {
_26
listeners = listeners.filter((l) => l !== listener);
_26
};
_26
};
_26
_26
const getSnapshot = (): AuthResult | null => {
_26
return session;
_26
};
_26
_26
const emitChange = () => {
_26
for (let listener of listeners) {
_26
listener();
_26
}
_26
};
_26
_26
const setSession = (newSession: AuthResult | null) => {
_26
session = newSession;
_26
emitChange();
_26
};

We need a function that we can use to set the session. For now, this is the only function that changes the session and emits the change.

src/utils/session-store.ts

_28
import { AuthResult } from '@ionic-enterprise/auth';
_28
_28
let session: AuthResult | null = null;
_28
let listeners: any[] = [];
_28
_28
const subscribe = (listener: any) => {
_28
listeners = [...listeners, listener];
_28
return () => {
_28
listeners = listeners.filter((l) => l !== listener);
_28
};
_28
};
_28
_28
const getSnapshot = (): AuthResult | null => {
_28
return session;
_28
};
_28
_28
const emitChange = () => {
_28
for (let listener of listeners) {
_28
listener();
_28
}
_28
};
_28
_28
const setSession = (newSession: AuthResult | null) => {
_28
session = newSession;
_28
emitChange();
_28
};
_28
_28
export { subscribe, getSnapshot, setSession };

Export the functions that will need to be used elsewhere in our application.

We know we will need to store the session, and that the session is defined by the AuthResult.

To sync the store with React, we will eventually use useSyncExternalHook. In preparation, we need some boilerplate code starting with a subscribe() method.

Add a getSnapshot() function that just returns the current session.

When the session changes, we need to inform any subscribed listeners.

We need a function that we can use to set the session. For now, this is the only function that changes the session and emits the change.

Export the functions that will need to be used elsewhere in our application.

src/utils/session-store.ts

_10
import { AuthResult } from '@ionic-enterprise/auth';
_10
_10
let session: AuthResult | null = null;

Create the Authentication Utility Functions

All interaction with Auth Connect will be abstracted into a set of functions in an authentication utility module. Create an empty src/utils/authentication.ts file.

Setup and Initialization

Before we use Auth Connect, we need to make sure that it is properly set up and initialized. We will build a utility function to perform the setup and initialization required by Auth Connect.

src/utils/authentication.ts

_10
import { Capacitor } from '@capacitor/core';
_10
_10
const isNative = Capacitor.isNativePlatform();

Auth Connect needs a slightly different configuration between mobile and web, so we need to know in which context we are currently running.

src/utils/authentication.ts

_10
import { Capacitor } from '@capacitor/core';
_10
import { Auth0Provider } from '@ionic-enterprise/auth';
_10
_10
const isNative = Capacitor.isNativePlatform();
_10
const provider = new Auth0Provider();

For this tutorial, we are using Auth0 as the authentication vendor. We need to create an Auth0Provider to help Auth Connect with the communication with Auth0.

src/utils/authentication.ts

_19
import { Capacitor } from '@capacitor/core';
_19
import { Auth0Provider, ProviderOptions } from '@ionic-enterprise/auth';
_19
_19
const isNative = Capacitor.isNativePlatform();
_19
const provider = new Auth0Provider();
_19
_19
const authOptions: ProviderOptions = {
_19
audience: 'https://io.ionic.demo.ac',
_19
clientId: 'yLasZNUGkZ19DGEjTmAITBfGXzqbvd00',
_19
discoveryUrl:
_19
'https://dev-2uspt-sz.us.auth0.com/.well-known/openid-configuration',
_19
logoutUrl: isNative
_19
? 'io.ionic.acdemo://auth-action-complete'
_19
: 'http://localhost:8100/auth-action-complete',
_19
redirectUri: isNative
_19
? 'io.ionic.acdemo://auth-action-complete'
_19
: 'http://localhost:8100/auth-action-complete',
_19
scope: 'openid offline_access email picture profile',
_19
};

Auth Connect needs to know how to communicate with our authentication vendor. You will likely need to get this information from the team that manages your cloud infrastructure.

src/utils/authentication.ts

_32
import { Capacitor } from '@capacitor/core';
_32
import {
_32
Auth0Provider,
_32
AuthConnect,
_32
ProviderOptions,
_32
} from '@ionic-enterprise/auth';
_32
_32
const isNative = Capacitor.isNativePlatform();
_32
const provider = new Auth0Provider();
_32
_32
const authOptions: ProviderOptions = {
_32
audience: 'https://io.ionic.demo.ac',
_32
clientId: 'yLasZNUGkZ19DGEjTmAITBfGXzqbvd00',
_32
discoveryUrl:
_32
'https://dev-2uspt-sz.us.auth0.com/.well-known/openid-configuration',
_32
logoutUrl: isNative
_32
? 'io.ionic.acdemo://auth-action-complete'
_32
: 'http://localhost:8100/auth-action-complete',
_32
redirectUri: isNative
_32
? 'io.ionic.acdemo://auth-action-complete'
_32
: 'http://localhost:8100/auth-action-complete',
_32
scope: 'openid offline_access email picture profile',
_32
};
_32
_32
const setupAuthConnect = async (): Promise<void> => {
_32
return AuthConnect.setup({
_32
platform: isNative ? 'capacitor' : 'web',
_32
logLevel: 'DEBUG',
_32
ios: { webView: 'private' },
_32
web: { uiMode: 'popup', authFlow: 'PKCE' },
_32
});
_32
};

We need to perform a one-time setup with Auth Connect. Please refer to the documentation if you have any questions about the individual properties. We will start here with a simple set up that is good for development.

The utility module needs to return the setupAuthConnect function. It will be called elsewhere.

src/utils/authentication.ts

_34
import { Capacitor } from '@capacitor/core';
_34
import {
_34
Auth0Provider,
_34
AuthConnect,
_34
ProviderOptions,
_34
} from '@ionic-enterprise/auth';
_34
_34
const isNative = Capacitor.isNativePlatform();
_34
const provider = new Auth0Provider();
_34
_34
const authOptions: ProviderOptions = {
_34
audience: 'https://io.ionic.demo.ac',
_34
clientId: 'yLasZNUGkZ19DGEjTmAITBfGXzqbvd00',
_34
discoveryUrl:
_34
'https://dev-2uspt-sz.us.auth0.com/.well-known/openid-configuration',
_34
logoutUrl: isNative
_34
? 'io.ionic.acdemo://auth-action-complete'
_34
: 'http://localhost:8100/auth-action-complete',
_34
redirectUri: isNative
_34
? 'io.ionic.acdemo://auth-action-complete'
_34
: 'http://localhost:8100/auth-action-complete',
_34
scope: 'openid offline_access email picture profile',
_34
};
_34
_34
const setupAuthConnect = async (): Promise<void> => {
_34
return AuthConnect.setup({
_34
platform: isNative ? 'capacitor' : 'web',
_34
logLevel: 'DEBUG',
_34
ios: { webView: 'private' },
_34
web: { uiMode: 'popup', authFlow: 'PKCE' },
_34
});
_34
};
_34
_34
export { setupAuthConnect };

The utility module needs to return the setupAuthConnect function. It will be called elsewhere.

Auth Connect needs a slightly different configuration between mobile and web, so we need to know in which context we are currently running.

For this tutorial, we are using Auth0 as the authentication vendor. We need to create an Auth0Provider to help Auth Connect with the communication with Auth0.

Auth Connect needs to know how to communicate with our authentication vendor. You will likely need to get this information from the team that manages your cloud infrastructure.

We need to perform a one-time setup with Auth Connect. Please refer to the documentation if you have any questions about the individual properties. We will start here with a simple set up that is good for development.

The utility module needs to return the setupAuthConnect function. It will be called elsewhere.

The utility module needs to return the setupAuthConnect function. It will be called elsewhere.

src/utils/authentication.ts

_10
import { Capacitor } from '@capacitor/core';
_10
_10
const isNative = Capacitor.isNativePlatform();

Login and Logout

We need to create login() and logout() functions.

src/utils/authentication.ts

_35
import { Capacitor } from '@capacitor/core';
_35
import {
_35
Auth0Provider,
_35
AuthConnect,
_35
ProviderOptions,
_35
} from '@ionic-enterprise/auth';
_35
import { getSnapshot, setSession } from './session-store';
_35
_35
const isNative = Capacitor.isNativePlatform();
_35
const provider = new Auth0Provider();
_35
_35
const authOptions: ProviderOptions = {
_35
audience: 'https://io.ionic.demo.ac',
_35
clientId: 'yLasZNUGkZ19DGEjTmAITBfGXzqbvd00',
_35
discoveryUrl:
_35
'https://dev-2uspt-sz.us.auth0.com/.well-known/openid-configuration',
_35
logoutUrl: isNative
_35
? 'io.ionic.acdemo://auth-action-complete'
_35
: 'http://localhost:8100/auth-action-complete',
_35
redirectUri: isNative
_35
? 'io.ionic.acdemo://auth-action-complete'
_35
: 'http://localhost:8100/auth-action-complete',
_35
scope: 'openid offline_access email picture profile',
_35
};
_35
_35
const setupAuthConnect = async (): Promise<void> => {
_35
return AuthConnect.setup({
_35
platform: isNative ? 'capacitor' : 'web',
_35
logLevel: 'DEBUG',
_35
ios: { webView: 'private' },
_35
web: { uiMode: 'popup', authFlow: 'PKCE' },
_35
});
_35
};
_35
_35
export { setupAuthConnect };

Import the functions from our session store that are used to get and set the session.

src/utils/authentication.ts

_40
import { Capacitor } from '@capacitor/core';
_40
import {
_40
Auth0Provider,
_40
AuthConnect,
_40
ProviderOptions,
_40
} from '@ionic-enterprise/auth';
_40
import { getSnapshot, setSession } from './session-store';
_40
_40
const isNative = Capacitor.isNativePlatform();
_40
const provider = new Auth0Provider();
_40
_40
const authOptions: ProviderOptions = {
_40
audience: 'https://io.ionic.demo.ac',
_40
clientId: 'yLasZNUGkZ19DGEjTmAITBfGXzqbvd00',
_40
discoveryUrl:
_40
'https://dev-2uspt-sz.us.auth0.com/.well-known/openid-configuration',
_40
logoutUrl: isNative
_40
? 'io.ionic.acdemo://auth-action-complete'
_40
: 'http://localhost:8100/auth-action-complete',
_40
redirectUri: isNative
_40
? 'io.ionic.acdemo://auth-action-complete'
_40
: 'http://localhost:8100/auth-action-complete',
_40
scope: 'openid offline_access email picture profile',
_40
};
_40
_40
const setupAuthConnect = async (): Promise<void> => {
_40
return AuthConnect.setup({
_40
platform: isNative ? 'capacitor' : 'web',
_40
logLevel: 'DEBUG',
_40
ios: { webView: 'private' },
_40
web: { uiMode: 'popup', authFlow: 'PKCE' },
_40
});
_40
};
_40
_40
const login = async (): Promise<void> => {
_40
const authResult = await AuthConnect.login(provider, authOptions);
_40
setSession(authResult);
_40
};
_40
_40
export { login, setupAuthConnect };

For the login(), we need to pass both the provider and the authOptions we established earlier. It returns an AuthResult that we need to store. Be sure to export the function so we can use it in the rest of our app.

src/utils/authentication.ts

_49
import { Capacitor } from '@capacitor/core';
_49
import {
_49
Auth0Provider,
_49
AuthConnect,
_49
ProviderOptions,
_49
} from '@ionic-enterprise/auth';
_49
import { getSnapshot, setSession } from './session-store';
_49
_49
const isNative = Capacitor.isNativePlatform();
_49
const provider = new Auth0Provider();
_49
let authResult: AuthResult | null = null;
_49
_49
const authOptions: ProviderOptions = {
_49
audience: 'https://io.ionic.demo.ac',
_49
clientId: 'yLasZNUGkZ19DGEjTmAITBfGXzqbvd00',
_49
discoveryUrl:
_49
'https://dev-2uspt-sz.us.auth0.com/.well-known/openid-configuration',
_49
logoutUrl: isNative
_49
? 'io.ionic.acdemo://auth-action-complete'
_49
: 'http://localhost:8100/auth-action-complete',
_49
redirectUri: isNative
_49
? 'io.ionic.acdemo://auth-action-complete'
_49
: 'http://localhost:8100/auth-action-complete',
_49
scope: 'openid offline_access email picture profile',
_49
};
_49
_49
const setupAuthConnect = async (): Promise<void> => {
_49
return AuthConnect.setup({
_49
platform: isNative ? 'capacitor' : 'web',
_49
logLevel: 'DEBUG',
_49
ios: { webView: 'private' },
_49
web: { uiMode: 'popup', authFlow: 'PKCE' },
_49
});
_49
};
_49
_49
const login = async (): Promise<void> => {
_49
const authResult = await AuthConnect.login(provider, authOptions);
_49
setSession(authResult);
_49
};
_49
_49
const logout = async (): Promise<void> => {
_49
const authResult = getSnapshot();
_49
if (authResult) {
_49
await AuthConnect.logout(provider, authResult);
_49
setSession(null);
_49
}
_49
};
_49
_49
export { login, logout, setupAuthConnect };

For the logout(), when calling Auth Connect we need to pass the provider as well as the AuthResult we established with the login(). If the logout succeeds, the session needs to be cleared.

Import the functions from our session store that are used to get and set the session.

For the login(), we need to pass both the provider and the authOptions we established earlier. It returns an AuthResult that we need to store. Be sure to export the function so we can use it in the rest of our app.

For the logout(), when calling Auth Connect we need to pass the provider as well as the AuthResult we established with the login(). If the logout succeeds, the session needs to be cleared.

src/utils/authentication.ts

_35
import { Capacitor } from '@capacitor/core';
_35
import {
_35
Auth0Provider,
_35
AuthConnect,
_35
ProviderOptions,
_35
} from '@ionic-enterprise/auth';
_35
import { getSnapshot, setSession } from './session-store';
_35
_35
const isNative = Capacitor.isNativePlatform();
_35
const provider = new Auth0Provider();
_35
_35
const authOptions: ProviderOptions = {
_35
audience: 'https://io.ionic.demo.ac',
_35
clientId: 'yLasZNUGkZ19DGEjTmAITBfGXzqbvd00',
_35
discoveryUrl:
_35
'https://dev-2uspt-sz.us.auth0.com/.well-known/openid-configuration',
_35
logoutUrl: isNative
_35
? 'io.ionic.acdemo://auth-action-complete'
_35
: 'http://localhost:8100/auth-action-complete',
_35
redirectUri: isNative
_35
? 'io.ionic.acdemo://auth-action-complete'
_35
: 'http://localhost:8100/auth-action-complete',
_35
scope: 'openid offline_access email picture profile',
_35
};
_35
_35
const setupAuthConnect = async (): Promise<void> => {
_35
return AuthConnect.setup({
_35
platform: isNative ? 'capacitor' : 'web',
_35
logLevel: 'DEBUG',
_35
ios: { webView: 'private' },
_35
web: { uiMode: 'popup', authFlow: 'PKCE' },
_35
});
_35
};
_35
_35
export { setupAuthConnect };

Handling the Authentication Flow

Now that the utility functions are complete, we will use them to implement the authentication flow of our application. To do this, we will need to:

  • Create the AuthActionCompletePage and route that is used in our configuration.
  • Expose the authentication state to our application via the creation of an AuthenticationProvider.
  • Add buttons to the Tab1 page that will be used to perform the login() and logout() operations.

Create the AuthActionCompletePage

The logoutUrl and redirectUri properties are using the /auth-action-complete route. Create a page for the route. The page does not have to do much. We will just display a spinner in case someone sees it momentarily.

src/pages/AuthActionCompletePage.tsx

_13
import { IonContent, IonPage, IonSpinner } from '@ionic/react';
_13
import './AuthActionCompletePage.css';
_13
_13
const AuthActionCompletePage: React.FC = () => (
_13
<IonPage>
_13
<IonContent className="main-content auth-action-complete">
_13
<div className="container">
_13
<IonSpinner name="dots" />
_13
</div>
_13
</IonContent>
_13
</IonPage>
_13
);
_13
export default AuthActionCompletePage;

We will perform a small bit of styling to make the page look nice.

src/pages/AuthActionCompletePage.css

_10
.auth-action-complete .container {
_10
text-align: center;
_10
position: absolute;
_10
left: 0;
_10
right: 0;
_10
top: 50%;
_10
transform: translateY(-50%);
_10
}

Be sure to add the route in the router setup. In a production app, this likely would not be put within the IonTabs structure. Refactoring the routing, however, to avoid this is beyond the scope of this tutorial.

src/App.tsx

_80
import { Redirect, Route } from 'react-router-dom';
_80
import {
_80
IonApp,
_80
IonIcon,
_80
IonLabel,
_80
IonRouterOutlet,
_80
IonTabBar,
_80
IonTabButton,
_80
IonTabs,
_80
setupIonicReact,
_80
} from '@ionic/react';
_80
import { IonReactRouter } from '@ionic/react-router';
_80
import { ellipse, square, triangle } from 'ionicons/icons';
_80
import AuthActionCompletePage from './pages/AuthActionCompletePage';
_80
import Tab1 from './pages/Tab1';
_80
import Tab2 from './pages/Tab2';
_80
import Tab3 from './pages/Tab3';
_80
_80
/* Core CSS required for Ionic components to work properly */
_80
import '@ionic/react/css/core.css';
_80
_80
/* Basic CSS for apps built with Ionic */
_80
import '@ionic/react/css/normalize.css';
_80
import '@ionic/react/css/structure.css';
_80
import '@ionic/react/css/typography.css';
_80
_80
/* Optional CSS utils that can be commented out */
_80
import '@ionic/react/css/padding.css';
_80
import '@ionic/react/css/float-elements.css';
_80
import '@ionic/react/css/text-alignment.css';
_80
import '@ionic/react/css/text-transformation.css';
_80
import '@ionic/react/css/flex-utils.css';
_80
import '@ionic/react/css/display.css';
_80
_80
/* Theme variables */
_80
import './theme/variables.css';
_80
_80
setupIonicReact();
_80
_80
const App: React.FC = () => (
_80
<IonApp>
_80
<IonReactRouter>
_80
<IonTabs>
_80
<IonRouterOutlet>
_80
<Route exact path="/tab1">
_80
<Tab1 />
_80
</Route>
_80
<Route exact path="/tab2">
_80
<Tab2 />
_80
</Route>
_80
<Route path="/tab3">
_80
<Tab3 />
_80
</Route>
_80
<Route path="/auth-action-complete">
_80
<AuthActionCompletePage />
_80
</Route>
_80
<Route exact path="/">
_80
<Redirect to="/tab1" />
_80
</Route>
_80
</IonRouterOutlet>
_80
<IonTabBar slot="bottom">
_80
<IonTabButton tab="tab1" href="/tab1">
_80
<IonIcon aria-hidden="true" icon={triangle} />
_80
<IonLabel>Tab 1</IonLabel>
_80
</IonTabButton>
_80
<IonTabButton tab="tab2" href="/tab2">
_80
<IonIcon aria-hidden="true" icon={ellipse} />
_80
<IonLabel>Tab 2</IonLabel>
_80
</IonTabButton>
_80
<IonTabButton tab="tab3" href="/tab3">
_80
<IonIcon aria-hidden="true" icon={square} />
_80
<IonLabel>Tab 3</IonLabel>
_80
</IonTabButton>
_80
</IonTabBar>
_80
</IonTabs>
_80
</IonReactRouter>
_80
</IonApp>
_80
);
_80
_80
export default App;

Create the AuthenticationProvider

The Auth Connect related functionality will be exposed to our application via a context provider, which we will now build. Create a src/providers directory containing a file named src/providers/AuthenticationProvider.tsx

src/providers/AuthenticationProvider.tsx

_10
import { createContext } from 'react';
_10
_10
type Context = {
_10
isAuthenticated: boolean;
_10
};
_10
_10
export const AuthenticationContext = createContext<Context>({
_10
isAuthenticated: false,
_10
});

Start with creating the context we want to provide to the application.

src/providers/AuthenticationProvider.tsx

_21
import { PropsWithChildren, createContext, useState } from 'react';
_21
_21
type Context = {
_21
isAuthenticated: boolean;
_21
};
_21
_21
export const AuthenticationContext = createContext<Context>({
_21
isAuthenticated: false,
_21
});
_21
_21
export const AuthenticationProvider: React.FC<PropsWithChildren> = ({
_21
children,
_21
}) => {
_21
const [isAuthenticated, setIsAuthenticated] = useState(false);
_21
_21
return (
_21
<AuthenticationContext.Provider value={{ isAuthenticated }}>
_21
{children}
_21
</AuthenticationContext.Provider>
_21
);
_21
};

Create a shell component for the AuthenticationProvider. It needs to render the context we created.

src/providers/AuthenticationProvider.tsx

_28
import { IonSpinner } from '@ionic/react';
_28
import { PropsWithChildren, createContext, useEffect, useState } from 'react';
_28
import { setupAuthConnect } from '../utils/authentication';
_28
_28
type Context = {
_28
isAuthenticated: boolean;
_28
};
_28
_28
export const AuthenticationContext = createContext<Context>({
_28
isAuthenticated: false,
_28
});
_28
_28
export const AuthenticationProvider: React.FC<PropsWithChildren> = ({
_28
children,
_28
}) => {
_28
const [isAuthenticated, setIsAuthenticated] = useState(false);
_28
const [isSetup, setIsSetup] = useState(false);
_28
_28
useEffect(() => {
_28
setupAuthConnect().then(() => setIsSetup(true));
_28
}, []);
_28
_28
return (
_28
<AuthenticationContext.Provider value={{ isAuthenticated }}>
_28
{isSetup ? children : <IonSpinner />}
_28
</AuthenticationContext.Provider>
_28
);
_28
};

Initialize AuthConnect. The child components should only be able to interact with AuthConnect after it has completed the initialization process, so display a spinner until this is complete.

src/providers/AuthenticationProvider.tsx

_40
import { IonSpinner } from '@ionic/react';
_40
import {
_40
PropsWithChildren,
_40
createContext,
_40
useEffect,
_40
useState,
_40
useSyncExternalStore,
_40
} from 'react';
_40
import { setupAuthConnect } from '../utils/authentication';
_40
import { getSnapshot, subscribe } from '../utils/session-store';
_40
_40
type Context = {
_40
isAuthenticated: boolean;
_40
};
_40
_40
export const AuthenticationContext = createContext<Context>({
_40
isAuthenticated: false,
_40
});
_40
_40
export const AuthenticationProvider: React.FC<PropsWithChildren> = ({
_40
children,
_40
}) => {
_40
const [isAuthenticated, setIsAuthenticated] = useState(false);
_40
const [isSetup, setIsSetup] = useState(false);
_40
const session = useSyncExternalStore(subscribe, getSnapshot);
_40
_40
useEffect(() => {
_40
setupAuthConnect().then(() => setIsSetup(true));
_40
}, []);
_40
_40
useEffect(() => {
_40
setIsAuthenticated(!!session);
_40
}, [session]);
_40
_40
return (
_40
<AuthenticationContext.Provider value={{ isAuthenticated }}>
_40
{isSetup ? children : <IonSpinner />}
_40
</AuthenticationContext.Provider>
_40
);
_40
};

Get the session value from our session store and update isAuthenticated when its value changes.

Start with creating the context we want to provide to the application.

Create a shell component for the AuthenticationProvider. It needs to render the context we created.

Initialize AuthConnect. The child components should only be able to interact with AuthConnect after it has completed the initialization process, so display a spinner until this is complete.

Get the session value from our session store and update isAuthenticated when its value changes.

src/providers/AuthenticationProvider.tsx

_10
import { createContext } from 'react';
_10
_10
type Context = {
_10
isAuthenticated: boolean;
_10
};
_10
_10
export const AuthenticationContext = createContext<Context>({
_10
isAuthenticated: false,
_10
});

Now that the AuthenticationProvider has been created, we can add it to our application, which will allow us to perform the login and logout actions. Modify src/App.tsx to surround the bulk of our application with the provider.

src/App.tsx

_83
import { Redirect, Route } from 'react-router-dom';
_83
import {
_83
IonApp,
_83
IonIcon,
_83
IonLabel,
_83
IonRouterOutlet,
_83
IonTabBar,
_83
IonTabButton,
_83
IonTabs,
_83
setupIonicReact,
_83
} from '@ionic/react';
_83
import { IonReactRouter } from '@ionic/react-router';
_83
import { ellipse, square, triangle } from 'ionicons/icons';
_83
import { AuthenticationProvider } from './providers/AuthenticationProvider';
_83
import AuthActionCompletePage from './pages/AuthActionCompletePage';
_83
import Tab1 from './pages/Tab1';
_83
import Tab2 from './pages/Tab2';
_83
import Tab3 from './pages/Tab3';
_83
_83
/* Core CSS required for Ionic components to work properly */
_83
import '@ionic/react/css/core.css';
_83
_83
/* Basic CSS for apps built with Ionic */
_83
import '@ionic/react/css/normalize.css';
_83
import '@ionic/react/css/structure.css';
_83
import '@ionic/react/css/typography.css';
_83
_83
/* Optional CSS utils that can be commented out */
_83
import '@ionic/react/css/padding.css';
_83
import '@ionic/react/css/float-elements.css';
_83
import '@ionic/react/css/text-alignment.css';
_83
import '@ionic/react/css/text-transformation.css';
_83
import '@ionic/react/css/flex-utils.css';
_83
import '@ionic/react/css/display.css';
_83
_83
/* Theme variables */
_83
import './theme/variables.css';
_83
_83
setupIonicReact();
_83
_83
const App: React.FC = () => (
_83
<IonApp>
_83
<AuthenticationProvider>
_83
<IonReactRouter>
_83
<IonTabs>
_83
<IonRouterOutlet>
_83
<Route exact path="/tab1">
_83
<Tab1 />
_83
</Route>
_83
<Route exact path="/tab2">
_83
<Tab2 />
_83
</Route>
_83
<Route path="/tab3">
_83
<Tab3 />
_83
</Route>
_83
<Route path="/auth-action-complete">
_83
<AuthActionCompletePage />
_83
</Route>
_83
<Route exact path="/">
_83
<Redirect to="/tab1" />
_83
</Route>
_83
</IonRouterOutlet>
_83
<IonTabBar slot="bottom">
_83
<IonTabButton tab="tab1" href="/tab1">
_83
<IonIcon aria-hidden="true" icon={triangle} />
_83
<IonLabel>Tab 1</IonLabel>
_83
</IonTabButton>
_83
<IonTabButton tab="tab2" href="/tab2">
_83
<IonIcon aria-hidden="true" icon={ellipse} />
_83
<IonLabel>Tab 2</IonLabel>
_83
</IonTabButton>
_83
<IonTabButton tab="tab3" href="/tab3">
_83
<IonIcon aria-hidden="true" icon={square} />
_83
<IonLabel>Tab 3</IonLabel>
_83
</IonTabButton>
_83
</IonTabBar>
_83
</IonTabs>
_83
</IonReactRouter>
_83
</AuthenticationProvider>
_83
</IonApp>
_83
);
_83
_83
export default App;

Hook Up the Login and Logout

We can use the first tab of our application to test the login() and logout() functions.

src/pages/Tab1.tsx

_31
import {
_31
IonContent,
_31
IonHeader,
_31
IonPage,
_31
IonTitle,
_31
IonToolbar,
_31
} from '@ionic/react';
_31
import ExploreContainer from '../components/ExploreContainer';
_31
import './Tab1.css';
_31
_31
const Tab1: React.FC = () => {
_31
return (
_31
<IonPage>
_31
<IonHeader>
_31
<IonToolbar>
_31
<IonTitle>Tab 1</IonTitle>
_31
</IonToolbar>
_31
</IonHeader>
_31
<IonContent fullscreen>
_31
<IonHeader collapse="condense">
_31
<IonToolbar>
_31
<IonTitle size="large">Tab 1</IonTitle>
_31
</IonToolbar>
_31
</IonHeader>
_31
<ExploreContainer name="Tab 1 page" />
_31
</IonContent>
_31
</IonPage>
_31
);
_31
};
_31
_31
export default Tab1;

Currently, the Tab1 page contains the default skeleton code.

src/pages/Tab1.tsx

_35
import {
_35
IonContent,
_35
IonHeader,
_35
IonPage,
_35
IonTitle,
_35
IonToolbar,
_35
} from '@ionic/react';
_35
import { useContext } from 'react';
_35
import { AuthenticationContext } from '../providers/AuthenticationProvider';
_35
import ExploreContainer from '../components/ExploreContainer';
_35
import './Tab1.css';
_35
_35
const Tab1: React.FC = () => {
_35
const { isAuthenticated } = useContext(AuthenticationContext);
_35
_35
return (
_35
<IonPage>
_35
<IonHeader>
_35
<IonToolbar>
_35
<IonTitle>Tab 1</IonTitle>
_35
</IonToolbar>
_35
</IonHeader>
_35
<IonContent fullscreen>
_35
<IonHeader collapse="condense">
_35
<IonToolbar>
_35
<IonTitle size="large">Tab 1</IonTitle>
_35
</IonToolbar>
_35
</IonHeader>
_35
<ExploreContainer name="Tab 1 page" />
_35
</IonContent>
_35
</IonPage>
_35
);
_35
};
_35
_35
export default Tab1;

Use the AuthenticationContext to get the isAuthenticated value.

src/pages/Tab1.tsx

_39
import {
_39
IonButton,
_39
IonContent,
_39
IonHeader,
_39
IonPage,
_39
IonTitle,
_39
IonToolbar,
_39
} from '@ionic/react';
_39
import { useContext } from 'react';
_39
import { AuthenticationContext } from '../providers/AuthenticationProvider';
_39
import './Tab1.css';
_39
_39
const Tab1: React.FC = () => {
_39
const { isAuthenticated } = useContext(AuthenticationContext);
_39
_39
return (
_39
<IonPage>
_39
<IonHeader>
_39
<IonToolbar>
_39
<IonTitle>Tab 1</IonTitle>
_39
</IonToolbar>
_39
</IonHeader>
_39
<IonContent fullscreen>
_39
<IonHeader collapse="condense">
_39
<IonToolbar>
_39
<IonTitle size="large">Tab 1</IonTitle>
_39
</IonToolbar>
_39
</IonHeader>
_39
{isAuthenticated ? (
_39
<IonButton>Logout</IonButton>
_39
) : (
_39
<IonButton>Login</IonButton>
_39
)}
_39
</IonContent>
_39
</IonPage>
_39
);
_39
};
_39
_39
export default Tab1;

Replace the ExploreContainer component with a Login or Logout button, depending on the current authentication status.

src/pages/Tab1.tsx

_40
import {
_40
IonButton,
_40
IonContent,
_40
IonHeader,
_40
IonPage,
_40
IonTitle,
_40
IonToolbar,
_40
} from '@ionic/react';
_40
import { useContext } from 'react';
_40
import { AuthenticationContext } from '../providers/AuthenticationProvider';
_40
import { login, logout } from '../utils/authentication';
_40
import './Tab1.css';
_40
_40
const Tab1: React.FC = () => {
_40
const { isAuthenticated } = useContext(AuthenticationContext);
_40
_40
return (
_40
<IonPage>
_40
<IonHeader>
_40
<IonToolbar>
_40
<IonTitle>Tab 1</IonTitle>
_40
</IonToolbar>
_40
</IonHeader>
_40
<IonContent fullscreen>
_40
<IonHeader collapse="condense">
_40
<IonToolbar>
_40
<IonTitle size="large">Tab 1</IonTitle>
_40
</IonToolbar>
_40
</IonHeader>
_40
{isAuthenticated ? (
_40
<IonButton onClick={logout}>Logout</IonButton>
_40
) : (
_40
<IonButton onClick={login}>Login</IonButton>
_40
)}
_40
</IonContent>
_40
</IonPage>
_40
);
_40
};
_40
_40
export default Tab1;

Perform a login() or logout() on click accordingly.

Currently, the Tab1 page contains the default skeleton code.

Use the AuthenticationContext to get the isAuthenticated value.

Replace the ExploreContainer component with a Login or Logout button, depending on the current authentication status.

Perform a login() or logout() on click accordingly.

src/pages/Tab1.tsx

_31
import {
_31
IonContent,
_31
IonHeader,
_31
IonPage,
_31
IonTitle,
_31
IonToolbar,
_31
} from '@ionic/react';
_31
import ExploreContainer from '../components/ExploreContainer';
_31
import './Tab1.css';
_31
_31
const Tab1: React.FC = () => {
_31
return (
_31
<IonPage>
_31
<IonHeader>
_31
<IonToolbar>
_31
<IonTitle>Tab 1</IonTitle>
_31
</IonToolbar>
_31
</IonHeader>
_31
<IonContent fullscreen>
_31
<IonHeader collapse="condense">
_31
<IonToolbar>
_31
<IonTitle size="large">Tab 1</IonTitle>
_31
</IonToolbar>
_31
</IonHeader>
_31
<ExploreContainer name="Tab 1 page" />
_31
</IonContent>
_31
</IonPage>
_31
);
_31
};
_31
_31
export default Tab1;

Test this in the web using the following credentials:

  • email: test@ionic.io
  • password: Ion54321

At this point if we press the Login button, a tab should open where we can log in using Auth0. This tab will close after we log in. When we press the logout button a tab will briefly open to perform the logout and then automatically close. The button that is displayed changes based on our current authentication status.

Configure the Native Projects

Login and logout are working in your web browser. Build your application for mobile and try to run them there. You can use an emulator or an actual device for this test.

Terminal

_10
npm run build
_10
npx cap open android
_10
npx cap open ios

On Android, you get an error like this one:


_10
Manifest merger failed : Attribute data@scheme at AndroidManifest.xml requires a placeholder substitution but no value for <AUTH_URL_SCHEME> is provided.

On iOS, the application runs, but you get an invalid URL error after successfully logging in on Auth0.

The problem is that on mobile we are deep-linking back into our application using io.ionic.acdemo://auth-action-complete. We have not registered that scheme with the OS so it does not know to deep-link back to our application. We will set that up now.

For Android, modify the android section of the android/app/build.gradle file to include the AUTH_URL_SCHEME:

app/build.gradle

_28
apply plugin: 'com.android.application'
_28
_28
android {
_28
namespace "io.ionic.gettingstartedac"
_28
compileSdkVersion rootProject.ext.compileSdkVersion
_28
defaultConfig {
_28
applicationId "io.ionic.gettingstartedac"
_28
minSdkVersion rootProject.ext.minSdkVersion
_28
targetSdkVersion rootProject.ext.targetSdkVersion
_28
'AUTH_URL_SCHEME': 'io.ionic.acdemo'

For iOS, add a CFBundleURLTypes section to the ios/App/App/Info.plist file:

App/App/Info.plist

_58
<?xml version="1.0" encoding="UTF-8"?>
_58
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
_58
<plist version="1.0">
_58
<dict>
_58
<key>CFBundleDevelopmentRegion</key>
_58
<string>en</string>
_58
<key>CFBundleDisplayName</key>
_58
<string>getting-started-ac</string>
_58
<key>CFBundleExecutable</key>
_58
<string>io.ionic.acdemo</string>

Re-run the application from Xcode and Android Studio. You should now be able to perform the authentication properly on the mobile applications.

Persist the AuthResult

The user can perform login and logout operations, but if the browser is refreshed, the application loses the AuthResult. This value needs to be persisted between sessions of the application. To fix this, we will modify our session store to use the Preferences plugin to persist the AuthResult. In a production application, we should store the result more securely by using Identity Vault. However, setting up Identity Vault is beyond the scope of this tutorial.

Terminal

_10
npm install @capacitor/preferences

Use the @capacitor/preferences in our session store to persist the AuthResult we received when logging in.

src/utils/session-store.ts
src/main.tsx

_28
import { AuthResult } from '@ionic-enterprise/auth';
_28
_28
let session: AuthResult | null = null;
_28
let listeners: any[] = [];
_28
_28
const subscribe = (listener: any) => {
_28
listeners = [...listeners, listener];
_28
return () => {
_28
listeners = listeners.filter((l) => l !== listener);
_28
};
_28
};
_28
_28
const getSnapshot = (): AuthResult | null => {
_28
return session;
_28
};
_28
_28
const emitChange = () => {
_28
for (let listener of listeners) {
_28
listener();
_28
}
_28
};
_28
_28
const setSession = (newSession: AuthResult | null) => {
_28
session = newSession;
_28
emitChange();
_28
};
_28
_28
export { subscribe, getSnapshot, setSession };

Currently, the session information is only stored in the session variable. We will modify our code to store the session information using the Preferences plugin.

src/utils/session-store.ts
src/main.tsx

_29
import { AuthResult } from '@ionic-enterprise/auth';
_29
import { Preferences } from '@capacitor/preferences';
_29
_29
let session: AuthResult | null = null;
_29
let listeners: any[] = [];
_29
_29
const subscribe = (listener: any) => {
_29
listeners = [...listeners, listener];
_29
return () => {
_29
listeners = listeners.filter((l) => l !== listener);
_29
};
_29
};
_29
_29
const getSnapshot = (): AuthResult | null => {
_29
return session;
_29
};
_29
_29
const emitChange = () => {
_29
for (let listener of listeners) {
_29
listener();
_29
}
_29
};
_29
_29
const setSession = (newSession: AuthResult | null) => {
_29
session = newSession;
_29
emitChange();
_29
};
_29
_29
export { subscribe, getSnapshot, setSession };

Import the Preferences plugin.

src/utils/session-store.ts
src/main.tsx

_30
import { AuthResult } from '@ionic-enterprise/auth';
_30
import { Preferences } from '@capacitor/preferences';
_30
_30
let session: AuthResult | null = null;
_30
let listeners: any[] = [];
_30
_30
const subscribe = (listener: any) => {
_30
listeners = [...listeners, listener];
_30
return () => {
_30
listeners = listeners.filter((l) => l !== listener);
_30
};
_30
};
_30
_30
const getSnapshot = (): AuthResult | null => {
_30
return session;
_30
};
_30
_30
const emitChange = () => {
_30
for (let listener of listeners) {
_30
listener();
_30
}
_30
};
_30
_30
const setSession = async (newSession: AuthResult | null) => {
_30
session = newSession;
_30
await Preferences.set({ key: 'session', value: JSON.stringify(newSession) });
_30
emitChange();
_30
};
_30
_30
export { subscribe, getSnapshot, setSession };

Save the session to Preferences when the session is set.

src/utils/session-store.ts
src/main.tsx

_37
import { AuthResult } from '@ionic-enterprise/auth';
_37
import { Preferences } from '@capacitor/preferences';
_37
_37
let session: AuthResult | null = null;
_37
let listeners: any[] = [];
_37
_37
const subscribe = (listener: any) => {
_37
listeners = [...listeners, listener];
_37
return () => {
_37
listeners = listeners.filter((l) => l !== listener);
_37
};
_37
};
_37
_37
const getSnapshot = (): AuthResult | null => {
_37
return session;
_37
};
_37
_37
const emitChange = () => {
_37
for (let listener of listeners) {
_37
listener();
_37
}
_37
};
_37
_37
const setSession = async (newSession: AuthResult | null) => {
_37
session = newSession;
_37
await Preferences.set({ key: 'session', value: JSON.stringify(newSession) });
_37
emitChange();
_37
};
_37
_37
const initialize = async (): Promise<void> => {
_37
const { value } = await Preferences.get({ key: 'session' });
_37
if (value) {
_37
session = JSON.parse(value);
_37
}
_37
};
_37
_37
export { initialize, subscribe, getSnapshot, setSession };

Create an initialize() function that gets the value from the Preferences plugin.

src/utils/session-store.ts
src/main.tsx

_14
import React from 'react';
_14
import { createRoot } from 'react-dom/client';
_14
import App from './App';
_14
import { initialize } from './utils/session-store';
_14
_14
const container = document.getElementById('root');
_14
const root = createRoot(container!);
_14
initialize().then(() =>
_14
root.render(
_14
<React.StrictMode>
_14
<App />
_14
</React.StrictMode>
_14
)
_14
);

Ensure the authentication information is initialized before the application is mounted.

Currently, the session information is only stored in the session variable. We will modify our code to store the session information using the Preferences plugin.

Import the Preferences plugin.

Save the session to Preferences when the session is set.

Create an initialize() function that gets the value from the Preferences plugin.

Ensure the authentication information is initialized before the application is mounted.

src/utils/session-store.ts
src/main.tsx

_28
import { AuthResult } from '@ionic-enterprise/auth';
_28
_28
let session: AuthResult | null = null;
_28
let listeners: any[] = [];
_28
_28
const subscribe = (listener: any) => {
_28
listeners = [...listeners, listener];
_28
return () => {
_28
listeners = listeners.filter((l) => l !== listener);
_28
};
_28
};
_28
_28
const getSnapshot = (): AuthResult | null => {
_28
return session;
_28
};
_28
_28
const emitChange = () => {
_28
for (let listener of listeners) {
_28
listener();
_28
}
_28
};
_28
_28
const setSession = (newSession: AuthResult | null) => {
_28
session = newSession;
_28
emitChange();
_28
};
_28
_28
export { subscribe, getSnapshot, setSession };

If the user logs in and either refreshes the browser or restarts the application the authentication state is preserved.

Next Steps

Explore the specific topics that are of interest to you at this time. This application is used as the foundation to build upon as those topics are explored.

Happy coding!! 🤓