AzureAD / microsoft-authentication-library-for-js

Microsoft Authentication Library (MSAL) for JS
http://aka.ms/aadv2
MIT License
3.64k stars 2.64k forks source link

Redirect URL in hybrid (cordova) app results in error #5052

Closed mpbiehn closed 2 years ago

mpbiehn commented 2 years ago

Core Library

MSAL.js v2 (@azure/msal-browser)

Core Library Version

2.27.0

Wrapper Library

MSAL React (@azure/msal-react)

Wrapper Library Version

1.4.3

Description

I'm trying to leverage msal-react within a hybrid app using Cordova. Web is working great but I can't figure out what redirectUri to specify to get it working in the hybrid app (web view). I've leveraged the react B2C sample, modified slightly to test this out in our hybrid app.

The issue I'm having is getting the hybrid app to recognize the redirectUri, which is specified in our App Registration under Single-Page Application Redirect URIs.

Error Message

Running the hybrid app locally with my Android device connected up to Chrome, I get a "Webpage not available" error with ERR_NAME_NOT_RESOLVED.

image

image

If I "refresh" the hybrid app in Dev Tools while on the error message screen the error goes away and my app refreshes and I'm logged in so it looks like msal-react is grabbing the hash from the location.path, just can't figure out how to handle redirectUri to prevent the initial error.

Msal Logs

No response

MSAL Configuration

auth: {
    clientId: "our-client-id",
    authority:
      "https://our-custom-b2c.domain/our-tenantid.onmicrosoft.com/b2c_1a_our-policy",
    knownAuthorities: ["https://our-custom-b2c.domain"],
    redirectUri: "https://our-app/", // which is listed in our B2C app registration
    postLogoutRedirectUri: "https://our-app/logout",
    navigateToLoginRequestUrl: true
  },
  cache: {
    cacheLocation: "sessionStorage",
    storeAuthStateInCookie: false,
    secureCookies: true
  },
  system: {
    loggerOptions: {
      loggerCallback: (level, message, containsPii) => {
        if (containsPii) {
          return;
        }
        switch (level) {
          case LogLevel.Error:
            console.error(message);
            return;
          case LogLevel.Info:
            console.info(message);
            return;
          case LogLevel.Verbose:
            console.debug(message);
            return;
          case LogLevel.Warning:
            console.warn(message);
            return;
          default:
            return;
        }
      }
    }
  }

Relevant Code Snippets

import React from "react";
import { useMsal } from "@azure/msal-react";
import { loginRequest } from "../util/authConfig";

const LogInButton = () => {
  const { instance } = useMsal();

  const handleLogin = async () => {

    instance
      .loginRedirect(loginRequest)
      .then((response) => {
        console.log("login successful", response);
      })
      .catch((error) => {
        console.log(error);
      });
  };

  return (
    <React.Fragment>
      <button
        className="btn btn-primary"
        onClick={handleLogin}
        type="button"
      >
        Login Redirect
      </button>
    </React.Fragment>
  );
};

export default LogInButton;

Reproduction Steps

Leveraging the React B2C sample app:

  1. build app and run on android device

    cordova run android --device

  2. Once app opens on device, select Login Redirect button, which executes msalInstance.loginRedirect(msalConfig) image

  3. Sign in using my userid/password image

  4. B2C redirects back to app, which is when I get the error: ERR_NAME_NOT_RESOLVED

  5. Select Refresh in DevTools, note that the app indicates I've logged in. I can navigate to the Protected sample to get see the data image

Likewise, if I specify our "web" redirectUri for the hybrid apps, I'm able to login but I end up having to "refresh" or select the login button again for react to recognize that I've authenticated.

Expected Behavior

I would expect the hybrid app to handle the redirectUri like it does for the web browser.

Identity Provider

Azure B2C Custom Policy

Browsers Affected (Select all that apply)

Other

Regression

No response

Source

External (Customer)

jasonnutter commented 2 years ago

@mpbiehn Is this domain of this domain in the picture localhost bychance?

This error indicates that the domain is not accessible from the device, which is common mistake if using localhost. Make sure when running the app on a phone, that the application domain (and redirect uri, which must be on teh same domain) is accessible from the device.

mpbiehn commented 2 years ago

@jasonnutter thanks for the question. Nope, the domain redacted in the screenshot is not localhost. It's the scheme://hostname we have set in config.xml of our Cordova app. I've tried several alternatives, including the domain of our web URL, without any luck.

phyr0s commented 2 years ago

@jasonnutter Same problem in android and iOS i can get this log in xCode -->

2022-08-02 11:49:54.796392+0200 demo-app[6468:453896] webView:didFailProvisionalNavigation - -1004: Could not connect to the server.

2022-08-02 11:50:21.057345+0200 demo-app[6468:453896] Could not signal service com.apple.WebKit.WebContent: 113: Could not find specified service

To test just add this lines to config.xml file in cordova application and ensure that cordova-plugin-ionic-webview is installed and cordova-plugin-inappbrowser (cordova plugin list --> command to show installed plugins )

Error happen also using capacitor not only Cordova.

config.xml code -->

 <preference name="UseScheme" value="true" />
    <preference name="Scheme" value="https" />
    <preference name="HostName" value="mycoolApp" />
    <preference name="iosScheme" value="httpsionic" />

Probably the error is when this library pick the location origin from window.location.origin. In mobile hybrid apps location origin can be fileSystem

GusLAN commented 2 years ago

@jasonnutter Same problem in android and iOS i can get this log in xCode -->

2022-08-02 11:49:54.796392+0200 demo-app[6468:453896] webView:didFailProvisionalNavigation - -1004: Could not connect to the server.

2022-08-02 11:50:21.057345+0200 demo-app[6468:453896] Could not signal service com.apple.WebKit.WebContent: 113: Could not find specified service

To test just add this lines to config.xml file in cordova application and ensure that cordova-plugin-ionic-webview is installed and cordova-plugin-inappbrowser (cordova plugin list --> command to show installed plugins )

Error happen also using capacitor not only Cordova.

config.xml code -->

<preference name="UseScheme" value="true" />
 <preference name="Scheme" value="https" />
 <preference name="HostName" value="mycoolApp" />
 <preference name="iosScheme" value="httpsionic" />

Probably the error is when this library pick the location origin from window.location.origin. In mobile hybrid apps location origin can be fileSystem

Same error

jasonnutter commented 2 years ago

@jasonnutter Jason Nutter FTE thanks for the question. Nope, the domain redacted in the screenshot is not localhost. It's the scheme://hostname we have set in config.xml of our Cordova app. I've tried several alternatives, including the domain of our web URL, without any luck.

@mpbiehn Unfortunately, I'm not sure how much we can help here, given this is a network level error, and likely not caused by MSAL itself. Furthermore, we don't fully support Ionic/Cordova at this time, so there is limited expertise we can provide.

mpbiehn commented 2 years ago

@jasonnutter bummer, would love to get this added to the feature request list. :)

snydergd commented 2 years ago

Possibly related to this is this issue for cordova-plugin-ionic-webview: https://github.com/ionic-team/cordova-plugin-ionic-webview/issues/623 -- according to that there seems that external sites may not be able to successfully do a 302 redirect (or similar) back to the cordova app. Maybe this could be some sort of cross-site security measure. A workaround provided there is to have a separate external web page receive the 302 redirect and use javascript there to direct the user back to your app, or (if possible), maybe even having Azure use javascript for the redirect directly vs. an HTTP redirect.

jasonnutter commented 2 years ago

or (if possible), maybe even having Azure use javascript for the redirect directly vs. an HTTP redirect.

AAD should already do this for browser scenarios, can you debug and double check?

mpbiehn commented 2 years ago

@jasonnutter still working out some kinks but came across a react-router-sample and an Angular example that leveraged CustomNavigationClient and Cordova's InAppBrowser to get login/logout working in hybrid app. Using CustomNavigationClient's navigateExternal we can leverage cordova.InAppBrowser to capture the redirect from the auth provider and handle #state passed in the redirectUri accordingly.

Still have to test on iOS but here's a working version (happy path) of CustomNavigationClient on Android.

import { NavigationClient } from "@azure/msal-browser";

export class CustomNavigationClient extends NavigationClient {
  constructor(history) {
    super();
    this.history = history;
  }

  async navigateInternal(url, options) {
    const relativePath = url.replace(window.location.origin, "");
    if (options.noHistory) {
      this.history.replace(relativePath);
    } else {
      this.history.push(relativePath);
    }

    return false;
  }

  async navigateExternal(url, options) {
    console.log("navigateExternal", url, options);

    if (window.hasOwnProperty("cordova")) {
      let windowOptions =
        "hidenavigationbuttons=yes,hideurlbar=yes,closebuttoncaption=Close,";

      // window options vary depending on android/ios and vary based on the redirect login from Azure B2C policy
      if (appEnv.REACT_APP_PLATFORM === "android") {
        windowOptions += "location=yes,zoom=no,toolbarcolor=#9a3324";
      } else if (appEnv.REACT_APP_PLATFORM === "ios") {
        windowOptions +=
       "location=no,toolbartranslucent=false,toolbarcolor=#ffffff,toolbarposition=bottom,lefttoright=yes,closebuttoncolor=#000000";
      }

      const ref = window.cordova.InAppBrowser.open(
        url,
        "_blank",
        windowOptions
      );

      // Check if the appbrowser started a navigation
      ref.addEventListener("loadstart", (event) => {
        console.log("loadstart", event);

        if (event.url.includes("#state")) {
          //#state will be including in the redirectUri after login which Cordova can't handle OOTB (results in network error)

          // Close the in app browser
          ref.close();

          // update location.href to origin/#state=
          const myUrl = new URL(event.url);
          const newUrl = new URL(
            `${appEnv.REACT_APP_OAUTH_REDIRECT_URI}/${myUrl.hash}`
          );
          window.location.href = newUrl;
        } else if (event.url.includes("post_logout_redirect_uri")) {
          // post_logout_redirect_uri will be included in the logout redirect, which Cordova can't handle (results in network error)

          // Close the in app browser
          ref.close();

          // update location.href to post_logout_redirect_uri?client-request-id=
          const myUrl = new URL(event.url);
          const redirectUri = myUrl.searchParams.get(
            "post_logout_redirect_uri"
          );
          const clientRequestId = myUrl.searchParams.get("client-request-id");

          // it's important that newUrl is a valid route in the app, in my case I have the route configured to redirect to ${redirectUri}/index.html
          const newUrl = new URL(
            `${redirectUri}?client-request-id=${clientRequestId}`
          );

          window.location.href = newUrl.href;
        }
      });
    } else {
      if (options.noHistory) {
        window.location.replace(url);
      } else {
        window.location.assign(url);
      }
    }

    return true;
  }
}
jasonnutter commented 2 years ago

@mpbiehn Interesting, thanks for the update.

snydergd commented 2 years ago

@jasonnutter I've poked around for the Javascript redirect a little bit. I'm looking at the options for the auth code endpoint and do not see one to have redirection done with Javascript rather than with an HTTP 302. I have tried using PKCE challenges, using response_type=token or response_type=id_token, and using response_mode=fragment - but no matter what I've still been getting an HTTP 302 redirect rather than a javascript one.

I also thought maybe it had to do with my app registration configuration. I switched the redirect URI type to be "SPA" instead of "Web", but I also received an HTTP 302 redirect in that scenario. Am thinking maybe there is another setting hidden in here somewhere but so far have been striking out on finding other settings that could be relevant.

Can you confirm that this is where you were thinking this might be supported? Am I just missing it? It will be awesome if that is a solution and all that is needed is to tweak some settings rather than using the CustomNavigationClient approach above.

Thanks a bunch for the tip.

jasonnutter commented 2 years ago

@snydergd It would be done for requests made to the authorize endpoint. It is possible the server doesn't always do a javascript-based redirect. I'll see if I can find out more.

jasonnutter commented 2 years ago

@snydergd Looks like this behavior is done conditionally and is not something you have direct control over, unfortunately.

ghost commented 2 years ago

@mpbiehn This issue has been automatically marked as stale because it is marked as requiring author feedback but has not had any activity for 5 days. If your issue has been resolved please let us know by closing the issue. If your issue has not been resolved please leave a comment to keep this open. It will be closed automatically in 7 days if it remains stale.

phyr0s commented 2 years ago

@jasonnutter any news?

hectormmg commented 2 years ago

I would recommend trying to register the redirect URI as mobile/desktop rather than SPA or Web, given that the mobile device is technically a native public client. Although we're not seeing the server error that is my best guess on what could be the problem. It's important that the redirect URI registered as mobile/desktop isn't exactly the same as the one used for the web URL since the portal won't allow a duplicate.

snydergd commented 2 years ago

@hectormmg I updated the redirect URI to be InstalledClient in the app registration which was the only one of the documented options that I didn't try previously. This is also giving me a 302 redirect rather than a JavaScript one, though.

phyr0s commented 2 years ago

Reopen this issue

snydergd commented 2 years ago

@hectormmg It looks like the issue has closed due to lack of a response from the original author, but it does not seem that the issue has been resolved quite yet for the others of us contributing to it. Can it be reopened? Also, was my last post the correct way to try what you were suggesting, or should I try something else?

Hrushi01 commented 9 months ago

Still trying to resolve the issue but not able to as the msal-openid is failing to give any response image (3) image (2)