OAuth2 auth code flow w/ Capacitor Browser

#1

I want to use the Capacitor browser for an OAuth2 auth code flow. In the Cordova inappbrowser world, you could capture the URL and close the browser, once the redirect is complete. However, I’m running into several issues:

  • The listeners are returning empty data objects
  • Close is only supported in iOS

Is this even possible in the Capacitor world?

  await Browser.open({url: 'https://xxxxx'});

  Browser.addListener('browserPageLoaded', (data) => {
    console.log('Data - browserPageLoaded: '+JSON.stringify(data));  ---> returning {}
  });

  Browser.addListener('browserFinished', (data) => {
    console.log('Data - browserFinished: '+JSON.stringify(data)); ---> returning {}
  });
0 Likes

OpenID Connect via Capacitor for iOS
#2

I’m seeing the same thing. I have to upgrade an app from Ionic 3 to Ionic 4 and would like to use Capacitor. The inAppBrowser has worked well, and I’ve enjoyed using Capacitor (without plugins) so far.
However, this would be a blocker for using Capacitor for the upgraded production app.

I will be watching this thread closely and trying to find a solution to this myself also.

0 Likes

#3


Saw this on the Capacitor slack channel.

0 Likes

#4

@yulemata provided a solution on the slack channel which I haven’t tried out yet due to other work. Posting it here as it might be helpful to others.

I did a preliminary test with ios and I’ve got the redirection working with Browser and App plugins. The App plugin is the one that actually handles the redirection. For that you will need to:

  1. Open your auth url with the Browser plugin await Browser.open({url: <your url>});

  2. Set up a listener with the App plugin https://capacitor.ionicframework.com/docs/apis/app as follows

capacitor.ionicframework.com

App - Capacitor

The Native Bridge for Cross-Platform Web Apps. Invoke Native SDKs on iOS, Android, Electron, and the Web with one code base. Optimized for Ionic Framework apps, or use with any web app framework.

private async setupRedirectListener(){

 App.addListener('appUrlOpen', async (data) =&gt; {

   // could be explicit with the redirectUri since we know we're native

   if (data.url.indexOf(AUTH_CONFIG.redirectUri) !== -1) {

       await Browser.close();

       await this.handleRedirectUrl(data.url);

   }

});

so, in the listener you first close the Browser (since the redirection callback has been provided) and then you handle the redirection according to your application requirements (the handleRedirectUrl() method)

I kept the capacitor default URLScheme which is capacitor://. In that case my callback url was capacitor://callback

but you will need to change that accordingly with your app bundle id in the Info.plist

0 Likes

#5

The docs suggest that Browser.close() only works on iOS. Additionally, I could not get the appUrlOpen listener to trigger correctly on Android. It appears the latter is being tracked as an issue here.

0 Likes

#6

Below code works fine on iOS, but the listener does not fire on Android and Browser.close() does not work. The logs clearly show the listener getting registered, but the event is never fired.

async linkAccount() {

   App.addListener('appUrlOpen', (data) => {
   console.log('Data: '+JSON.stringify(data));
  })

  this.addRedirectListener();

  await Browser.open({url: 'https://domain/oauth/authenticate/?client_id=xxxx&response_type=token&redirect_url=capacitor://localhost/callback'});
}

private async addRedirectListener() {
  App.addListener('appUrlOpen', async (data: any) => {
    console.log('appUrlOpen: '+data.url);
    console.debug('AppComponent - constructor - appUrlOpen');
    if(data.url.indexOf('callback#')!=-1) {
      let regEx = /(callback#access_token=)(.*)/g;
      let code = regEx.exec(data.url)[2];
      console.log(code);
      this.router.navigate(['/tabs/profile/link/'+code]);
    }
    Browser.close();
  });
}

Logs:

W/zygote: Attempt to remove non-JNI local reference, dumping thread
V/Capacitor/Plugin: To native (Capacitor plugin): callbackId: 24424146, pluginId: App, methodName: addListener
V/Capacitor: callback: 24424146, pluginId: App, methodName: addListener, methodData: {“eventName”:“appUrlOpen”}
V/Capacitor/Plugin: To native (Capacitor plugin): callbackId: 24424147, pluginId: Browser, methodName: open
V/Capacitor: callback: 24424147, pluginId: Browser, methodName: open, methodData: {“url”:“xxxxxxx”}
W/cr_Ime: updateState: type [0->0], flags [0], show [false],
D/Capacitor: App paused
W/zygote: Attempt to remove non-JNI local reference, dumping thread
W/zygote: Attempt to remove non-JNI local reference, dumping thread
W/zygote: Attempt to remove non-JNI local reference, dumping thread
I/zygote: NativeAllocBackground concurrent copying GC freed 10(48KB) AllocSpace objects, 0(0B) LOS objects, 57% free, 1122KB/2MB, paused 659us total 188.309ms
W/zygote: Attempt to remove non-JNI local reference, dumping thread
I/chatty: uid=10083(u0_a83) events.beerfest.app identical 1 line
W/zygote: Attempt to remove non-JNI local reference, dumping thread
I/zygote: NativeAllocBackground concurrent copying GC freed 10(47KB) AllocSpace objects, 0(0B) LOS objects, 57% free, 1123KB/2MB, paused 5.489ms total 34.557ms
W/zygote: Attempt to remove non-JNI local reference, dumping thread
W/zygote: Attempt to remove non-JNI local reference, dumping thread
I/zygote: NativeAllocBackground concurrent copying GC freed 15(47KB) AllocSpace objects, 0(0B) LOS objects, 57% free, 1124KB/2MB, paused 29.306ms total 85.913ms
D/EGL_emulation: eglMakeCurrent: 0x9fd061a0: ver 3 0 (tinfo 0x8df3f2e0)
D/Capacitor: Saving instance state!
D/Capacitor/Plugin/App: Firing change: false
V/Capacitor/Plugin/App: Notifying listeners for event appStateChange
D/Capacitor: App stopped
V/Capacitor: callback: -1, pluginId: Console, methodName: log, methodData: {“level”:“log”,“message”:“App state changed {“isActive”:false}”}
I/Capacitor/Plugin/Console: App state changed {“isActive”:false}

------ Browser close ------

D/Capacitor: App restarted
D/Capacitor: App started
D/Capacitor/Plugin/App: Firing change: true
V/Capacitor/Plugin/App: Notifying listeners for event appStateChange
D/Capacitor: App resumed
V/Capacitor: callback: -1, pluginId: Console, methodName: log, methodData: {“level”:“log”,“message”:“App state changed {“isActive”:true}”}
I/Capacitor/Plugin/Console: App state changed {“isActive”:true}
V/Capacitor/Plugin/Network: Notifying listeners for event networkStatusChange
D/Capacitor/Plugin/Network: No listeners found for event networkStatusChange
W/zygote: Attempt to remove non-JNI local reference, dumping thread
D/EGL_emulation: eglMakeCurrent: 0x9fd061a0: ver 3 0 (tinfo 0x8df3f2e0)
W/zygote: Attempt to remove non-JNI local reference, dumping thread
W/zygote: Attempt to remove non-JNI local reference, dumping thread
I/zygote: NativeAllocBackground concurrent copying GC freed 10(32KB) AllocSpace objects, 0(0B) LOS objects, 57% free, 1114KB/2MB, paused 8.106ms total 58.317ms
W/zygote: Attempt to remove non-JNI local reference, dumping thread
W/zygote: Attempt to remove non-JNI local reference, dumping thread
I/zygote: NativeAllocBackground concurrent copying GC freed 11(31KB) AllocSpace objects, 0(0B) LOS objects, 57% free, 1130KB/2MB, paused 953us total 112.234ms
W/zygote: Attempt to remove non-JNI local reference, dumping thread
I/zygote: NativeAllocBackground concurrent copying GC freed 9(31KB) AllocSpace objects, 0(0B) LOS objects, 57% free, 1114KB/2MB, paused 7.752ms total 29.100ms
W/zygote: Attempt to remove non-JNI local reference, dumping thread

0 Likes

#7

Hi angulat0r and timofeysie, thanks for sharing your findings on this. I wondered if either of you has had any luck with oAuth code flow with Android on capacitor.

I’m trying to get this working with Dropbox and Google oAuth for a file sharing application. My next step is to try using the capacitor-oauth2 plugin (https://github.com/moberwasserlechner/capacitor-oauth2) assuming that Android is not working with App.addListener(‘appUrlOpen’…) yet. If I get it working I’ll let you know.

0 Likes

#8

I have not. Honestly, I’ve given up on Capacitor for the time being.

0 Likes

#9

In case this helps anyone, I tried a few approaches but in the end the best option for me was to use the Browser plugin to open the link to authenticate through oAuth, and to have the redirect page set up to direct the user to a custom URI scheme (e.g. com.mydomain.app://redirect).

By firing the Custom URI on the redirect page, the app came back up and I didn’t need to worry about doing a browser.close() on Android.

The App plugin was enough to let me parse the URL upon App startup (or when resuming). e.g.

  App.addListener('appUrlOpen', (data: AppUrlOpen) => {
  let url = data.url;
  // process URL as needed 
}

The redirect page was hosted on a server in the middle in my case, but it had a redirect to a custom URI such as:

   var target = "com.mydomain.app://" + (location.pathname+location.search).substr(1);
   window.location.replace(target);

I had previously tried a hack-ish webview type approach to OAuth but it was a brutal user experience (the user had to type in their username/password every time, plus MFA), and goes against best practices from a security standpoint. Plus the only way it would work with Google was to change the user-agent, as they do not allow web view based oAuth. (https://auth0.com/blog/google-blocks-oauth-requests-from-embedded-browsers/).

Under the hood, the Browser plugin uses SFSafariViewController (iOS) and Chrome Custom Tabs (Android) so the user can often be re-authenticated from an existing session without having to log in again.

On a side note it’s been a few months with Capacitor now and I’m really liking it compared to Cordova… there’s been a learning curve but it seems to give you more control and should be easier for ongoing maintenance.

0 Likes

#10

I’m trying to get https://github.com/wi3land/ionic-appauth-capacitor-demo working with OAuth 2.0 code flow and Okta. It works in a browser, but not with Capacitor. On iOS, it fails on startup:

⚡️  Loading app at capacitor://localhost...
Reachable via WiFi
APP ACTIVE
⚡️  [log] - onscript loading complete
⚡️  [log] - Angular is running in the development mode. Call enableProdMode() to enable the production mode.
⚡️  [log] - Ionic Native: deviceready event fired after 187 ms
⚡️  To Native ->  App addListener 13145406
⚡️  To Native ->  SplashScreen hide 13145407
⚡️  TO JS {}
To Native Cordova ->  SecureStorage init SecureStorage1273972186 ["options": [SecretStore]]
To Native Cordova ->  SecureStorage get SecureStorage1273972187 ["options": [SecretStore, token_response]]
2019-04-18 13:45:54.834526-0600 App[21982:220634] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSBundle initWithURL:]: nil URL argument'
*** First throw call stack:
(
	0   CoreFoundation                      0x000000010e4996fb __exceptionPreprocess + 331
	1   libobjc.A.dylib                     0x000000010bbf8ac5 objc_exception_throw + 48
	2   CoreFoundation                      0x000000010e499555 +[NSException raise:format:] + 197
	3   Foundation                          0x000000010b627475 -[NSBundle initWithURL:] + 87
	4   Foundation                          0x000000010b627524 +[NSBundle bundleWithURL:] + 45
	5   CordovaPlugins                      0x000000010a9cb527 __34+[SAMKeychainQuery errorWithCode:]_block_invoke + 183
	6   libdispatch.dylib                   0x0000000113049db5 _dispatch_client_callout + 8
	7   libdispatch.dylib                   0x000000011304b83d _dispatch_once_callout + 66
	8   CordovaPlugins                      0x000000010a9cad6c +[SAMKeychainQuery errorWithCode:] + 156
	9   CordovaPlugins                      0x000000010a9ca588 -[SAMKeychainQuery fetch:] + 568
	10  CordovaPlugins                      0x000000010a9cc139 __21-[SecureStorage get:]_block_invoke + 169
	11  libdispatch.dylib                   0x0000000113048d7f _dispatch_call_block_and_release + 12
	12  libdispatch.dylib                   0x0000000113049db5 _dispatch_client_callout + 8
	13  libdispatch.dylib                   0x000000011304c7b9 _dispatch_queue_override_invoke + 1022
	14  libdispatch.dylib                   0x000000011305a632 _dispatch_root_queue_drain + 351
	15  libdispatch.dylib                   0x000000011305afca _dispatch_worker_thread2 + 130
	16  libsystem_pthread.dylib             0x00000001134326b3 _pthread_wqthread + 583
	17  libsystem_pthread.dylib             0x00000001134323fd start_wqthread + 13
)
⚡️  To Native ->  App addListener 13145408
libc++abi.dylib: terminating with uncaught exception of type NSException

I tried your suggestion @Tommclellan , but it doesn’t seem to help. On Android, it redirects back to the app OK, but it doesn’t seem to execute the callback. You can find my fork and changes in https://github.com/mraible/ionic-appauth-capacitor-demo/pull/1.

0 Likes

#11

I haven’t worked with ionic-appauth-capacitor-demo… sounds like it may be missing a URL parameter or something.

I had looked at another plugin https://github.com/moberwasserlechner/capacitor-oauth2 that uses AppAuth for Android under the hood and OAuthSwift for iOS, but for my needs it worked okay just to use the standard capacitor Browser plugin.

I configured my Android and iOS apps to do custom URI’s just through Xcode and Android Manifest using standard tutorials.

This led to having an intent-filter with custom_url_scheme specified in the Android Manifest, where @string/custom_url_scheme was defined in android/src/main/res/values/strings.xml

    <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="@string/custom_url_scheme" />
        </intent-filter>

One thing to watch out for with Android’s handling of custom URL schemes is to set the launchMode to singleTask so that the URL does not trigger a new instance of your app.

  <activity
        android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale"
        android:name="com.mydomain.app.MainActivity"
        android:label="@string/title_activity_main"
        android:launchMode="singleTask"
        android:theme="@style/AppTheme.NoActionBarLaunch">

Some background on this here: https://github.com/ionic-team/capacitor/issues/971

0 Likes

#12

Adding android:launchMode="singleTask" was the key. Thanks @Tommclellan!

1 Like