Repository navigation
Fix InsecureConnectionBuilder (Android) and add allowInsecureConnections support (iOS/macOS) - #650
jrevillard wants to merge 2 commits into
Conversation
…ecureConnections support for iOS/macOS Android: Replace no-op InsecureConnectionBuilder.openConnection() with real SSL bypass using TrustManager + HostnameVerifier when allowInsecureConnections=true. iOS/macOS: Add InsecureURLProtocol (NSURLProtocol subclass) that intercepts HTTPS connections and trusts all certificates. Register/unregister conditionally based on allowInsecureConnections flag. Previously, the flag was completely ignored on Apple platforms. Fixes MaikuB#386
|
Thanks for the PR. Before I go take a look into in more depth, do you have instructions I could follow to test this PR and ensure the functionality continues to work in the future? |
|
Hi @MaikuB, here's how to test this PR: PrerequisitesYou need an OIDC provider with a self-signed HTTPS certificate. The easiest way is a local Keycloak: Wait for it to start (~15s), then create a client and a test user via the admin console at
Alternatively, via the API: Test with the example app
What to verify
The key thing to confirm is that the flag is opt-in only: when I've tested this exact setup — Keycloak 26.6 starts correctly with the self-signed cert, the OIDC discovery endpoint responds, and token grants work against it. |
|
Any update @MaikuB ? |
|
I still need to get around to testing it out. I was part way through setting up Keycloak and had other priorities to deal with |
|
I've not gotten to try on iOS yet but ran into the follow problems after following the steps you had with the only differences being that redirect and post logout redirect URLs are set to match what current example app uses
Any advice here? |
|
Sorry for the delayed response @MaikuB. Both issues are in the example app config, not the PR. 1. Invalid scopesThe example app includes final List<String> _scopes = <String>["openid", "profile", "email"];2. localhost vs 10.0.2.2The two sign-in buttons use different config sources:
3. allowInsecureConnectionsAdd AuthorizationTokenRequest(
_clientId, _redirectUrl,
serviceConfiguration: _serviceConfiguration,
scopes: _scopes,
allowInsecureConnections: true,
)Keycloak startup with correct hostnamedocker run -d --name keycloak-test -p 8443:8443 \
-v $(pwd)/cert.pem:/etc/keycloak/cert.pem \
-v $(pwd)/key.pem:/etc/keycloak/key.pem \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:26.6 \
start \
--hostname=10.0.2.2 \
--https-certificate-file=/etc/keycloak/cert.pem \
--https-certificate-key-file=/etc/keycloak/key.pem |
|
Thanks for the info. I'll give it a go again will reach other if I need more help |
|
Unfortunately, I still cannot test this properly on Android though iOS and macOS work fine. Can you please provide end to end instructions that would allow the example app in this repo to connect to the Keycloak container via an Android emulator? Alternatively this could be instructions on getting the Keycloak container running but use another fork/branch of yours where it has the changes in this PR and updates required in the example app. In both cases, this should be something that has been tested to work end to end |
|
Other thing I forgot to mention is originally |
|
Hi @MaikuB, Thanks for the patience. I just ran the full setup end-to-end on my side (Keycloak 26.0.8 in Docker with a self-signed cert, exercising both HTTPS and HTTP). The two blockers you hit on Jun 14 are reproducible and have nothing to do with this PR — they're in What I verified
The two bugs you reported1.
|
| Line | Change |
|---|---|
| 46 | _issuer → 'https://10.0.2.2:8443/realms/flutter-appauth-test' |
| 47-48 | _discoveryUrl → 'https://10.0.2.2:8443/realms/flutter-appauth-test/.well-known/openid-configuration' |
| 55 | Remove 'api' (and the trailing comma on line 54) |
| 60-62 | Three _serviceConfiguration endpoints → https://10.0.2.2:8443/realms/flutter-appauth-test/protocol/openid-connect/{auth,token,logout} |
Then add allowInsecureConnections: true to all six request call sites in the file:
- Line 208 (
EndSessionRequestin_endSession) - Line 241 (
TokenRequestin_refresh) - Line 256 (
TokenRequestin_exchangeCode) - Line 287 (
AuthorizationRequestin_signInWithNoCodeExchange) - Line 319 (
AuthorizationRequestin_signInWithNoCodeExchangeAndGeneratedNonce) - Line 346 (
AuthorizationTokenRequestin_signInWithAutoCodeExchange)
Lines 293-294 and 361 are inside /* ... */ or ```dart ``` doc comments — leave them alone.
The minimum to reproduce the bugs you reported is just lines 287 and 346 (the two buttons you tried).
5. Install the cert into the emulator's CA store
CERT_HASH=$(openssl x509 -inform PEM -subject_hash_old -in ~/kc-cert/cert.pem | head -1)
cp ~/kc-cert/cert.pem /tmp/${CERT_HASH}.0
adb push /tmp/${CERT_HASH}.0 /system/etc/security/cacerts/
adb shell chmod 644 /system/etc/security/cacerts/${CERT_HASH}.0(Requires a -writable-system image. Modern Google APIs images need adb root && adb remount first.)
6. Run
cd flutter_appauth/example
flutter run -d emulator-5554Tap "Sign in with no code exchange" first (uses discovery → exercises HTTPS bypass). Then "Sign in with auto code exchange" (uses the hardcoded _serviceConfiguration → also exercises HTTPS bypass on token exchange).
HTTP test (your second question)
HTTP is not broken by this PR. Verified live: POST http://localhost:8080/realms/<realm>/protocol/openid-connect/token (ROPC) returned a valid JWT access_token + refresh_token + id_token with no TLS at all.
That said, the example app cannot reach an HTTP issuer out of the box — that's blocked at the OS layer, not at the plugin:
- Android: the example
AndroidManifest.xmlhas neitherandroid:usesCleartextTrafficnorandroid:networkSecurityConfig. On API 28+, cleartext to non-loopback is denied by default. To test HTTP, add anetwork_security_config.xmlpermitting cleartext to10.0.2.2and reference it from the manifest. - iOS: the example
Info.plisthas noNSAppTransportSecurityexception. Same story — you need an ATS exception to hit a plain-HTTP OIDC server.
Both are out of scope for this plugin PR. If you want a clean HTTP-test run, the manifest/plist changes are the prerequisite, not anything in this repo.
What I'm NOT asking you to change in this PR
Nothing. The PR is good as-is for the intended use case (HTTPS with self-signed cert against a local Keycloak during testing). The bypass is opt-in (allowInsecureConnections=true), defaults to false, and is only active for the duration of a single OIDC call.
One small follow-up I'd suggest (optional)
If you want a one-line test to convince yourself the bypass actually does something, in example/lib/main.dart flip allowInsecureConnections: true → false on line 346 only, rerun, and the token exchange should fail with CertPathValidatorException in logcat (the browser portion still works because Chrome has its own trust store with the cert you pushed in step 5). That's the cleanest "PR is doing real work" signal.
Happy to push a separate PR that updates example/lib/main.dart with all six edits if you'd prefer reviewers to test against a known-working baseline. Let me know.
While the trust-all NSURLProtocol was registered, every HTTPS request in the process was intercepted - not only the OIDC exchange but any concurrent request the app happens to make (images, analytics, other API traffic). The protocol is now armed with the exact set of hosts the exchange can contact (the service configuration endpoints, plus the issuer/discovery URL for the discovery flow) and only intercepts requests to those hosts:port; everything else keeps certificate verification even during an insecure exchange. Also normalize the service-configuration endpoint values and the issuer/discovery URL candidates through a type-checking helper: the Dart side sends NSNull for absent fields, and an NSNull reaching [NSURL URLWithString:] raises NSInvalidArgumentException on the platform thread. These are the same changes GENIE.AI carries in its vendored copy of this patch (mutation-gated by a structural test suite there), so they have been exercised against the OIDC flows indirectly, but they have NOT been compiled with Xcode in this environment - worth covering in the test pass.
|
I've pushed an update to the Apple-platform part of this PR, with two hardening changes (the same ones we now carry in GENIE.AI's vendored copy of this patch): 1. Host-scoped interception instead of process-global. While the trust-all 2. NSNull-safe endpoint handling. The Dart side sends On your earlier HTTP question: nothing changes for plain-HTTP providers — there is no certificate to bypass, and those requests aren't intercepted by the protocol (arming only matters for the TLS exchange; HTTP endpoints still work with One honest caveat: these changes are exercised by a structural test suite in our vendored copy, but they have not been compiled with Xcode in this environment — worth covering in your iOS/macOS test pass. |
Summary
Fixes #386
The
allowInsecureConnectionsparameter does not work on any platform:InsecureConnectionBuilder.openConnection()is a no-op — it callsURL.openConnection()without any SSL/TLS configuration, so HTTPS connections still validate the certificate chain normally.allowInsecureConnectionsparameter is completely ignored. There is no equivalent ofInsecureConnectionBuilder, and no SSL bypass mechanism exists.This makes it impossible to use the plugin against OIDC providers with self-signed certificates during development (e.g., local Keycloak instances).
Changes
Android
Replaced the no-op
InsecureConnectionBuilder.openConnection()with a real implementation that:SSLContextwith a trust-allX509TrustManagerHostnameVerifieronHttpsURLConnectionallowInsecureConnections=true(the existing routing inFlutterAppauthPluginviagetConnectionBuilder()is unchanged)iOS & macOS
Added
InsecureURLProtocol(anNSURLProtocolsubclass) that:NSURLSessionwith emptyprotocolClassesto avoid recursionNSURLSessionAuthChallengeUseCredentialallowInsecureConnections=trueAlso added
allowInsecureConnectionsproperty toTokenRequestParametersandEndSessionRequestParameterson iOS/macOS (was previously only defined on Android).Testing
Verified on Android emulator against a local Keycloak instance with a self-signed certificate:
All operations succeed without "Network error" when
allowInsecureConnections=true.Security Note
The SSL bypass is only active when
allowInsecureConnections=trueis explicitly passed by the caller. Production configurations use CA-signed certificates andallowInsecureConnections=false(the default), so this change has no production impact.