Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ It receives three parameters:

```javascript
window.kmpJsBridge.callNative = function (methodName, params, callback) {
...
// implementation omitted
}
```

Expand Down Expand Up @@ -420,6 +420,54 @@ val navigator =
)
```

## HTTP Authentication (Basic and Digest)

This library allows you to handle HTTP authentication challenges by providing a `BasicAuthInterceptor`.

When a website issues an HTTP authentication challenge (Basic or Digest), the platform WebView will call into your interceptor so you can supply credentials (or cancel). The library then forwards those credentials back to the platform to complete the authentication flow. The underlying platform (Android/iOS) negotiates the exact scheme with the server.

### Usage

1) Create an implementation of `BasicAuthInterceptor` and prompt/fetch credentials as needed:

```kotlin
class MyBasicAuthInterceptor : BasicAuthInterceptor {
override fun onHttpAuthRequest(
challenge: BasicAuthChallenge,
handler: BasicAuthHandler,
navigator: WebViewNavigator,
): Boolean {
// Decide if you want to handle this challenge (host/realm-based, etc.)
// Return true if you will handle it, false to let default platform handling proceed.

// Example: Provide stored or prompted credentials
val username = "user"
val password = "pass"

// Exactly one of proceed or cancel should be called.
handler.proceed(username, password)
return true
}
}
```

2) Provide the interceptor when creating your `WebViewNavigator`:

```kotlin
@Composable
fun MyScreen() {
val navigator = rememberWebViewNavigator(
basicAuthInterceptor = MyBasicAuthInterceptor()
)
// pass navigator to your WebView as you normally do
}
```

Notes:
- Call `handler.proceed(username, password)` to continue with the provided credentials or `handler.cancel()` to abort.
- You can use `challenge.host`, `challenge.realm`, and `challenge.previousFailureCount` to decide how to respond (e.g., avoid repeated prompts on failures).
- The platform handles the specifics of Basic vs Digest; your interceptor only supplies credentials or cancels.

## WebSettings

Starting from version 1.3.0, this library allows users to customize web settings.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import androidx.core.graphics.createBitmap
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewAssetLoader
import androidx.webkit.WebViewFeature
import com.multiplatform.webview.basicauth.BasicAuthChallenge
import com.multiplatform.webview.basicauth.BasicAuthHandler
import com.multiplatform.webview.jsbridge.ConsoleBridge
import com.multiplatform.webview.jsbridge.WebViewJsBridge
import com.multiplatform.webview.request.WebRequest
Expand Down Expand Up @@ -432,6 +434,44 @@ open class AccompanistWebViewClient : WebViewClient() {
}
}
}

override fun onReceivedHttpAuthRequest(
view: WebView?,
handler: android.webkit.HttpAuthHandler?,
host: String?,
realm: String?
) {
val interceptor = navigator.basicAuthInterceptor
if (interceptor == null || handler == null || host == null) {
super.onReceivedHttpAuthRequest(view, handler, host, realm)
return
}
val challenge = BasicAuthChallenge(
host = host,
realm = realm,
isProxy = false,
previousFailureCount = try {
handler.useHttpAuthUsernamePassword(); 0
} catch (_: Throwable) {
0
}
)
val wrapped = object : BasicAuthHandler {
private var used = false
override fun proceed(username: String, password: String) {
if (used) return; used = true
handler.proceed(username, password)
}
override fun cancel() {
if (used) return; used = true
handler.cancel()
}
}
val stop = try { interceptor.onHttpAuthRequest(challenge, wrapped, navigator) } catch (_: Throwable) { false }
if (!stop) {
super.onReceivedHttpAuthRequest(view, handler, host, realm)
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.multiplatform.webview.basicauth

import com.multiplatform.webview.web.WebViewNavigator

data class BasicAuthChallenge(
val host: String,
val realm: String? = null,
val isProxy: Boolean = false,
val previousFailureCount: Int = 0,
)

/**
* Exactly one of proceed or cancel should be called.
*/
interface BasicAuthHandler {
fun proceed(username: String, password: String)
fun cancel()
}

interface BasicAuthInterceptor {
fun onHttpAuthRequest(
challenge: BasicAuthChallenge,
handler: BasicAuthHandler,
navigator: WebViewNavigator,
): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.multiplatform.webview.basicauth.BasicAuthInterceptor
import com.multiplatform.webview.request.RequestInterceptor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -28,6 +29,7 @@ import kotlinx.coroutines.withContext
class WebViewNavigator(
val coroutineScope: CoroutineScope,
val requestInterceptor: RequestInterceptor? = null,
val basicAuthInterceptor: BasicAuthInterceptor? = null,
) {
/**
* Sealed class for constraining possible navigation events.
Expand Down Expand Up @@ -310,4 +312,5 @@ class WebViewNavigator(
fun rememberWebViewNavigator(
coroutineScope: CoroutineScope = rememberCoroutineScope(),
requestInterceptor: RequestInterceptor? = null,
): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope, requestInterceptor) }
basicAuthInterceptor: BasicAuthInterceptor? = null,
): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope, requestInterceptor, basicAuthInterceptor) }
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.multiplatform.webview.web

import com.multiplatform.webview.basicauth.BasicAuthChallenge
import com.multiplatform.webview.basicauth.BasicAuthHandler
import com.multiplatform.webview.request.WebRequest
import com.multiplatform.webview.request.WebRequestInterceptResult
import com.multiplatform.webview.util.KLogger
Expand All @@ -10,7 +12,10 @@ import kotlinx.cinterop.ObjCSignatureOverride
import platform.CoreGraphics.CGPointMake
import platform.Foundation.HTTPMethod
import platform.Foundation.NSError
import platform.Foundation.NSURLCredentialPersistence
import platform.Foundation.NSURLSessionAuthChallengeUseCredential
import platform.Foundation.allHTTPHeaderFields
import platform.Foundation.credentialWithUser
import platform.WebKit.WKNavigation
import platform.WebKit.WKNavigationAction
import platform.WebKit.WKNavigationActionPolicy
Expand Down Expand Up @@ -180,4 +185,55 @@ class WKNavigationDelegate(
decisionHandler(WKNavigationActionPolicy.WKNavigationActionPolicyAllow)
}
}

// HTTP Auth (Basic/Digest) handling forwarded to interceptor if provided
@ObjCSignatureOverride
override fun webView(
webView: WKWebView,
didReceiveAuthenticationChallenge: platform.Foundation.NSURLAuthenticationChallenge,
completionHandler: (platform.Foundation.NSURLSessionAuthChallengeDisposition, platform.Foundation.NSURLCredential?) -> Unit,
) {
val interceptor = navigator.basicAuthInterceptor
val protectionSpace = didReceiveAuthenticationChallenge.protectionSpace
val method = protectionSpace.authenticationMethod
// Handle both HTTP Basic and HTTP Digest challenges.
val isSupported = method == platform.Foundation.NSURLAuthenticationMethodHTTPBasic ||
method == platform.Foundation.NSURLAuthenticationMethodHTTPDigest
if (interceptor == null || !isSupported) {
completionHandler(platform.Foundation.NSURLSessionAuthChallengePerformDefaultHandling, null)
return
}
val host = protectionSpace.host
val realm = protectionSpace.realm
val challenge = BasicAuthChallenge(
host = host,
realm = realm,
isProxy = protectionSpace.isProxy()
)
val handled = try {
interceptor.onHttpAuthRequest(
challenge,
object : BasicAuthHandler {
private var used = false
override fun proceed(username: String, password: String) {
if (used) return; used = true
val cred = platform.Foundation.NSURLCredential.credentialWithUser(
user = username,
password = password,
persistence = NSURLCredentialPersistence.NSURLCredentialPersistenceForSession,
)
completionHandler(NSURLSessionAuthChallengeUseCredential, cred)
}
override fun cancel() {
if (used) return; used = true
completionHandler(platform.Foundation.NSURLSessionAuthChallengeCancelAuthenticationChallenge, null)
}
},
navigator,
)
} catch (_: Throwable) { false }
if (!handled) {
completionHandler(platform.Foundation.NSURLSessionAuthChallengePerformDefaultHandling, null)
}
}
}
Loading