Description
When a native OIDC client (e.g. Element X on iOS) uses an https:// redirect_uri such as https://element.io/oauth/ios/io.element.elementx, the _complete endpoint returns a 307 redirect directly to that URL. On iOS, the browser does not trigger the app open via HTTP 307 redirect — the user sees a blank page or is redirected to element.io which does not handle the path. The login silently fails and the user is stuck.
Root cause
In src/api/oidc/complete.rs, needs_interstitial() only returns true when redirect_url.scheme().contains(".") (i.e. private-use reverse-DNS schemes like io.element.android). For https:// redirect URIs — which Element X iOS uses (https://element.io/oauth/ios/io.element.elementx) — it returns false, so the server sends a bare 307 redirect.
However, iOS Safari/Chrome do not trigger universal link / app deep link opening via HTTP 3xx redirects. They require either:
- A user-initiated navigation (click on a link), or
- A page load that calls
window.location via JavaScript
A 307/302 redirect is followed silently by the browser networking stack and does not trigger the app open. This means Element X on iOS can never complete the OIDC login flow.
Reproduction
- Deploy Tuwunel 1.8.1 (main branch, commit
92bef7a) with oidc_native_auth = true
- Configure
server_name, well_known.client, well_known.server correctly
- Use Element X on iOS to connect → OIDC flow starts
- Browser opens, login page appears, enter credentials
POST /_tuwunel/oidc/native → 303 → GET /_tuwunel/oidc/_complete
_complete returns 307 → https://element.io/oauth/ios/io.element.elementx?code=...&state=...
- Browser follows 307 to
element.io — app does not open
- User sees blank page or element.io 404 page
- If user goes back and submits login again → 404
Unknown or expired authorization request (oidc_req_id already consumed)
Request/response trace
POST /_tuwunel/oidc/native → 303
location: /_tuwunel/oidc/_complete?oidc_req_id=...&loginToken=...
GET /_tuwunel/oidc/_complete → 307
location: https://element.io/oauth/ios/io.element.elementx?code=...&state=...
(no body — direct redirect, no interstitial page)
Element X on iOS registers https://element.io/oauth/ios/io.element.elementx as a universal link. iOS should intercept this URL and open the app. But iOS does not intercept URLs reached via HTTP 3xx redirects — only direct user navigation.
Suggested fix
Option A (broad): Show the interstitial "Continue" page for all non-http://localhost redirect URIs, not just those with . in the scheme. A native app redirect URI — whether io.element.android.x:/callback or https://element.io/oauth/ios/io.element.elementx — always needs a user gesture (click) on iOS.
Option B (narrower): Show the interstitial when the redirect_uri host matches a known app-link pattern (e.g. path contains /oauth/ or /callback), or when the request includes prompt=consent.
Option C: Always show the interstitial for application_type: "native" clients regardless of redirect_uri scheme.
Environment
- Tuwunel: 1.8.1 (main, commit
92bef7a)
- Client: Element X iOS (latest)
- redirect_uri:
https://element.io/oauth/ios/io.element.elementx
- Platform: iOS (Safari and in-app browser)
- Server behind Traefik reverse proxy with TLS
Related
Description
When a native OIDC client (e.g. Element X on iOS) uses an
https://redirect_uri such ashttps://element.io/oauth/ios/io.element.elementx, the_completeendpoint returns a 307 redirect directly to that URL. On iOS, the browser does not trigger the app open via HTTP 307 redirect — the user sees a blank page or is redirected toelement.iowhich does not handle the path. The login silently fails and the user is stuck.Root cause
In
src/api/oidc/complete.rs,needs_interstitial()only returnstruewhenredirect_url.scheme().contains(".")(i.e. private-use reverse-DNS schemes likeio.element.android). Forhttps://redirect URIs — which Element X iOS uses (https://element.io/oauth/ios/io.element.elementx) — it returnsfalse, so the server sends a bare 307 redirect.However, iOS Safari/Chrome do not trigger universal link / app deep link opening via HTTP 3xx redirects. They require either:
window.locationvia JavaScriptA 307/302 redirect is followed silently by the browser networking stack and does not trigger the app open. This means Element X on iOS can never complete the OIDC login flow.
Reproduction
92bef7a) withoidc_native_auth = trueserver_name,well_known.client,well_known.servercorrectlyPOST /_tuwunel/oidc/native→ 303 →GET /_tuwunel/oidc/_complete_completereturns 307 →https://element.io/oauth/ios/io.element.elementx?code=...&state=...element.io— app does not openUnknown or expired authorization request(oidc_req_id already consumed)Request/response trace
Element X on iOS registers
https://element.io/oauth/ios/io.element.elementxas a universal link. iOS should intercept this URL and open the app. But iOS does not intercept URLs reached via HTTP 3xx redirects — only direct user navigation.Suggested fix
Option A (broad): Show the interstitial "Continue" page for all non-
http://localhostredirect URIs, not just those with.in the scheme. A native app redirect URI — whetherio.element.android.x:/callbackorhttps://element.io/oauth/ios/io.element.elementx— always needs a user gesture (click) on iOS.Option B (narrower): Show the interstitial when the redirect_uri host matches a known app-link pattern (e.g. path contains
/oauth/or/callback), or when the request includesprompt=consent.Option C: Always show the interstitial for
application_type: "native"clients regardless of redirect_uri scheme.Environment
92bef7a)https://element.io/oauth/ios/io.element.elementxRelated
_complete(this is the next issue in the same flow)M_UNRECOGNIZEDhandling when OIDC is not configured