diff --git a/changelog.txt b/changelog.txt index 366cdd474b..09b5eead87 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,6 @@ vNext ---------- +- [MINOR] Add SpanContext support to PasskeyWebListener and WebViewAuthorizationFragment (#2850) - [MINOR] Add additional allowed origins for PasskeyWebListener (#2839) - [PATCH] Add JavascriptInterface rules to consumer proguard rules (#2837) - [MINOR] Add optimized saveAndLoadAggregatedAccountData method in BrokerOAuth2TokenCache (#2832) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyWebListener.kt b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyWebListener.kt index a12f1fd1af..834bbccbc8 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyWebListener.kt +++ b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyWebListener.kt @@ -37,6 +37,7 @@ import com.microsoft.identity.common.BuildConfig import com.microsoft.identity.common.internal.ui.webview.AzureActiveDirectoryWebViewClient import com.microsoft.identity.common.java.exception.ClientException import com.microsoft.identity.common.logging.Logger +import io.opentelemetry.api.trace.SpanContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -51,10 +52,12 @@ import java.util.concurrent.atomic.AtomicBoolean * * @property coroutineScope Scope for launching credential operations. * @property credentialManagerHandler Handles passkey creation and retrieval. + * @property spanContext Optional OpenTelemetry span context for tracing. */ class PasskeyWebListener( private val coroutineScope: CoroutineScope, private val credentialManagerHandler: CredentialManagerHandler, + private val spanContext: SpanContext? = null ) : WebViewCompat.WebMessageListener { /** Tracks if a WebAuthN request is currently pending. Only one request is allowed at a time. */ @@ -106,7 +109,11 @@ class PasskeyWebListener( methodTag, "Received WebAuthN request of type: ${webAuthNMessage.type} from origin: $sourceOrigin" ) - val passkeyReplyChannel = PasskeyReplyChannel(javaScriptReplyProxy, webAuthNMessage.type) + val passkeyReplyChannel = PasskeyReplyChannel( + replyProxy = javaScriptReplyProxy, + requestType = webAuthNMessage.type, + spanContext = spanContext + ) // Only allow one request at a time. if (havePendingRequest.get()) { @@ -228,7 +235,10 @@ class PasskeyWebListener( messageData: String?, javaScriptReplyProxy: JavaScriptReplyProxy ): WebAuthNMessage? { - val passkeyReplyChannel = PasskeyReplyChannel(javaScriptReplyProxy) + val passkeyReplyChannel = PasskeyReplyChannel( + replyProxy = javaScriptReplyProxy, + spanContext = spanContext + ) return runCatching { if (messageData.isNullOrBlank()) { throw ClientException(ClientException.MISSING_PARAMETER, "Message data is null or blank") @@ -300,13 +310,15 @@ class PasskeyWebListener( * @param webView WebView to attach to. * @param activity Activity context for credential operations. * @param webClient WebViewClient to inject JavaScript into. + * @param spanContext Optional OpenTelemetry span context for tracing. * @return True if successfully hooked, false otherwise. */ @JvmStatic fun hook( webView: WebView, activity: Activity, - webClient: AzureActiveDirectoryWebViewClient + webClient: AzureActiveDirectoryWebViewClient, + spanContext: SpanContext? ): Boolean { val methodTag = "$TAG:hook" @@ -330,7 +342,8 @@ class PasskeyWebListener( PasskeyOriginRulesManager.getAllowedOriginRules(), PasskeyWebListener( coroutineScope = CoroutineScope(Dispatchers.Default), - credentialManagerHandler = CredentialManagerHandler(activity) + credentialManagerHandler = CredentialManagerHandler(activity), + spanContext = spanContext ) ) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java index 794089f4d7..af188fe12b 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java @@ -490,7 +490,8 @@ private void setupPasskeyWebListener(@NonNull final WebView webView, final String methodTag = TAG + ":setupPasskeyWebListener"; final String passkeyProtocolHeader = mRequestHeaders.get(FidoConstants.PASSKEY_PROTOCOL_HEADER_NAME); if (FidoConstants.PASSKEY_PROTOCOL_HEADER_AUTH_AND_REG.equals(passkeyProtocolHeader)) { - final boolean passkeyWebListenerHooked = PasskeyWebListener.hook(webView, requireActivity(), webViewClient); + final SpanContext spanContext = requireActivity() instanceof AuthorizationActivity ? ((AuthorizationActivity) requireActivity()).getSpanContext() : null; + final boolean passkeyWebListenerHooked = PasskeyWebListener.hook(webView, requireActivity(), webViewClient, spanContext); if (!passkeyWebListenerHooked) { Logger.warn(methodTag, "PasskeyWebListener hook failed, Downgrading to auth only."); // Downgrade to auth only diff --git a/common/src/test/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyWebListenerTest.kt b/common/src/test/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyWebListenerTest.kt index 54efaa2e87..af51e1b139 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyWebListenerTest.kt +++ b/common/src/test/java/com/microsoft/identity/common/internal/providers/oauth2/PasskeyWebListenerTest.kt @@ -479,7 +479,7 @@ class PasskeyWebListenerTest { @Config(sdk = [Build.VERSION_CODES.O_MR1]) // API 27 - below minimum fun `hook returns false on API below 28`() { // When - val result = PasskeyWebListener.hook(webView, activity, mockWebViewClient) + val result = PasskeyWebListener.hook(webView, activity, mockWebViewClient, null) // Then assertFalse(result) @@ -503,7 +503,7 @@ class PasskeyWebListenerTest { } just Runs // When - val result = PasskeyWebListener.hook(webView, activity, mockWebViewClient) + val result = PasskeyWebListener.hook(webView, activity, mockWebViewClient, null) // Then assertTrue(result) @@ -530,7 +530,7 @@ class PasskeyWebListenerTest { every { WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) } returns false // When - val result = PasskeyWebListener.hook(webView, activity, mockWebViewClient) + val result = PasskeyWebListener.hook(webView, activity, mockWebViewClient, null) // Then assertFalse(result)