Browser.open() rejects my perfectly valid URL. How do I fix this?

Happy new year everyone.

I am currently trying to implement an openIdConnect flow using PingIdentity in an Ionic 7 Angular app. The app is very simple and is just a prototype to demonstrate possible implementations on ping in Ionic 7 to my team as part of a spike. I am testing this in an xCode iPhone simulator.

I am using this sdk created by Ping themselves to connect.

This all works fine except the sdk is created for angular not for ionic, so it by default redirects to ping in a new browser window, rather than an in app browser, as would be more natural for a mobile application. I therefore am trying to edit the signOn method to use the Capacitor Browser API instead.

See below my modified signIn function from auth.js. Refer to the original file here. I have changed out window.open and window.location.replace for Browser.open.

  function signIn(reqOptions) {
    try {
      clearSessionData();

      // verify required parameters
      checkRequiredInfo(
          ['client_id', 'redirect_uri', 'authorization_endpoint']);

      let reqOptionsExist = !!reqOptions;
      let state = null;
      let nonce = null;

      if (reqOptionsExist && (reqOptions['nonce'] && reqOptions['state'])) {
        state = reqOptions['state'];
        nonce = reqOptions['nonce']
      }

      // Replace state and nonce with secure ones
      let crypto = window.crypto || window.msCrypto;
      if (crypto && crypto.getRandomValues) {
        let D = new Uint32Array(2);
        crypto.getRandomValues(D);
        state = reqOptionsExist && reqOptions['state'] ? reqOptions['state']
            : D[0].toString(36);
        nonce = reqOptionsExist && reqOptions['nonce'] ? reqOptions['nonce']
            : D[1].toString(36);
      } else {
        let byteArrayToLong = function (/*byte[]*/byteArray) {
          let value = 0;
          for (let i = byteArray.length - 1; i >= 0; i--) {
            value = (value * 256) + byteArray[i];
          }
          return value;
        };

        let sRandom = new SecureRandom();
        let randState = new Array(4);
        sRandom.nextBytes(randState);
        state = byteArrayToLong(randState).toString(36);

        let randNonce = new Array(4);
        sRandom.nextBytes(randNonce);
        nonce = byteArrayToLong(randNonce).toString(36);
      }

      // Store the them in session storage
      sessionStorage.setItem('state', state);
      sessionStorage.setItem('nonce', nonce);

      let response_type = 'id_token';
      let scope = 'openid';
      let acr_values = null;
      let max_age = null;

      if (reqOptionsExist) {
        if (reqOptions['response_type']) {
          let parts = reqOptions['response_type'].split(' ');
          let temp = [];
          if (parts) {
            for (let i = 0; i < parts.length; i++) {
              if (parts[i] === 'code' || parts[i] === 'token' || parts[i]
                  === 'id_token') {
                temp.push(parts[i]);
              }
            }
          }
          if (temp) {
            response_type = temp.join(' ');
          }
        }

        if (reqOptions['scope']) {
          scope = reqOptions['scope'];
        }
        if (reqOptions['acr_values']) {
          acr_values = reqOptions['acr_values'];
        }
        if (reqOptions['max_age']) {
          max_age = reqOptions['max_age'];
        }
      }

      // Construct the redirect URL for getting an id token, response_type of "token id_token" (note the space), scope of "openid", and some value for nonce is required.
      // client_id must be the consumer key of the connected app. redirect_uri must match the callback URL configured for the connected app.

      let optParams = '';
      if (acr_values) {
        optParams += '&acr_values=' + acr_values;
      }
      if (max_age) {
        optParams += '&max_age=' + max_age;
      }

      let url = activeParameters['authorization_endpoint']
          + '?response_type=' + response_type
          + '&client_id=' + activeParameters['client_id']
          + '&redirect_uri=' + activeParameters['redirect_uri']
          + '&state=' + state
          + '&scope=' + scope
          + '&nonce=' + nonce
          + optParams;

      // if (reqOptions['window']) {
      //   return window.open(url, '_blank', reqOptions['window']);
      // }
      return Browser.open({url: url});
    } catch (e) {
      throw new AuthException(
          'Unable to redirect to the Identity Provider for authenticaton: '
          + e.toString());
    }
  }

Unfortunately, when I do this I get the following error:

To Native ->  Browser open 54105259
ERROR MESSAGE:  {"message":"Must provide a valid URL to open","errorMessage":"Must provide a valid URL to open"}

This has baffled me for the past day or so because the URL is totally fine. It works with window.open and window.location.replace, and it even works somewhat if I just paste it in the browser (it sends me to a ping domain it just says some credentials are not correct, it doesn’t fail to use the url at all though).

The URL is valid and there are no other logs. I cannot find any documentation on this issue nor anyone else with this problem.

Does anyone have any idea of why this might be happening and how to fix it? Any help would be appreciated, since this is absolutely sinking my time.

On Android, the Browser plugin is using Uri.parse (see code).

Uri.parse

String : an RFC 2396-compliant, encoded URI (source)

Are you sure the URL is encoded correctly? I believe just pasting the URL into Chrome, for example, Chrome will auto encode it.

Thanks man @twestrick . It worked once I wrapped the URI in the javascript encodeURI() function. Top tip to anyone reading this: encodeURIComponent encodes too much stuff so only use that one if you have a specific portion of a URI you want to encode in a specific way. encodeURI is the one for whole URIs.

Anyway, thank you so much I would not have worked that out.

1 Like