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
async signinClicked() {
_10
try {
_10
await this.login();
_10
this.navController.navigateRoot('/');
_10
} catch (e) {
_10
this.loginFailed = true;
_10
}
_10
}

With this code:

  • The login() method 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 Service

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/app/core/authentication.service.ts

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

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/app/core/authentication.service.ts

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

Change the uiMode to current.

src/app/core/authentication.service.ts

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

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/app/core/authentication.service.ts

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

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 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/app/core/authentication.service.ts

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

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/app/auth-action-complete/auth-action-complete.html

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

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

src/app/auth-action-complete/auth-action-complete.html

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

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

src/app/auth-action-complete/auth-action-complete.html

_35
<ion-content class="main-content">
_35
<div class="container">
_35
<ion-spinner name="dots"></ion-spinner>
_35
</div>
_35
</ion-content>
_35
_35
<script setup lang="ts">
_35
import { Capacitor } from '@capacitor/core';
_35
import { IonContent, IonSpinner } from '@ionic/angular';
_35
import { AuthenticationService } from 'src/app/services/authentication.service';
_35
export class AuthActionCompletePage implements OnInit {
_35
constructor(
_35
private auth: AuthenticationService
_35
){}
_35
ngOnInit() {
_35
}
_35
}
_35
async ionViewDidEnter() {
_35
if (!Capacitor.isNativePlatform()) {
_35
await this.auth.handleAuthCallback();
_35
}
_35
}
_35
</script>
_35
_35
<style scoped>
_35
.container {
_35
text-align: center;
_35
_35
position: absolute;
_35
left: 0;
_35
right: 0;
_35
top: 50%;
_35
transform: translateY(-50%);
_35
}
_35
</style>

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

src/app/auth-action-complete/auth-action-complete.html

_38
<ion-content class="main-content">
_38
<div class="container">
_38
<ion-spinner name="dots"></ion-spinner>
_38
</div>
_38
</ion-content>
_38
_38
<script setup lang="ts">
_38
import { Capacitor } from '@capacitor/core';
_38
import { IonContent, IonSpinner } from '@ionic/angular';
_38
import { AuthenticationService } from 'src/app/services/authentication.service';
_38
import { NavController } from '@ionic/angular';
_38
export class AuthActionCompletePage implements OnInit {
_38
constructor(
_38
private navController: NavController,
_38
private auth: AuthenticationService
_38
){}
_38
ngOnInit() {
_38
}
_38
}
_38
async ionViewDidEnter() {
_38
if (!Capacitor.isNativePlatform()) {
_38
await this.auth.handleAuthCallback();
_38
this.navController.navigateRoot('/');
_38
}
_38
}
_38
</script>
_38
_38
<style scoped>
_38
.container {
_38
text-align: center;
_38
_38
position: absolute;
_38
left: 0;
_38
right: 0;
_38
top: 50%;
_38
transform: translateY(-50%);
_38
}
_38
</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/app/auth-action-complete/auth-action-complete.html

_26
<ion-content class="main-content">
_26
<div class="container">
_26
<ion-spinner name="dots"></ion-spinner>
_26
</div>
_26
</ion-content>
_26
_26
<script setup lang="ts">
_26
import { IonContent, IonSpinner } from '@ionic/angular';
_26
export class AuthActionCompletePage implements OnInit {
_26
constructor(){}
_26
ngOnInit() {
_26
}
_26
}
_26
</script>
_26
_26
<style scoped>
_26
.container {
_26
text-align: center;
_26
_26
position: absolute;
_26
left: 0;
_26
right: 0;
_26
top: 50%;
_26
transform: translateY(-50%);
_26
}
_26
</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!! 🤓