Skip to main content
Version: 6.0

Popup vs. Current

Overview

When the application is running in a web context, Auth Connect provides two different options for presenting the authentication page: popup or current. Up to this point, we have been using popup. In this tutorial we will explore current.

With popup, the authentication provider is opened in a new browser tab / window. This mode is the most consistent with how Auth Connect works on mobile where the authentication provider is displayed in a secure web view. On the web, this option requires no extra code, but it may not be the best user experience for web.

If your application is only distributed as a web-native mobile app, and the web-context is only used for development, then it is best to use popup.

current

With current, the authentication provider is opened in the current window, replacing your application. Your application will then be restarted with token information on the URL upon successful login. Since this is fundamentally different than the mobile implementation, it also means that special code is needed to handle it.

If your application is distributed in a web context, it is worth considering using current for an improved user experience.

General Strategy

When using popup, it is very common to have login logic such as the following in the component used for authentication:


_10
const handleSignIn = async () => {
_10
try {
_10
await login();
_10
setLoginFailed(false);
_10
history.replace('/');
_10
} catch (error) {
_10
setLoginFailed(true);
_10
}
_10
};

With this code:

  • The login() is called and Auth Connect opens the OIDC authentication provider in a new tab (web) or a secure web view (mobile).
  • Auth Connect listens for that tab or secure web view to close.
  • If the user successfully logs in:
    • Auth Connect unpacks the data sent back when the tab or secure web view is closed and creates an AuthResult.
    • Our login() stores the AuthResult and resolves.
  • If the user cancels the operation, our login() rejects with an error.

Since current only applies to the web, this code will still work like this on mobile. However, in a web context our app is completely replaced by the OIDC authentication provider's login page. As such, we are no longer awaiting the login and need to use a completely different mechanism to capture the authentication result when the app restarts.

On web, the flow becomes:

  • The login() is called and Auth Connect replaces the application with the OIDC authentication provider's login page.
  • The user logs in or cancels.
  • The application is restarted using the configured redirectUri.
  • In the case of a successful login, the authentication information will be included in the URL. It will need to be processed by our application.

In our case, the application is using the /auth-action-complete route. We can then use the page for that route to perform the following tasks:

  • Determine if the application is running on the web.
  • If so:
    • Call a process that will examine the extra parameters for the URL.
      • If parameters exist, this was a successful login, and the parameters are used to construct an AuthResult which is stored in the session vault.
      • If parameters do not exist, this was a logout and the session vault is cleared.
    • Continue navigation to the appropriate page within the application.

Let's Code

This tutorial builds upon the application created when doing the getting started tutorial and converts it from using popup to using current. If you have the code from when you performed that tutorial, then you are good to go. If you need the code you can make a copy from our GitHub repository.

The Authentication Utilities

The first thing that needs to be done is to modify the Auth Connect configuration to use current mode on the web. A function is then created that handles the URL parameters when Auth Connect restarts our application after login or logout.

src/utils/authentication.ts

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

Start by having a look at the current configuration that is used for our AuthConnect.setup() call. Note that it is using a uiMode of popup.

Also note the return URLs. The page(s) accessed by that route is where we will need to make some modifications.

src/utils/authentication.ts

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

Change the uiMode to current.

src/utils/authentication.ts

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

Since we will be coming back into the app after login, we need a function to handle that. For now just read the URL search parameters.

src/utils/authentication.ts

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

If search parameters are present, then this is the login returning. We package the data and send it to Auth Connect to process and create an AuthResult for the session.

If there are no parameters, we will assume a logout and set the session to null.

The first thing that needs to be done is to modify the Auth Connect configuration to use current mode on the web. A function is then created that handles the URL parameters when Auth Connect restarts our application after login or logout.

Start by having a look at the current configuration that is used for our AuthConnect.setup() call. Note that it is using a uiMode of popup.

Also note the return URLs. The page(s) accessed by that route is where we will need to make some modifications.

Change the uiMode to current.

Since we will be coming back into the app after login, we need a function to handle that. For now just read the URL search parameters.

If search parameters are present, then this is the login returning. We package the data and send it to Auth Connect to process and create an AuthResult for the session.

If there are no parameters, we will assume a logout and set the session to null.

src/utils/authentication.ts

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

note

URLSearchParams.size is supported by all modern browsers. If you need to support older browsers, you can use Array.from(params.keys()).length in place of params.size.

Auth Action Completed Page

The Auth Connect configuration for the application redirects back into the application via the /auth-action-complete route. The code needs to determine if we are running in a web context, and if so:

  • Handle the authentication.
  • Route back to the root page.
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;

The AuthActionCompletePage currently just contains markup to show a spinner. No logic is in place.

src/pages/AuthActionCompletePage.tsx

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

Modify the functional component syntax such that we can start adding the required logic.

src/pages/AuthActionCompletePage.tsx

_23
import { useEffect } from 'react';
_23
import { Capacitor } from '@capacitor/core';
_23
import { IonContent, IonPage, IonSpinner } from '@ionic/react';
_23
import './AuthActionCompletePage.css';
_23
_23
const AuthActionCompletePage: React.FC = () => {
_23
useEffect(() => {
_23
if (!Capacitor.isNativePlatform())) {
_23
}
_23
}, []);
_23
_23
return (
_23
<IonPage>
_23
<IonContent className="main-content auth-action-complete">
_23
<div className="container">
_23
<IonSpinner name="dots" />
_23
</div>
_23
</IonContent>
_23
</IonPage>
_23
);
_23
};
_23
_23
export default AuthActionCompletePage;

Import Capacitor so the page can determine if it is running in a native context or not. Wrap this in a useEffect.

src/pages/AuthActionCompletePage.tsx

_25
import { useEffect } from 'react';
_25
import { Capacitor } from '@capacitor/core';
_25
import { IonContent, IonPage, IonSpinner } from '@ionic/react';
_25
import { handleAuthCallback } from '../utils/authentication';
_25
import './AuthActionCompletePage.css';
_25
_25
const AuthActionCompletePage: React.FC = () => {
_25
useEffect(() => {
_25
if (!Capacitor.isNativePlatform())) {
_25
handleAuthCallback();
_25
}
_25
}, [handleAuthCallback]);
_25
_25
return (
_25
<IonPage>
_25
<IonContent className="main-content auth-action-complete">
_25
<div className="container">
_25
<IonSpinner name="dots" />
_25
</div>
_25
</IonContent>
_25
</IonPage>
_25
);
_25
};
_25
_25
export default AuthActionCompletePage;

If the application is not running in a native context, handle the return from the OIDC authentication provider.

src/pages/AuthActionCompletePage.tsx

_28
import { useEffect } from 'react';
_28
import { Capacitor } from '@capacitor/core';
_28
import { IonContent, IonPage, IonSpinner } from '@ionic/react';
_28
import { useHistory } from 'react-router-dom';
_28
import { handleAuthCallback } from '../utils/authentication';
_28
import './AuthActionCompletePage.css';
_28
_28
const AuthActionCompletePage: React.FC = () => {
_28
const history = useHistory();
_28
_28
useEffect(() => {
_28
if (!Capacitor.isNativePlatform())) {
_28
handleAuthCallback().then(() => history.replace('/'));
_28
}
_28
}, [handleAuthCallback]);
_28
_28
return (
_28
<IonPage>
_28
<IonContent className="main-content auth-action-complete">
_28
<div className="container">
_28
<IonSpinner name="dots" />
_28
</div>
_28
</IonContent>
_28
</IonPage>
_28
);
_28
};
_28
_28
export default AuthActionCompletePage;

Once the URL has been handled, redirect to the root page.

The AuthActionCompletePage currently just contains markup to show a spinner. No logic is in place.

Modify the functional component syntax such that we can start adding the required logic.

Import Capacitor so the page can determine if it is running in a native context or not. Wrap this in a useEffect.

If the application is not running in a native context, handle the return from the OIDC authentication provider.

Once the URL has been handled, redirect to the root page.

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;

Note: for this application, redirecting to the root page is the correct thing to do. In a more complex application, it may be more appropriate to check various states before determining the route. If so, such logic should be abstracted into a routing routine.

Next Steps

If you have not already done so, please see the following tutorials:

Happy coding!! 🤓