Conversation
iOS sign-in has never worked. It fails silently, and the trailing slash in the redirect URI is why. Entra normalises a custom-scheme redirect that carries no path, so a request sent as `org.ganesha.elebook://oauthredirect` comes back as `org.ganesha.elebook://oauthredirect/`. AppAuth-iOS compares the callback against the configured redirect component by component, path included, so '' != '/' and shouldHandleURL: rejects it. OIDExternalUserAgentIOS then discards the BOOL from resumeExternalUserAgentFlowWithURL:, so the rejection is never reported and the authorization session waits forever. The person sees a spinner that never stops, with no error and nothing to retry. Android never noticed. Its intent filter matches on the scheme alone (appAuthRedirectScheme in android/app/build.gradle), so the extra slash is irrelevant there. Same config, same Entra registration, opposite outcomes. Captured on an iPhone 15 Pro by routing the flow through the external browser so the callback surfaced in application(_:open:): scheme: org.ganesha.elebook host: oauthredirect path: '/' resumed: REJECTED (url mismatch) full: org.ganesha.elebook://oauthredirect/?code=1.AQkA3Rhcn71... Storing the redirect the way Entra returns it makes the comparison succeed on iOS and changes nothing on Android, which never inspected the path. The pre-fix spelling is accepted and normalised so existing configs self-heal rather than failing validation. One migration consequence: getTokenStorageService digests redirectUrl, so the Keychain/Keystore service name moves and anyone already signed in is signed out once and has to sign in again. Observations, packs and the local database are untouched. iOS loses nothing, never having been able to sign in. The field contributes no isolation in any case -- it is validated to a single constant, so every deployment shares it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collaborator
Author
|
Folded into #42, which now carries this commit unchanged ( Combining them on request: #42 was already the "stop failing silently" PR, and a redirect that makes iOS reject its own OAuth callback without reporting anything is the most extreme case of exactly that. Reviewing the mechanism alongside the timeouts and the background-download fix gives the full picture rather than three partial ones. Nothing is lost — the commit, its tests and the rationale are intact in #42. Closing this in favour of that one. |
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
iOS sign-in has never worked, and one character is why.
Entra normalises a custom-scheme redirect that carries no path. A request sent as
org.ganesha.elebook://oauthredirectcomes back asorg.ganesha.elebook://oauthredirect/. AppAuth-iOS compares the callback against the configured redirect component by component — scheme, user, password, host, port, path — so''!='/',shouldHandleURL:rejects it, andOIDExternalUserAgentIOSthen discards theBOOLreturned byresumeExternalUserAgentFlowWithURL:. The rejection is never reported. The authorization session waits forever, and the person watches a spinner that never stops.Android never noticed. Its intent filter matches on the scheme alone (
appAuthRedirectSchemeinandroid/app/build.gradle), so the extra slash is irrelevant. Identical config, identical Entra registration, opposite outcomes — which is exactly why this survived: every Android tester succeeded while every iOS tester failed, and the failure produced no error to investigate.Captured on an iPhone 15 Pro by routing the flow through the external browser so the callback surfaced in
application(_:open:):Storing the redirect as Entra returns it makes the comparison succeed on iOS and changes nothing on Android, which never inspected the path. The pre-fix spelling is accepted and normalised, so existing configs self-heal instead of failing validation.
Type of Change
Screenshots / Screen Recordings
No UI change.
Checklist
General
Testing
npm test)1034 tests pass plus
tsc --noEmit. The device boxes stay unchecked until the end-to-end sign-in is confirmed on the iPhone that produced the capture above; the root cause is proven from the callback URL and from AppAuth's comparison replicated natively against these exact strings, which is not the same as a green run.Android needs a regression check before merge. The reasoning that it is unaffected is sound — scheme-only intent filter, no path comparison — but it is reasoning, not a test, and this changes a value Android also sends.
React Native Specific
Remainder N/A — configuration and docs only, no components or native modules touched.
Security
Additional Notes
Everyone signed in gets signed out once.
getTokenStorageServicedigests all six config fields includingredirectUrl, so the Keychain/Keystore service name moves and tokens stored under the old name are orphaned. Observations, packs and the local database are untouched — this costs one sign-in, and only on Android, since iOS has never had a session to lose. Worth a line in release notes.The field contributes no isolation in any case: it is validated to a single constant, so every deployment already shares it. Excluding it from the digest would be defensible cleanup, but that moves the hash too, so it would buy a second forced sign-out for no benefit.
Add
org.ganesha.elebook://oauthredirect/to the Entra app registration before rolling this out. Entra matched the slash-less form on the way out and returned the slash-bearing one, so it normalises both, but registering the exact string the app now sends removes the assumption. Keep the existing entry so in-flight builds keep working.Worth fixing upstream
OIDExternalUserAgentIOScallsresumeExternalUserAgentFlowWithURL:error:witherror:niland ignores the return value, so aURLMismatchbecomes an infinite hang rather than an error. Every failure in that session also surfaces asOIDErrorCodeUserCanceledAuthorizationFlow, whichSignInScreensuppresses without an alert. Three layers of silence over one mismatched character — the reason this took a device capture rather than a log to find.