I have an Ionic app that needs to authenticate in Azure, so I installed MSAL following this tutorial: https://learn.microsoft.com/en-us/graph/tutorials/angular
It works like a charm with "ionic serve" but when I run it in the device, it crashes when I try to sign in Azure. I think it is because the popup window MSAL shows for login is not allowed in Ionic.
So my first attempt was to change the loginPopup() call for a loginRedirect(). So I removed this code:
async signIn(): Promise<void> {
const result = await this.msalService
.loginPopup(OAuthSettings)
.toPromise()
.catch((reason) => {
this.alertsService.addError('Login failed',
JSON.stringify(reason, null, 2));
});
if (result) {
this.msalService.instance.setActiveAccount(result.account);
this.authenticated = true;
this.user = await this.getUser();
}
}
And I added this new one (based on https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/errors.md):
async signIn(): Promise<void> {
await this.msalService.instance.handleRedirectPromise();
const accounts = this.msalService.instance.getAllAccounts();
if (accounts.length === 0) {
// No user signed in
this.msalService.instance.loginRedirect();
}
}
But this way, the user information is not saved, because I have not a "result" to handle or to call to setActiveAccount(result). It did not work even in "ionic serve", so I discarded this approach.
The second approach, after seaching for a posible solution for two days, was to show the popup in a InAppBrowser (https://ionicframework.com/docs/native/in-app-browser), so I changed the code to:
async signIn(): Promise<void> {
const browser = this.iab.create('https://www.microsoft.com/');
browser.executeScript({ code: "\
const result = await this.msalService\
.loginPopup(OAuthSettings)\
.toPromise()\
.catch((reason) => {\
this.alertsService.addError('Login failed',\
JSON.stringify(reason, null, 2));\
});\
if (result) {\
this.msalService.instance.setActiveAccount(result.account);\
this.authenticated = true;\
this.user = await this.getUser();\
}"
});
}
But it just open a new window and do nothing more, it does not execute loginPopup(), so I also discard this second approach.
Anyone knows how to avoid the popup problem in Ionic?
Thank you
I can confirm Paolo Cuscelas solution is working. We were using ionic & capacitor with the cordova InAppBrowser, since capacitors browser does not support listening to url changes, which is needed in order to "proxy" the msal route params.
Also, make sure to register the redirection uri in your azure portal.
The rest of the application is more or less setup based on the provided examples from microsoft for msal/angular package.
CustomNavigationClient for Capacitor
Make sure you setup the msal interaction type to "InteractionType.Redirect"
The constructor requires you to pass in a InAppBrowser reference.
Also azure returns the data in the url through #code instead of #state, so make sure to split the url accordingly.
class CustomNavigationClient extends NavigationClient {
constructor(private iab: InAppBrowser) {
super();
}
async navigateExternal(url: string, options: any) {
if (Capacitor.isNativePlatform()) {
const browser = this.iab.create(url, '_blank', {
location: 'yes',
clearcache: 'yes',
clearsessioncache: 'yes',
hidenavigationbuttons: 'yes',
hideurlbar: 'yes',
fullscreen: 'yes'
});
browser.on('loadstart').subscribe(event => {
if (event.url.includes('#code')) {
// Close the in app browser and redirect to localhost + the state parameter
browser.close();
const domain = event.url.split('#')[0];
const url = event.url.replace(domain, 'http://localhost/home');
console.log('will redirect to:', url);
window.location.href = url;
}
});
} else {
if (options.noHistory) {
window.location.replace(url);
} else {
window.location.assign(url);
}
}
return true;
}
}
app.component.ts
Register the navigation client
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { InAppBrowser } from '@awesome-cordova-plugins/in-app-browser/ngx';
import { MsalBroadcastService, MsalService } from '@azure/msal-angular';
import { AuthenticationResult, EventMessage, EventType, NavigationClient } from '@azure/msal-browser';
import { Capacitor } from '@capacitor/core';
import { Subject } from 'rxjs';
import { filter, tap } from 'rxjs/operators';
import { AzureAuthService } from '@core/auth';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
constructor(
private azureAuthService: AzureAuthService,
private authService: MsalService,
private msalBroadcastService: MsalBroadcastService,
private router: Router,
private iab: InAppBrowser,
private msalService: MsalService,
) {
this.msalService.instance.setNavigationClient(new CustomNavigationClient(this.iab));
}
ngOnInit(): void {
this.msalBroadcastService.msalSubject$
.pipe(
filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS),
)
.subscribe((result: EventMessage) => {
console.log('--> login success 1: ', result);
const payload = result.payload as AuthenticationResult;
this.authService.instance.setActiveAccount(payload.account);
// custom service to handle authentication result within application
this.azureAuthService.handleAuthentication(payload)
.pipe(
tap(() => {
console.log('--> login success 2: ');
this.router.navigate(['/home']);
})
)
.subscribe();
});
}
}
package.json
If you are using angulars monorepo approach, make sure you place the dependencies within you project specific package.json file, otherwise, when syncing plugins (cordova and capacitor) with npx cap sync, the plugins are ignored. Which results in the error "...plugin_not_installed"
"dependencies": {
...
"@capacitor/android": "3.4.1",
"cordova-plugin-inappbrowser": "^5.0.0",
"@awesome-cordova-plugins/in-app-browser": "^5.39.1",
"@azure/msal-angular": "^2.0.1",
"@azure/msal-browser": "^2.15.0",
...
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With