Ionic and MSAL Authentication
Asked Answered
B

4

8

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

Bobseine answered 30/9, 2021 at 11:20 Comment(2)
Did you get anything about this issue?Flouncing
@BASEERHAIDERJAFRI Nothing. Same situation...Bobseine
M
8

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",
    ...
}
Mate answered 12/2, 2022 at 23:49 Comment(6)
I will take a look deeper as soon as I can. I was not able to make it work, but I will try again with your code. Thank you a lotBobseine
How do you configure your redirection URI in Azure Portal? As a single page app or as a mobile app? Do you set it as "localhost:8100"?Bobseine
I registered the app as Single Page App and added localhost:8001 aswell as localhost:4200 as return URLs.Mate
Thanks. That is working. That answer should me mark as correct .Trilby
I also confirm that this solution works, but, as @Antonio says, with Ionic 6 and IOS, you have to adjust the replace's statement of the CustomNavigationClient with capacitor://localhost... instead of localhostSpiry
When trying this implementation I get the next exception: NullInjectorError: R3InjectorError(LoginPageModule)[[object Object] -> [object Object] -> [object Object]]: NullInjectorError: No provider for [object Object]! Does somebody else had this issue?Flout
R
6

I managed to solve this with cordova-plugin-inappbrowser by using a custom navigation client, here's my implementation:

CustomNavigationClient


    class CustomNavigationClient extends NavigationClient {
      async navigateExternal(url: string, options: any) {
        // Cortdova implementation
        if (window.hasOwnProperty("cordova")) {
          var ref = cordova.InAppBrowser.open(url, '_blank', 'location=yes,clearcache=yes,clearsessioncache=yes');

          // Check if the appbrowser started a navigation
          ref.addEventListener('loadstart', (event: any) => {
            // Check if the url contains the #state login parameter
            if (event.url.includes('#state')) {
              // Close the in app browser and redirect to localhost + the state parameter
              // msal-login is a fake route to trigger a page refresh
              ref.close();
              const domain = event.url.split('#')[0];
              const url = event.url.replace(domain, 'http://localhost/msal-login');
              window.location.href = url;
            }
          });
        } else {
          if (options.noHistory) {
            window.location.replace(url);
          } else {
            window.location.assign(url);
          }
        }
        return true;
      }
    }

app.component.ts


    const navigationClient = new CustomNavigationClient();
    this.msalService.instance.setNavigationClient(navigationClient);
    
    this.msalService.instance.handleRedirectPromise().then((authResult: any) => {
      console.debug('AuthResult ---->', authResult);
      if (authResult) { 
        // your login logic goes here. 
      } else {
        this.msalService.instance.loginRedirect();
      }
    });

Ringside answered 13/12, 2021 at 16:8 Comment(7)
Your answer could be improved by adding more information on what the code does and how it helps the OP.Rustin
Hi @Paolo, I will try your proposed solution as soon as I can. But there is one point I do not see clear: what is http://localhost/msal-login? Does it work in production in an android device with this URL? Thank youBobseine
http://localhost is the root url of your angular application served inside the android device, it will work in production because with cordova you don't fetch any remote path but instead you use local paths, /msal-login is a workaround to be sure that the angular router perceive the route change and re-initialize app.componentRingside
@PaoloCuscela is it an IONIC app? It seems like you are running an angular app (not Ionic). I now it is possible to make it work in an angular app, but the problem appears when you run the IONIC app in a real device.Bobseine
Yes, It's a Ionic cordova app, the part in the if(window.hasOwnProperty("cordova")) clause is the Ionic implementation, if you're using capacitor it may differ a littleRingside
Thanks for your answer! We successfully implemented this for capacitor.Mate
Has anyone actually managed to get this working on iOS? Android is just fine but we're stuck on iOS, can't log in at all. If interested you can check this issue: github.com/AzureAD/microsoft-authentication-library-for-js/…Hagioscope
D
3

I confirm that Jazjef solution works on Ionic 6 using capacitor on Android work whitout problems but on IOS need to change the code of event.url.replace to redirect to the app using capacitor://localhost/, if you let 'http://localhost/' It will try to open the url on the system browser,

async navigateExternal(url: string, options: any) {
// Cortdova implementation
if (Capacitor.isNativePlatform()) {
  var browser = this.iab.create(url, '_blank', 'location=yes,clearcache=yes,clearsessioncache=yes,hidenavigationbuttons=true,hideurlbar=true,fullscreen=true');

  browser.on('loadstart').subscribe(event => {
     if (event.url.includes('#code')) {
       browser.close();
      const domain = event.url.split('#')[0];
     const url = event.url.replace(domain, 'capacitor://localhost/home');
      window.location.href = url;
    }


  });


} else {
  if (options.noHistory) {
   // window.location.replace(url);
  } else {
   // window.location.assign(url);
  }
}
return true;

}

Dixie answered 15/9, 2022 at 10:45 Comment(0)
H
0

I managed to implement Microsoft authentication in my Ionic Angular app using the @recognizebv/capacitor-plugin-msauth plugin. It's a Capacitor plugin that simplifies integration with Azure AD and Microsoft identity platform.

Here's how I got it to work:

Install the plugin:

npm install @recognizebv/capacitor-plugin-msauth

Configure the plugin in capacitor.config.ts:

const config: CapacitorConfig = {
  // ...existing config...
  plugins: {
    MsAuthPlugin: {
      clientId: 'YOUR_CLIENT_ID',
      tenantId: 'YOUR_TENANT_ID',
      scopes: ['user.read'],
    },
  },
};

export default config; Use the plugin in your authentication service:

import { MsAuthPlugin } from '@recognizebv/capacitor-plugin-msauth';

async login() {
  try {
    const result = await MsAuthPlugin.login({ scopes: ['user.read'] });
    const accessToken = result.accessToken;
    // Store the access token securely
  } catch (error) {
    console.error('Authentication error:', error);
  }
}

Ensure platform-specific configurations are set up:

iOS: Add the URL scheme and enable Keychain Sharing.

Android: Update the AndroidManifest.xml with the appropriate intent filter.

You can find more details and setup instructions on the plugin's GitHub page: https://github.com/recognizegroup/capacitor-plugin-msauth

Hello answered 18/9 at 19:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.