Issue #1012
When you need to authenticate users through an external service in your iOS app, ASWebAuthenticationSession handles the flow gracefully. The API presents Safari’s authentication view inside your app, then captures the redirect back. But the callback mechanism you choose determines what setup you need and whether your code will work consistently across devices.
Starting with iOS 17.4, Apple introduced a new callback API that uses HTTPS URLs instead of custom URL schemes. This gives you two ways to capture the authentication redirect.
The original approach uses a custom URL scheme. You initialize the session with your app’s scheme, and when the authentication server redirects to something like myapp://callback?token=xyz, iOS intercepts it and passes the URL to your completion handler.
This method has been available since ASWebAuthenticationSession was introduced. It works on all iOS versions that support the API.
The newer approach uses HTTPS callbacks. Instead of a custom scheme, you specify a host and path that match your app’s associated domains.
When the server redirects to https://m.example.com/callback?token=xyz, iOS validates that your app owns this domain. After validation passes, it delivers the URL to your handler.
Custom Scheme
Custom schemes require minimal setup. You declare your scheme in Info.plist under URL Types, then pass it to the session initializer. No server configuration needed, no entitlements beyond the basics.
let session = ASWebAuthenticationSession(
url: authURL,
callbackURLScheme: "myapp"
) { url, error in
// Handle the callback
}
The limitation emerges when you consider what the authentication server must support. If the server only redirects to HTTPS URLs, custom schemes won’t work. You can’t force an external service to redirect to your custom scheme if they’re designed for standard web URLs.
This constraint matters more than you might expect. Many enterprise authentication systems exclusively use HTTPS redirects for security and compatibility reasons.
Custom schemes also show a permission prompt on first use. iOS asks users to confirm that they want the authentication session to hand control back to your app. While not a dealbreaker, it adds friction to what should be a seamless flow.
HTTPS Callbacks
HTTPS callbacks feel more native because they use standard web URLs. The authentication server redirects to a URL that looks like any other web address, and iOS handles the interception automatically.
if #available(iOS 17.4, *) {
let session = ASWebAuthenticationSession(
url: authURL,
callback: .https(host: "m.example.com", path: "/callback")
) { url, error in
// Handle the callback
}
}
This approach requires more infrastructure. Your app needs associated domains configured with the webcredentials service type. In your entitlements file, you declare the domains that can redirect to your app.
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:m.example.com</string>
</array>
The server at that domain must host an apple-app-site-association file at /.well-known/apple-app-site-association. This JSON file lists your app’s bundle ID under the webcredentials key.
{
"webcredentials": {
"apps": ["TEAMID.com.example.myapp"]
}
}
Without both pieces, iOS refuses to start the authentication session. On simulator, validation is lenient and often works anyway.
On device, validation is strict. If anything is misconfigured, it fails silently. This inconsistency makes debugging frustrating when code works perfectly in the simulator but does nothing on a physical device.
Why Validation Matters
iOS validates associated domains before starting the session, not after. When you call session.start() with an HTTPS callback, the system checks that your entitlements list the domain and that the server has the association file.
If validation fails, start() returns false immediately. No error, no alert, no session. The authentication view never appears.
This upfront validation protects users from malicious apps claiming to handle callbacks for domains they don’t control. But it means your configuration must be perfect before anything works.
A typo in the entitlements, a missing association file, or domains that haven’t propagated through Apple’s CDN will all produce the same silent failure.
The validation also requires internet connectivity. iOS doesn’t cache these checks indefinitely. If a user’s device can’t reach Apple’s CDN, HTTPS callbacks won’t work even with correct configuration.
Custom schemes have no such dependency since they require no external validation.
Query Parameters and Development
Associated domains support a ?mode=developer suffix that bypasses Apple’s CDN and connects directly to your server. This seems useful for local development or internal testing environments.
<string>webcredentials:staging.example.com?mode=developer</string>
However, ASWebAuthenticationSession with HTTPS callbacks doesn’t recognize query parameters in associated domain entries. The validation logic expects plain domains.
You need both the plain domain for the session to work and optionally the developer mode entry for other features like Password AutoFill.
<string>webcredentials:staging.example.com</string>
<string>webcredentials:staging.example.com?mode=developer</string>
Keeping both entries ensures the authentication session validates correctly while maintaining developer mode benefits for other features.
Building Your Own Authentication Session
When ASWebAuthenticationSession refuses to start on physical devices despite correct configuration, you need an alternative. A custom view controller with WKWebView can replace the built-in authentication session entirely.
The approach is straightforward. Present a view controller that loads the authentication URL in a web view. Watch for navigation events, and when the URL matches your expected callback, intercept it and dismiss the view controller.
final class AuthenticationSessionViewController: UIViewController {
enum Result {
case cancelled
case callback(URL)
}
struct RedirectMatch {
let host: String
let path: String
}
private let url: URL
private let redirectMatch: RedirectMatch
private let completion: (Result) -> Void
private let webView: WKWebView
init(
url: URL,
redirectMatch: RedirectMatch,
completion: @escaping (Result) -> Void
) {
self.url = url
self.redirectMatch = redirectMatch
self.completion = completion
let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = .nonPersistent()
self.webView = WKWebView(frame: .zero, configuration: configuration)
super.init(nibName: nil, bundle: nil)
webView.navigationDelegate = self
}
}
The key is using a non-persistent data store. This ensures cookies and website data don’t persist between authentication sessions. Each time the view controller appears, the user starts with a clean slate.
The navigation delegate intercepts every navigation request. When a URL matches the expected host and path, you call the completion handler with the callback URL and dismiss the view controller.
extension AuthenticationSessionViewController: WKNavigationDelegate {
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction
) async -> WKNavigationActionPolicy {
guard let url = navigationAction.request.url else {
return .cancel
}
if url.host == redirectMatch.host, url.path == redirectMatch.path {
completion(.callback(url))
dismiss(animated: true)
return .cancel
}
return .allow
}
}
This approach bypasses all the associated domain validation that causes ASWebAuthenticationSession to fail. You control when to intercept the redirect based on simple URL matching. No entitlements, no association files, no CDN delays.
The trade-off is you’re responsible for the user interface. Add a close button so users can cancel the flow. Update the navigation bar title with the current domain to help users understand where they are. Handle errors when the web view fails to load.
But you gain reliability. The authentication session works identically on simulator and device. You’re not dependent on Apple’s CDN or association file propagation. And you can debug the flow with standard web view logging.
Debugging Silent Failures
When HTTPS callbacks fail silently on device, start with the entitlements. Open the built app’s entitlements in Console.app or extract them from the archive. Verify that your domains appear exactly as you typed them, with no typos or extra whitespace.
Check the association file next. Fetch it directly with curl to confirm it exists and contains your bundle ID. The file must be valid JSON, served over HTTPS, and include your team ID prefix.
curl https://m.example.com/.well-known/apple-app-site-association
If both pieces look correct, wait. Apple’s CDN can take time to pick up association files, and iOS caches validation results. Uninstall the app completely, wait a minute, then reinstall. Sometimes the delay is just propagation lag.
For persistent issues, clear trusted domains in Settings → Developer on your test device. This forces iOS to revalidate all associated domains from scratch.
Start the conversation