Every SSO guide ends the same way: the provider redirects to your callback URL, the session begins, and the article stops. On the web that ending is the easy part — the callback is just a page you own. In a native mobile app there is no page. The redirect has to land inside the app, delivered by the operating system, and that handoff — browser out, deep link back — is where mobile SSO is actually won or lost.
This post is about that handoff. It is provider-agnostic: everything here applies whether the identity provider is Google, Microsoft Entra ID or GitHub, and largely whether the backend is Directus or anything else. The one Directus-specific wrinkle — and it is a real one — gets its own section near the end.
Why the login must happen in the system browser
The instinct is to open the provider’s login page in a webview inside the app. Resist it, for three reasons that compound.
Providers refuse it. Google has blocked OAuth requests from embedded webviews since 2021 — the flow dies with disallowed_useragent before a password field ever appears. The policy exists because a webview’s host app can read every keystroke, and the providers know it.
The user loses their session. The whole promise of SSO is not typing a password again. A webview has its own empty cookie jar: the user who is signed into Google in Safari is a stranger in your webview. Password managers and passkeys do not autofill there either.
The platforms already solved this. iOS has ASWebAuthenticationSession, Android has Custom Tabs — a real browser sheet with the real cookie jar, presented over your app, that hands control back when the redirect matching your callback fires. RFC 8252, the IETF’s best-practice document for OAuth in native apps, says plainly: use the external browser, never an embedded user-agent. Everything else in this post assumes you do.
Two kinds of deep link, one job
The redirect back needs a URL the operating system routes to your app. There are two mechanisms, and the trade-off between them is not what the marketing pages suggest.
Custom schemes — myapp://auth/callback — are the old way. Registration is a manifest entry; nothing is verified; any app on the device can claim the same scheme. That sounds disqualifying, but hold the thought.
Verified HTTPS links — Universal Links on iOS, App Links on Android — tie https://app.example.com/callback to your app through files you host: apple-app-site-association and assetlinks.json. Ownership is proven, hijacking is closed. They are unambiguously the right tool for content links — a shared article, a password reset.
For the OAuth redirect specifically, though, verified links have a failure mode that matters: they are not reliably triggered by server-side redirect chains. An authorisation response arrives as a 302 from the provider, often through several hops, and both platforms treat programmatic redirects to universal links inconsistently — the link that works when tapped in Messages quietly opens in the browser tab instead when it arrives as a redirect. The auth session APIs sidestep all of this: ASWebAuthenticationSession is told your callback scheme up front and intercepts it itself, deterministically. Which is why, in practice, the OAuth callback is a custom scheme, driven through the auth session API, made safe by PKCE — and the verified links are for everything else.
PKCE, or why the scheme being hijackable stops mattering
The attack you must assume: a malicious app registers your scheme and receives your redirect, authorisation code included. Without PKCE, that code is a bearer instrument — whoever redeems it first gets the tokens.
PKCE (RFC 7636) breaks exactly this. Before opening the browser, the app generates a random verifier, sends its hash (the challenge) with the authorisation request, and must present the original verifier when redeeming the code. The eavesdropping app has the code but not the verifier, and the exchange fails. Combine it with the state parameter — generated per attempt, checked on return — and interception buys an attacker nothing. Every mobile OAuth library supports PKCE; the only mistake available is not turning it on.
Cold starts and dead state
Between opening the browser and receiving the deep link, your app can be killed — low memory, user switches away, Android being Android. The deep link then cold-starts the app, and any flow state held in memory is gone: verifier, state value, the screen the user was on.
So persist the verifier and state to storage keyed to the attempt, restore them in the deep-link handler rather than assuming a warm process, and make the handler idempotent — links can be delivered twice, users do tap twice, and authorisation codes are single-use, so the second redemption must fail gracefully rather than crash into an error screen. Test the killed-app path deliberately; it is the difference between a flow that works in the demo and one that works on a two-year-old Android phone.
The Directus wrinkle: the session lands in the wrong place
Now the part specific to a Directus backend. The SSO entry point is a URL — /auth/login/<provider> — and it accepts a redirect parameter for where to send the user afterwards. Point that at your app’s callback and Directus will refuse unless the destination is allow-listed in AUTH_<PROVIDER>_REDIRECT_ALLOW_LIST. So far, so mobile-friendly.
The catch: when the flow completes, Directus establishes the session as a cookie in the browser that ran the flow. Your app gets the deep link — the signal that login succeeded — but its own HTTP client holds no credentials, because the cookie lives in the system browser’s jar, which iOS and Android deliberately do not share with your app’s requests. The flow was designed with web clients on the same domain in mind, and a native app is neither.
Three patterns close the gap, in ascending order of engineering and correctness. A same-domain webview wrapper — if your “app” is a wrapped website, the cookie simply works, but you have re-entered webview territory with everything that costs. A shared browser session — keep using auth-session sheets for every authenticated hop so the cookie is always present; workable for apps that are mostly web anyway, fragile as an architecture. Or the robust one: a token handoff. A small custom endpoint — a Directus extension — that the authenticated redirect passes through, which mints a short-lived, single-use token bound to that login and appends it to the deep link; the app exchanges it over the API for a session it holds itself, and the one-time token dies. That last mile is not in the box, but it is a well-bounded piece of work, and it is the design we reach for when the app is genuinely native.
A testing checklist
The flows that fail in production are rarely the happy path. Before shipping, walk each of these on real devices:
- Fresh install, first launch, straight into login — scheme registration works with no prior app open.
- App killed mid-flow (swipe away while the browser sheet is up), then complete the login — cold-start restore works.
- The user denies consent at the provider — you show something sane, not a spinner.
- The deep link fires while the app is already foregrounded, and twice in a row — no duplicate sessions, no crash on the spent code.
- On Android, App Links verification is actually passing (
adb shell pm get-app-links) if you use them anywhere — a silent verification failure downgrades every link to the chooser dialog. - On iOS, remember the
apple-app-site-associationfile is fetched through Apple’s CDN and cached — a fix you deployed an hour ago has not necessarily reached the device. - Sign in with a second account — the provider’s account chooser appears when it should, and your app does not weld itself to the first identity it ever saw.
None of this is exotic. It is the same OAuth you already run on the web — with the redirect’s last hop rerouted through the operating system, and the session handed over deliberately instead of by cookie. Get those two joints right and mobile SSO is as boring as it deserves to be.
Common questions
Why can’t I just open the login page in a WebView inside my app?
Because providers block it and users lose everything that makes SSO pleasant. Google has refused sign-ins from embedded webviews since 2021 (the disallowed_useragent error), and inside a webview there is no shared browser session, no password manager and no passkeys — every user types credentials from scratch into a window that could be reading them. The platform auth session APIs exist precisely so the login can happen in the real browser and still return to your app.
Should the OAuth callback use a custom URL scheme or a Universal Link / App Link?
For the OAuth redirect specifically, a custom scheme driven through ASWebAuthenticationSession or Custom Tabs is the pragmatic default: it cannot be swallowed by the browser, it does not depend on domain verification being healthy, and PKCE closes the interception risk that schemes have alone. Verified HTTPS links are the right tool for content deep links — and worth having as well — but they are the fragile choice for a redirect chain.
Does Directus support SSO from native mobile apps?
Yes, with one caveat. The provider flow itself works — you open the Directus SSO login URL in the system browser and allow-list your app’s callback in AUTH_<PROVIDER>_REDIRECT_ALLOW_LIST. The caveat is that Directus establishes the session as a browser cookie, which your app’s HTTP client never sees, so the last mile — turning the browser’s session into credentials your app can hold — is something you design: same-domain wrapper, session share, or a small token-handoff endpoint.
What is PKCE and do I really need it in a mobile app?
PKCE binds the authorisation code to the app instance that started the flow: the app sends a hashed secret (the challenge) when the flow opens and must present the original (the verifier) to redeem the code. Without it, any app that manages to receive your redirect — trivial with an unverified custom scheme — can exchange the stolen code for tokens. RFC 8252 makes it required practice for native apps; treat it as non-negotiable.
Building a mobile app on a Directus backend?
The provider setup is a solved problem — the judgement calls are the handoff: scheme design, PKCE, the token exchange, and what happens on the fifty devices that behave almost like the documentation says. We have built this last mile before. Tell us what you are building and we will tell you what yours needs.
Get in touch →