-
Notifications
You must be signed in to change notification settings - Fork 8
Improve compatibility with hybrid mobile apps #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
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.
nicknisi
left a comment
There was a problem hiding this 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.
src/create-client.ts
Outdated
| removeSessionData({ devMode: this.#devMode }); | ||
| window.location.assign(url); | ||
| if (options?.noRedirect) { | ||
| fetch(url); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>;There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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()andgetSignUpUrl()methods, this should fully address the mobile WebView challenges you're facing. What do you think?
Yes it does, thanks!
nicknisi
left a comment
There was a problem hiding this 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
left a comment
There was a problem hiding this 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!
|
Thank you for the rapid feedback and great collaboration on this PR, it is much appreciated 🙏 |
TL;DR
Problem 1:
signIn(),signUp(), and defaultsignOut()broke in WebViews by opening URLs in the external browser.Solution 1: Added
getSignInUrl(),getSignUpUrl()to get URLs without auto-redirect and anoRedirectoption forsignOut().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):
The use of window.location.assign() breaks the mobile app authentication flow
-> Added new methods to obtain auth URLs without automatic navigation
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()andsignOut()methods breaks the authentication flow. This is due to the default behavior ofwindow.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
signInorsignUpURLs in an in-app browser, so the user does not leave the app during the authentication flow. Also, I need to be able to callsignOut()without leaving the app.Changes:
Added two new public methods to get authentication URLs without automatic navigation:
getSignInUrl()- Returns the sign-in URL without navigationgetSignUpUrl()- Returns the sign-up URL without navigationAdded a
noRedirectoption tosignOut()method that makes a call to the logout URL without navigating away from the app :Refactored the original
signIn()andsignUp()methods to use a common internal method#getAuthorizationUrl()to generate the authentication URLsThese changes allow mobile applications to:
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://localhoston iOS orhttp://localhoston 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:
noRedirectoption in the sign-out flowWhile 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.