Skip to main content
Version: 5.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 signinClicked = async () => {
_10
try {
_10
await login();
_10
router.replace('/');
_10
} catch (e) {
_10
loginFailed.value = 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 Composable

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/composables/authentication.ts

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

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 need to eventually modify.

src/composables/authentication.ts

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

Change the uiMode to current.

src/composables/authentication.ts

_65
import { useSession } from '@/composables/session';
_65
import { Capacitor } from '@capacitor/core';
_65
import { Auth0Provider, AuthConnect, AuthResult, ProviderOptions } from '@ionic-enterprise/auth';
_65
_65
const isNative = Capacitor.isNativePlatform();
_65
const provider = new Auth0Provider();
_65
const authOptions: ProviderOptions = {
_65
audience: 'https://io.ionic.demo.ac',
_65
clientId: 'yLasZNUGkZ19DGEjTmAITBfGXzqbvd00',
_65
discoveryUrl: 'https://dev-2uspt-sz.us.auth0.com/.well-known/openid-configuration',
_65
logoutUrl: isNative ? 'io.ionic.acdemo://auth-action-complete' : 'http://localhost:8100/auth-action-complete',
_65
redirectUri: isNative ? 'io.ionic.acdemo://auth-action-complete' : 'http://localhost:8100/auth-action-complete',
_65
scope: 'openid offline_access email picture profile',
_65
};
_65
let authResult: AuthResult | null = null;
_65
_65
const { clearSession, getSession, setSession } = useSession();
_65
_65
const getAuthResult = async (): Promise<AuthResult | null> => {
_65
return getSession();
_65
};
_65
_65
const saveAuthResult = async (authResult: AuthResult | null): Promise<void> => {
_65
if (authResult) {
_65
await setSession(authResult);
_65
} else {
_65
await clearSession();
_65
}
_65
};
_65
_65
const isReady: Promise<void> = AuthConnect.setup({
_65
platform: isNative ? 'capacitor' : 'web',
_65
logLevel: 'DEBUG',
_65
ios: {
_65
webView: 'private',
_65
},
_65
web: {
_65
uiMode: 'current',
_65
authFlow: 'PKCE',
_65
},
_65
});
_65
_65
export const useAuthentication = () => ({
_65
handleLoginReturn: async (): Promise<void> => {
_65
await isReady;
_65
const params = new URLSearchParams(window.location.search);
_65
},
_65
isAuthenticated: async (): Promise<boolean> => {
_65
const authResult = await getAuthResult();
_65
return !!authResult && (await AuthConnect.isAccessTokenAvailable(authResult));
_65
},
_65
login: async (): Promise<void> => {
_65
await isReady;
_65
authResult = await AuthConnect.login(provider, authOptions);
_65
await saveAuthResult(authResult);
_65
},
_65
logout: async (): Promise<void> => {
_65
await isReady;
_65
const authResult = await getAuthResult();
_65
if (authResult) {
_65
await AuthConnect.logout(provider, authResult);
_65
saveAuthResult(null);
_65
}
_65
},
_65
});

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

src/composables/authentication.ts

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

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.

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

Either way, we will save the current authResult.

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 need to eventually modify.

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 wait for Auth Connect to be ready, then 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.

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

Either way, we will save the current authResult.

src/composables/authentication.ts

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

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/views/AuthActionCompletePage.vue

_23
<template>
_23
<ion-content class="main-content">
_23
<div class="container">
_23
<ion-spinner name="dots"></ion-spinner>
_23
</div>
_23
</ion-content>
_23
</template>
_23
_23
<script setup lang="ts">
_23
import { IonContent, IonSpinner } from '@ionic/vue';
_23
</script>
_23
_23
<style scoped>
_23
.container {
_23
text-align: center;
_23
_23
position: absolute;
_23
left: 0;
_23
right: 0;
_23
top: 50%;
_23
transform: translateY(-50%);
_23
}
_23
</style>

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

src/views/AuthActionCompletePage.vue

_27
<template>
_27
<ion-content class="main-content">
_27
<div class="container">
_27
<ion-spinner name="dots"></ion-spinner>
_27
</div>
_27
</ion-content>
_27
</template>
_27
_27
<script setup lang="ts">
_27
import { Capacitor } from '@capacitor/core';
_27
import { IonContent, IonSpinner } from '@ionic/vue';
_27
_27
if (!Capacitor.isNativePlatform())) {
_27
}
_27
</script>
_27
_27
<style scoped>
_27
.container {
_27
text-align: center;
_27
_27
position: absolute;
_27
left: 0;
_27
right: 0;
_27
top: 50%;
_27
transform: translateY(-50%);
_27
}
_27
</style>

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

src/views/AuthActionCompletePage.vue

_30
<template>
_30
<ion-content class="main-content">
_30
<div class="container">
_30
<ion-spinner name="dots"></ion-spinner>
_30
</div>
_30
</ion-content>
_30
</template>
_30
_30
<script setup lang="ts">
_30
import { Capacitor } from '@capacitor/core';
_30
import { IonContent, IonSpinner } from '@ionic/vue';
_30
import { useAuthentication } from '@/composables/authentication'
_30
_30
if (!Capacitor.isNativePlatform())) {
_30
const { handleLoginReturn } = useAuthentication();
_30
handleLoginReturn();
_30
}
_30
</script>
_30
_30
<style scoped>
_30
.container {
_30
text-align: center;
_30
_30
position: absolute;
_30
left: 0;
_30
right: 0;
_30
top: 50%;
_30
transform: translateY(-50%);
_30
}
_30
</style>

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

src/views/AuthActionCompletePage.vue

_32
<template>
_32
<ion-content class="main-content">
_32
<div class="container">
_32
<ion-spinner name="dots"></ion-spinner>
_32
</div>
_32
</ion-content>
_32
</template>
_32
_32
<script setup lang="ts">
_32
import { Capacitor } from '@capacitor/core';
_32
import { useRouter } from 'vue-router';
_32
import { IonContent, IonSpinner } from '@ionic/vue';
_32
import { useAuthentication } from '@/composables/authentication'
_32
_32
if (!Capacitor.isNativePlatform())) {
_32
const { handleLoginReturn } = useAuthentication();
_32
const router = useRouter();
_32
handleLoginReturn().then(() => router.replace('/'));
_32
}
_32
</script>
_32
_32
<style scoped>
_32
.container {
_32
text-align: center;
_32
_32
position: absolute;
_32
left: 0;
_32
right: 0;
_32
top: 50%;
_32
transform: translateY(-50%);
_32
}
_32
</style>

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.

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

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/views/AuthActionCompletePage.vue

_23
<template>
_23
<ion-content class="main-content">
_23
<div class="container">
_23
<ion-spinner name="dots"></ion-spinner>
_23
</div>
_23
</ion-content>
_23
</template>
_23
_23
<script setup lang="ts">
_23
import { IonContent, IonSpinner } from '@ionic/vue';
_23
</script>
_23
_23
<style scoped>
_23
.container {
_23
text-align: center;
_23
_23
position: absolute;
_23
left: 0;
_23
right: 0;
_23
top: 50%;
_23
transform: translateY(-50%);
_23
}
_23
</style>

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!! 🤓