Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit dce83ee

Browse files
author
thomas
committed
fix(iOS): use ASWebAuthenticationSession instead of WKWebView for login
WKWebView silently fails to complete cross-domain OIDC redirects (e.g. when Nextcloud delegates authentication to an external IdP like Authentik). The user authenticates successfully on the IdP side, but WKWebView drops the callback redirect back to the Nextcloud origin, leaving the login flow stuck in a polling loop that never resolves. ASWebAuthenticationSession uses the system browser which properly handles cross-domain redirects, passkeys, and deep links. Credentials continue to be obtained via the existing polling mechanism. macOS retains the WKWebView sheet since ASWebAuthenticationSession behaves differently on that platform and cross-domain redirects are less problematic there. Ref: nextcloud/ios#3996 (same fix applied to the main iOS app) Made-with: Cursor
1 parent 0b3cbdc commit dce83ee

2 files changed

Lines changed: 73 additions & 3 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-FileCopyrightText: Nextcloud GmbH
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
#if os(iOS)
5+
import AuthenticationServices
6+
import UIKit
7+
8+
///
9+
/// Provides the presentation anchor for `ASWebAuthenticationSession` in SwiftUI contexts.
10+
///
11+
/// `ASWebAuthenticationSession` requires an `ASWebAuthenticationPresentationContextProviding`
12+
/// to determine which window to present the authentication UI in. This coordinator bridges
13+
/// the gap between SwiftUI (which doesn't expose `UIWindow` directly) and the UIKit-based API.
14+
///
15+
@MainActor
16+
class AuthSessionCoordinator: NSObject, ASWebAuthenticationPresentationContextProviding {
17+
nonisolated func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
18+
return MainActor.assumeIsolated {
19+
guard let windowScene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene,
20+
let window = windowScene.windows.first(where: \.isKeyWindow)
21+
else {
22+
return ASPresentationAnchor()
23+
}
24+
return window
25+
}
26+
}
27+
}
28+
#endif

Sources/SwiftNextcloudUI/Views/ServerAddressView.swift

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
import CodeScanner
66
import SwiftUI
77

8+
#if os(iOS)
9+
import AuthenticationServices
10+
#endif
11+
812
///
913
/// A new remote user account should be added locally in the persistence of the app.
1014
///
@@ -111,6 +115,10 @@ public struct ServerAddressView: View, QRCodeParsing, URLSanitizing {
111115
///
112116
@State var pollingToken: String?
113117

118+
#if os(iOS)
119+
@State private var authSessionCoordinator = AuthSessionCoordinator()
120+
#endif
121+
114122
// MARK: - Implementation
115123

116124
public var body: some View {
@@ -216,7 +224,9 @@ public struct ServerAddressView: View, QRCodeParsing, URLSanitizing {
216224
.safeAreaPadding(.all)
217225
}
218226
.ignoresSafeArea()
227+
#if os(macOS)
219228
.webSheet(initialURL: $loginAddress, isPresented: $isPresentingWebView, userAgent: userAgent, onDismiss: endWebView)
229+
#endif
220230
.alert(String(localized: "Login Failed", comment: "Alert title"), isPresented: $isPresentingAlert) {
221231
Button(role: .cancel) {
222232
errorMessage = nil
@@ -290,16 +300,48 @@ public struct ServerAddressView: View, QRCodeParsing, URLSanitizing {
290300

291301
func beginWebView(_ url: URL) {
292302
self.loginAddress = url
303+
304+
#if os(iOS)
305+
startAuthSession(url: url)
306+
#else
293307
isPresentingWebView = true
308+
#endif
294309
}
295310

311+
#if os(iOS)
312+
///
313+
/// Start an `ASWebAuthenticationSession` to present the login page.
314+
///
315+
/// This replaces the `WKWebView` sheet on iOS. `ASWebAuthenticationSession` uses the system
316+
/// browser which properly handles cross-domain redirects (e.g. OIDC providers), passkeys, and
317+
/// deep links — all of which fail silently in `WKWebView`.
318+
///
319+
/// Credentials are still obtained via polling by the host app. The callback URL scheme (`nc://`)
320+
/// is used by Nextcloud's Login Flow v2 to signal completion; receiving it cancels polling early.
321+
///
322+
private func startAuthSession(url: URL) {
323+
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: "nc") { _, error in
324+
if let error = error as? ASWebAuthenticationSessionError, error.code == .canceledLogin {
325+
endWebView()
326+
return
327+
}
328+
// Login completed (callback received or session ended).
329+
// Credentials are picked up via the host app's polling mechanism.
330+
}
331+
332+
session.presentationContextProvider = authSessionCoordinator
333+
session.prefersEphemeralWebBrowserSession = true
334+
session.start()
335+
}
336+
#endif
337+
296338
///
297-
/// Dismisses the sheet with the web view.
339+
/// Dismisses the login UI.
298340
///
299341
/// Multiple paths can lead to here. In example:
300342
///
301-
/// - The user dismisses the sheet without logging in.
302-
/// - The login completed successfully.
343+
/// - The user dismisses the session without logging in.
344+
/// - The login completed successfully (detected by polling).
303345
///
304346
func endWebView() {
305347
if let pollingToken {

0 commit comments

Comments
 (0)