Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@vdsbenoit
Copy link
Contributor

TL;DR

  • Problem 1: signIn(), signUp(), and default signOut() broke in WebViews by opening URLs in the external browser.
    Solution 1: Added getSignInUrl(), getSignUpUrl() to get URLs without auto-redirect and a noRedirect option for signOut().

  • Problem 2: isRedirectCallback() failed in WebViews due to different URL schemes.
    Solution 2: Now isRedirectCallback() only compares the URL path, ignoring the scheme.

  • Impact: authkit-js is now compatible with hybrid mobile apps without breaking navigation. Tests added and an existing test fixed.

Overview

This pull request introduces two key fixes to enable authkit-js to work properly in mobile app WebViews (Android and iOS):

  1. The use of window.location.assign() breaks the mobile app authentication flow

    -> Added new methods to obtain auth URLs without automatic navigation

  2. isRedirectCallback does not work with WebViews URL schemes

    -> Modified the URL comparison to focus on pathname instead of the full URL

Issue 1: window.location.assign()

When using authkit-js in a WebView within an Android or iOS application, using the signIn(), signUp() and signOut() methods breaks the authentication flow. This is due to the default behavior of window.location.assign() in a WebView, which opens the URL in the default web browsing app. As a consequence the user leaves the hybrid app where authkit-js is used.

As a mobile app developer, I need to be able to open the signIn or signUp URLs in an in-app browser, so the user does not leave the app during the authentication flow. Also, I need to be able to call signOut() without leaving the app.

Changes:

  1. Added two new public methods to get authentication URLs without automatic navigation:

    • getSignInUrl() - Returns the sign-in URL without navigation
    • getSignUpUrl() - Returns the sign-up URL without navigation
  2. Added a noRedirect option to signOut() method that makes a call to the logout URL without navigating away from the app :

    signOut({ noRedirect: true });
  3. Refactored the original signIn() and signUp() methods to use a common internal method #getAuthorizationUrl() to generate the authentication URLs

These changes allow mobile applications to:

  • Obtain authentication URLs and handle them within the WebView without breaking the app's navigation flow
  • Handle logout operations without uncontrolled redirects

Issue 2: isRedirectCallback logic

The original implementation of isRedirectCallback() compared the full current URL against the redirect URI. This doesn't work properly in a mobile apps WebView, where the URL scheme might be different (e.g., capacitor://localhost on iOS or http://localhost on Android). See Capacitor documentation about these URL schemes.

Changes:

  • Modified isRedirectCallback() to only compare URL pathnames rather than the full URL.

  • Added tests to verify the new pathname-based comparison works correctly.

This change allows the library to correctly identify redirect callbacks regardless of the URL scheme (http, https, capacitor, etc.), making it compatible with hybrid mobile apps running in WebViews.

Testing

The changes have been thoroughly tested with:

  • Unit tests for the new methods and modified logic
  • Tests for the noRedirect option in the sign-out flow

While implementing the tests, I found out one of the isRedirectCallback() tests was not testing its intended functionality. This PR also addresses this issue.

Impact

These changes maintain backward compatibility while enabling authkit-js to be used in hybrid mobile applications.

vdsbenoit added 8 commits May 19, 2025 10:45
The second test was exactly the same as the first one.
In this commit, we add a trailing slash to the current URI to reflect
the test description.
Comparing the whole URL does not work when the library runs on a mobile
app (e.g. with Capacitor). For instance, the host on a mobile app
WebView is localhost.
Copy link
Member

@nicknisi nicknisi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! The changes look good, overall. I just have one suggestion.

removeSessionData({ devMode: this.#devMode });
window.location.assign(url);
if (options?.noRedirect) {
fetch(url);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I'm wondering if this Promise should be returned so it could be properly error-handled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do this yes! I just did not want to change the signature of the signOut() method. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm of the opinion we should, probably changing the method signature to:

async signOut(options?: { returnTo?: string; noRedirect?: boolean }): Promise<void | Response>;

Copy link
Contributor Author

@vdsbenoit vdsbenoit May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can do that. Let's highlight that the method becomes asynchronous then. It might trigger linters in the consumer projects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to avoid all side effects on other users of the library, another option would be to create a separate method instead :

  async signOutWithoutRedirection() {
    const accessToken = memoryStorage.getItem(storageKeys.accessToken);
    if (typeof accessToken !== "string") return;
    const { sid: sessionId } = getClaims(accessToken);

    const url = this.#httpClient.getLogoutUrl({
      sessionId,
      returnTo: undefined
    });

    if (url) {
      removeSessionData({ devMode: this.#devMode });
      return fetch(url);
    }
  }

What is cleaner to you ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, thinking about this further, changing the method signature could be a considered a breaking change. I like your idea better of going with a separate method. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All right, I'll do it this way then!

Copy link
Contributor Author

@vdsbenoit vdsbenoit May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, the fetch(url) call throws an 302 error on mobile:

Cross-origin redirection to https://auth.our.company.com/api/logout?logout_context=<REMOVED_KEY> denied by Cross-Origin Resource Sharing policy: Origin capacitor://localhost is not allowed by Access-Control-Allow-Origin. Status code: 302

It doesn't really block me because I just ignore it. But you might want to do something about it ?

Copy link
Member

@nicknisi nicknisi May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: Sorry for the back-and-forth as we think through this!

@vdsbenoit After discussing with the AuthKit team, I'd like to propose a simple approach that keeps most of your implementation while addressing the Promise return concern.

Let's enhance the existing signOut method with a conditional return type:

  signOut(options?: { returnTo?: string; navigate?: true }): void;
  signOut(options?: { returnTo?: string; navigate: false }): Promise<void>;
  signOut(
    options: { returnTo?: string; navigate?: boolean } = { navigate: true },
  ): void | Promise<void> {
    const navigate = options.navigate ?? true;
    const accessToken = memoryStorage.getItem(storageKeys.accessToken);
    if (typeof accessToken !== "string") return;
    const { sid: sessionId } = getClaims(accessToken);

    const url = this.#httpClient.getLogoutUrl({
      sessionId,
      returnTo: options?.returnTo,
    });

    if (url) {
      removeSessionData({ devMode: this.#devMode });

      if (!navigate) {
        return new Promise(async (resolve) => {
          fetch(url, {
            mode: "no-cors",
            credentials: "include",
          })
            .catch((error) => {
              console.warn("AuthKit: Failed to send logout request", error);
            })
            .finally(resolve);
        });
      } else {
        window.location.assign(url);
      }
    }
  }

This keeps the existing behavior for current users while allowing mobile apps to avoid redirections. Since this is backward compatible, we can ship it as a minor update.

Additionally, adding the { mode: 'no-cors', credentials: 'include' } options to the fetch call should allow it to follow the redirect with the proper credentials, and make the response opaque, so you shouldn't see the CORs error.

Combined with your getSignInUrl() and getSignUpUrl() methods, this should fully address the mobile WebView challenges you're facing. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, thank you! 🙏

I integrated this comment in my latest commit. I just inverted the if condition in order to prevent and unnecessary ! not operator.

I confirm that I do not longer have any cors error.

Combined with your getSignInUrl() and getSignUpUrl() methods, this should fully address the mobile WebView challenges you're facing. What do you think?

Yes it does, thanks!

Copy link
Member

@nicknisi nicknisi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed up a small fix to the tests to account for the option name change. Looks great, thank you!

Copy link
Member

@nicknisi nicknisi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed up a small fix to the tests to account for the option name change. Looks great, thank you!

@nicknisi nicknisi merged commit 74cc6f5 into workos:main May 21, 2025
2 checks passed
@nicknisi nicknisi mentioned this pull request May 21, 2025
@vdsbenoit
Copy link
Contributor Author

Thank you for the rapid feedback and great collaboration on this PR, it is much appreciated 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants