Skip to main content
Version: 4.0

Getting Started with Auth Connect in @ionic/react

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

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


_10
import { CapacitorConfig } from "@capacitor/cli";
_10
_10
const config: CapacitorConfig = {
_10
appId: "io.ionic.gettingstartedacreact",
_10
appName: "getting-started-ac-react",
_10
webDir: "build",
_10
bundledWebRuntime: false,
_10
};
_10
_10
export default config;

Next, open index.tsx and remove React's strict mode. Your root should only render <App /> like so:


_10
...
_10
const container = document.getElementById("root");
_10
const root = createRoot(container!);
_10
root.render(<App />);
_10
...

Then, 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": "react-scripts 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/providers/AuthProvider.tsx and fill it with the following boilerplate content:


_20
import { ProviderOptions } from "@ionic-enterprise/auth";
_20
import { isPlatform } from "@ionic/react";
_20
import { PropsWithChildren, createContext } from "react";
_20
_20
const isNative = isPlatform("hybrid");
_20
_20
const options: ProviderOptions = {
_20
clientId: "",
_20
discoveryUrl: "",
_20
scope: "openid offline_access",
_20
audience: "",
_20
redirectUri: isNative ? "" : "",
_20
logoutUrl: isNative ? "" : "",
_20
};
_20
_20
export const AuthContext = createContext<{}>({});
_20
_20
export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
_20
return <AuthContext.Provider value={{}}>{children}</AuthContext.Provider>;
_20
};

Open src/App.tsx and wrap IonReactRouter with the provider:


_14
...
_14
import { AuthProvider } from './providers/AuthProvider';
_14
_14
setupIonicReact();
_14
_14
const App: React.FC = () => (
_14
<IonApp>
_14
<AuthProvider>
_14
<IonReactRouter>
_14
...
_14
</IonReactRouter>
_14
</AuthProvider>
_14
</IonApp>
_14
);

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:
_10
"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. React uses port 3000 by default, so we will need to make a minor change to our package.json file as well:


_10
"scripts": {
_10
"start": "export PORT=8100 && react-scripts start",
_10
...
_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/providers/AuthProvider.tsx.


_34
import { AuthConnect, ProviderOptions } from "@ionic-enterprise/auth";
_34
import { isPlatform } from "@ionic/react";
_34
import { PropsWithChildren, createContext, useState, useEffect } from "react";
_34
_34
const isNative = isPlatform("hybrid");
_34
_34
const options: ProviderOptions = {
_34
// see the options setting above
_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: "implicit" },
_34
});
_34
};
_34
_34
export const AuthContext = createContext<{}>({});
_34
_34
export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
_34
const [isSetup, setIsSetup] = useState<boolean>(false);
_34
_34
useEffect(() => {
_34
setupAuthConnect().then(() => setIsSetup(true));
_34
}, []);
_34
_34
return (
_34
<AuthContext.Provider value={{}}>
_34
{isSetup && children}
_34
</AuthContext.Provider>
_34
);
_34
};

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, 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 isSetup state variable ensures the setup is complete before we make 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.

note

While they share the same name, providers bundled with Auth Connect are not React Context Providers like the one we're building in this guide. "Providers" from an Auth Connect perspective refers to Authentication Providers.

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


_11
import { AuthConnect, Auth0Provider, ProviderOptions } from '@ionic-enterprise/auth';
_11
import { isPlatform } from '@ionic/react';
_11
import { PropsWithChildren, createContext, useState, useEffect } from 'react';
_11
...
_11
const provider = new Auth0Provider();
_11
_11
export const AuthContext = createContext<{}>({});
_11
_11
export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
_11
...
_11
};

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 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/providers/AuthProvider.tsx:


_20
...
_20
export const AuthContext = createContext<{
_20
login: () => Promise<void>;
_20
}>({
_20
login: () => { throw new Error("Method not implemented."); }
_20
});
_20
_20
export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
_20
const [isSetup, setIsSetup] = useState<boolean>(false);
_20
const [authResult, setAuthResult] = useState<AuthResult | null>(null);
_20
_20
...
_20
_20
const login = async (): Promise<void> => {
_20
const authResult = await AuthConnect.login(provider, options);
_20
setAuthResult(authResult);
_20
};
_20
_20
return <AuthContext.Provider value={{ login }}>{isSetup && children}</AuthContext.Provider>;
_20
};

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


_21
...
_21
export const AuthContext = createContext<{
_21
...
_21
logout: () => Promise<void>;
_21
}>({
_21
...
_21
logout: () => { throw new Error("Method not implemented."); },
_21
});
_21
_21
export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
_21
...
_21
_21
const logout = async (): Promise<void> => {
_21
if (authResult) {
_21
await AuthConnect.logout(provider, authResult);
_21
setAuthResult(null);
_21
}
_21
};
_21
_21
return <AuthContext.Provider value={{ login, logout }}>{isSetup && children}</AuthContext.Provider>;
_21
};

To test these new functions, replace the ExploreContainer with "Login" and "Logout" buttons in the src/pages/Tab1.tsx file:


_26
...
_26
import { useContext } from "react";
_26
import { IonButton, IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
_26
import { AuthContext } from "../providers/AuthProvider";
_26
import "./Tab1.css";
_26
_26
const Tab1: React.FC = () => {
_26
const { login, logout } = useContext(AuthContext);
_26
_26
return (
_26
<IonPage>
_26
<IonHeader>
_26
...
_26
</IonHeader>
_26
<IonContent fullscreen>
_26
<IonHeader collapse="condense">
_26
...
_26
</IonHeader>
_26
<IonButton onClick={login}>Login</IonButton>
_26
<IonButton onClick={logout}>Logout</IonButton>
_26
</IonContent>
_26
</IonPage>
_26
);
_26
};
_26
_26
export default Tab1;

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 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 (if you are using our Auth0 Provider). To do this, we need to modify our build.gradle and Info.plist files as noted here. If you are using our Auth0 Provider, use msauth in place of $AUTH_URL_SCHEME.

Determine Current Auth Status

Right now, the user is shown both the login and logout buttons but you don't really know if the user is logged in or not. Let's change that.

A simple strategy to use is tracking the status using state, updating the value after calling certain Auth Connect API methods. Add code to do this in src/providers/AuthProvider.tsx. Ignore the extra complexity with the getAuthResult() function -- we will expand on that as we go.


_36
...
_36
export const AuthContext = createContext<{
_36
isAuthenticated: boolean;
_36
...
_36
}>({
_36
isAuthenticated: false,
_36
...
_36
});
_36
_36
export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
_36
...
_36
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
_36
_36
const getAuthResult = async (): Promise<AuthResult | null> => {
_36
setIsAuthenticated(!!authResult);
_36
return authResult;
_36
};
_36
_36
...
_36
_36
const login = async (): Promise<void> => {
_36
const authResult = await AuthConnect.login(provider, options);
_36
setAuthResult(authResult);
_36
setIsAuthenticated(true);
_36
};
_36
_36
const logout = async (): Promise<void> => {
_36
if (authResult) {
_36
await AuthConnect.logout(provider, authResult);
_36
setAuthResult(null);
_36
setIsAuthenticated(false);
_36
}
_36
};
_36
_36
return <AuthContext.Provider value={{ isAuthenticated, login, logout }}>{isSetup && children}</AuthContext.Provider>;
_36
};

Use isAuthenticated in Tab1.tsx to display only the Login or the Logout button, depending on the current login status. First, update the bindings on the buttons:


_10
{
_10
!isAuthenticated ? (
_10
<IonButton onClick={handleLogin}>Login</IonButton>
_10
) : (
_10
<IonButton onClick={handleLogout}>Logout</IonButton>
_10
);
_10
}

Notice the added conditions to display the buttons and the changes to the onClick event bindings. Integrate the following code into the existing Tab1 component code:


_13
const { isAuthenticated, login, logout } = useContext(AuthContext);
_13
_13
const handleLogin = async () => {
_13
try {
_13
await login();
_13
} catch (err) {
_13
console.log("Error logging in:", err);
_13
}
_13
};
_13
_13
const handleLogout = async () => {
_13
await logout();
_13
};

Notice the try ... catch in handleLogin(). The login() method 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:


_15
const getAccessToken = async (): Promise<string | undefined> => {
_15
const res = await getAuthResult();
_15
return res?.accessToken;
_15
};
_15
_15
const getUserName = async (): Promise<string | undefined> => {
_15
const res = await getAuthResult();
_15
if (res) {
_15
const data = await AuthConnect.decodeToken<{ name: string }>(
_15
TokenType.id,
_15
res
_15
);
_15
return data?.name;
_15
}
_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.

Add these to src/providers/AuthProvider.tsx and export them as part of the AuthContext like we did for 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 to create an HTTP interceptor that adds the token.

We don't need an interceptor for this app, but as a challenge for you, update Tab1.tsx 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/providers/AuthProvider.tsx that does the refresh, and then modify getAuthResult() to call it when needed.


_24
const refreshAuth = async (
_24
authResult: AuthResult
_24
): Promise<AuthResult | null> => {
_24
let newAuthResult: AuthResult | null = null;
_24
_24
if (await AuthConnect.isRefreshTokenAvailable(authResult)) {
_24
try {
_24
newAuthResult = await AuthConnect.refreshSession(provider, authResult);
_24
} catch (err) {
_24
console.log("Error refreshing session.", err);
_24
}
_24
}
_24
_24
return newAuthResult;
_24
};
_24
_24
const getAuthResult = async (): Promise<AuthResult | null> => {
_24
if (authResult && (await AuthConnect.isAccessTokenExpired(authResult))) {
_24
const newAuthResult = await refreshAuth(authResult);
_24
setAuthResult(newAuthResult);
_24
}
_24
setIsAuthenticated(!!authResult);
_24
return authResult;
_24
};

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/providers/AuthProvider.tsx. 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 Ionic 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/providers/SessionVaultProvider.tsx:


_10
import {
_10
BrowserVault,
_10
IdentityVaultConfig,
_10
Vault,
_10
} from "@ionic-enterprise/identity-vault";
_10
import { isPlatform } from "@ionic/react";
_10
_10
const createVault = (config: IdentityVaultConfig): Vault | BrowserVault => {
_10
return isPlatform("hybrid") ? new Vault(config) : new BrowserVault(config);
_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.

With the factory in place to build our Vault, let's create a Context that will allow us to manage our authentication result. Add the following code to src/providers/SessionVaultProvider.tsx:


_47
...
_47
const key = 'auth-result';
_47
const vault = createVault({
_47
key: 'io.ionic.gettingstartedacreact',
_47
type: VaultType.SecureStorage,
_47
deviceSecurityType: DeviceSecurityType.None,
_47
lockAfterBackgrounded: 5000,
_47
shouldClearVaultAfterTooManyFailedAttempts: true,
_47
customPasscodeInvalidUnlockAttempts: 2,
_47
unlockVaultOnLoad: false,
_47
});
_47
_47
export const SessionVaultContext = createContext<{
_47
clearSession: () => Promise<void>;
_47
getSession: () => Promise<AuthResult | null>;
_47
setSession: (value?: AuthResult) => Promise<void>;
_47
}>({
_47
clearSession: () => {
_47
throw new Error('Method not implemented.');
_47
},
_47
getSession: () => {
_47
throw new Error('Method not implemented.');
_47
},
_47
setSession: () => {
_47
throw new Error('Method not implemented.');
_47
},
_47
});
_47
_47
export const SessionVaultProvider: React.FC<PropsWithChildren> = ({ children }) => {
_47
const clearSession = (): Promise<void> => {
_47
return vault.clear();
_47
};
_47
_47
const getSession = (): Promise<AuthResult | null> => {
_47
return vault.getValue<AuthResult>(key);
_47
};
_47
_47
const setSession = (value?: AuthResult): Promise<void> => {
_47
return vault.setValue(key, value);
_47
};
_47
_47
return (
_47
<SessionVaultContext.Provider value={{ clearSession, getSession, setSession }}>
_47
{children}
_47
</SessionVaultContext.Provider>
_47
);
_47
};

Then, add the provider to App.tsx. Place the component in between <IonApp> and <AuthProvider>, like so:


_10
<IonApp>
_10
<SessionVaultProvider>
_10
<AuthProvider>...</AuthProvider>
_10
</SessionVaultProvider>
_10
</IonApp>

Finally, modify src/providers/AuthProvider.tsx to use SessionVaultContext. 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 const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false); line of code and replace it with the following:


_10
const { clearSession, getSession, setSession } =
_10
useContext(SessionVaultContext);

Create a function named saveAuthResult(). This function will either save the AuthResult to the Vault, or clear the auth result from the Vault when the session is no longer valid.


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

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


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

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


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

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

Guard the Routes

It's common to have routes in your application that only logged in users could see.

One way this could be achieved is by using the isAuthenticated state variable to guard the routes. In a production scenario, the route guard component could look something like this:


_16
import { useContext, useEffect, useState } from "react";
_16
import { Redirect, Route, useLocation } from "react-router";
_16
import { AuthContext } from "./providers/AuthProvider";
_16
_16
export const PrivateRoute = ({ children }: any) => {
_16
const { getAccessToken, isAuthenticated } = useContext(AuthContext);
_16
_16
// Calling `getAccessToken()` will check if the session is valid,
_16
// and update `isAuthenticated` accordingly.
_16
useEffect(() => {
_16
getAccessToken();
_16
}, [getAccessToken]);
_16
_16
if (!isAuthenticated) return <Redirect to="/login" />;
_16
return children;
_16
};

<PrivateRoute /> would then wrap protected components like so:


_10
<Route path="/user-settings">
_10
<PrivateRoute>
_10
<UserSettings />
_10
</PrivateRoute>
_10
</Route>

If the current user is not authenticated, when /user-settings is navigated to, the application will redirect to /login.