diff --git a/changelog.txt b/changelog.txt index 1afb155305..5a74408334 100644 --- a/changelog.txt +++ b/changelog.txt @@ -9,6 +9,7 @@ vNext - [MINOR] Replace Deprecated Keystore API for Android 28+ (#2558) - [MINOR] Managed profile Android util method (#2561) - [PATCH] Make userHandle response field optional (#2560) +- [MINOR] Nonce redirect changes (#2552) Version 18.2.2 ---------- diff --git a/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java b/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java index 7061299e98..baabc607ce 100644 --- a/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java +++ b/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java @@ -1218,6 +1218,8 @@ public static String computeMaxHostBrokerProtocol() { */ public static final String COMPANY_PORTAL_APP_LAUNCH_ACTIVITY_NAME = Broker.COMPANY_PORTAL_APP_PACKAGE_NAME + ".views.SplashActivity"; + public static final String SSO_NONCE_PARAMETER = "sso_nonce"; + /** * PRT nonce. */ diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java index 521f2aebd7..7fc6cf781e 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java @@ -51,9 +51,14 @@ import com.microsoft.identity.common.internal.ui.webview.certbasedauth.AbstractSmartcardCertBasedAuthChallengeHandler; import com.microsoft.identity.common.internal.ui.webview.certbasedauth.AbstractCertBasedAuthChallengeHandler; import com.microsoft.identity.common.internal.ui.webview.certbasedauth.CertBasedAuthFactory; +import com.microsoft.identity.common.internal.ui.webview.challengehandlers.NonceRedirectHandler; import com.microsoft.identity.common.java.constants.FidoConstants; import com.microsoft.identity.common.java.flighting.CommonFlight; import com.microsoft.identity.common.java.flighting.CommonFlightsManager; +import com.microsoft.identity.common.java.opentelemetry.AttributeName; +import com.microsoft.identity.common.java.opentelemetry.OTelUtility; +import com.microsoft.identity.common.java.opentelemetry.SpanExtension; +import com.microsoft.identity.common.java.opentelemetry.SpanName; import com.microsoft.identity.common.java.ui.webview.authorization.IAuthorizationCompletionCallback; import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallenge; import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallengeFactory; @@ -65,7 +70,9 @@ import com.microsoft.identity.common.java.util.StringUtil; import com.microsoft.identity.common.logging.Logger; +import java.net.MalformedURLException; import java.net.URISyntaxException; +import java.net.URL; import java.security.Principal; import java.util.HashMap; import java.util.Locale; @@ -78,7 +85,10 @@ import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.PLAY_STORE_INSTALL_PREFIX; import static com.microsoft.identity.common.java.AuthenticationConstants.AAD.APP_LINK_KEY; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Scope; /** * For web view client, we do not distinguish V1 from V2. @@ -192,7 +202,11 @@ private boolean handleUrl(final WebView view, final String url) { spanContext, ViewTreeLifecycleOwner.get(view)); challengeHandler.processChallenge(challenge); - } else if (isRedirectUrl(formattedURL)) { + } else if (CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_ATTACH_NEW_PRT_HEADER_WHEN_NONCE_EXPIRED) && isNonceRedirect(formattedURL)) { + Logger.info(methodTag,"Navigation contains new nonce within the redirect uri. "+ url); + processNonceAndReAttachHeaders(view, url); + } + else if (isRedirectUrl(formattedURL)) { Logger.info(methodTag,"Navigation starts with the redirect uri."); processRedirectUrl(view, url); } else if (isWebsiteRequestUrl(formattedURL)) { @@ -271,6 +285,10 @@ private boolean isRedirectUrl(@NonNull final String url) { return url.startsWith(mRedirectUrl.toLowerCase(Locale.US)); } + private boolean isNonceRedirect(@NonNull final String url) { + return url.contains(AuthenticationConstants.Broker.SSO_NONCE_PARAMETER); + } + private boolean isWebsiteRequestUrl(@NonNull final String url) { return url.startsWith(AuthenticationConstants.Broker.BROWSER_EXT_PREFIX); } @@ -515,6 +533,38 @@ private void processHeaderForwardingRequiredUri(@NonNull final WebView view, @No view.loadUrl(url, mRequestHeaders); } + private void processNonceAndReAttachHeaders(@NonNull final WebView view, @NonNull final String url) { + final String methodTag = TAG + ":processNonceAndReAttachHeaders"; + + final HashMap queryParams = StringExtensions.getUrlParameters(url); + final String nonceQueryParam = queryParams.get("sso_nonce"); + SpanExtension.current().setAttribute( + AttributeName.is_sso_nonce_found_in_ests_request.name(), nonceQueryParam != null + ); + if (nonceQueryParam != null) { + final SpanContext spanContext = getActivity() instanceof AuthorizationActivity ? ((AuthorizationActivity) getActivity()).getSpanContext() : null; + final Span span = spanContext != null ? + OTelUtility.createSpanFromParent(SpanName.ProcessNonceFromEstsRedirect.name(), spanContext) : OTelUtility.createSpan(SpanName.ProcessNonceFromEstsRedirect.name()); + try (final Scope scope = SpanExtension.makeCurrentSpan(span)) { + final NonceRedirectHandler nonceRedirect = new NonceRedirectHandler(view, mRequestHeaders, span); + nonceRedirect.processChallenge(new URL(url)); + span.setStatus(StatusCode.OK); + } catch (MalformedURLException e) { + // No need to throw the error as we don't want to break the original flow. + Logger.errorPII(methodTag, "Redirect URI has invalid syntax, unable to parse", e); + span.setStatus(StatusCode.ERROR, "Redirect URI has invalid syntax, unable to parse"); + span.recordException(e); + } catch (final Throwable throwable) { + // No need to throw the error as we don't want to break the original flow. + Logger.error(methodTag, "Error processing nonce and re-attaching headers", throwable); + span.setStatus(StatusCode.ERROR, "Error processing nonce and re-attaching headers"); + span.recordException(throwable); + } finally { + span.end(); + } + } + } + private String removeQueryParametersOrRedact(@NonNull final String url) { final String methodTag = TAG + ":removeQueryParametersOrRedact"; try { diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/NonceRedirectHandler.kt b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/NonceRedirectHandler.kt new file mode 100644 index 0000000000..f61600f72c --- /dev/null +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/NonceRedirectHandler.kt @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.internal.ui.webview.challengehandlers + +import android.webkit.WebView +import com.microsoft.identity.common.java.broker.CommonRefreshTokenCredentialProvider +import com.microsoft.identity.common.adal.internal.AuthenticationConstants +import com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.SSO_NONCE_PARAMETER +import com.microsoft.identity.common.adal.internal.util.StringExtensions +import com.microsoft.identity.common.java.opentelemetry.AttributeName +import com.microsoft.identity.common.logging.Logger +import io.opentelemetry.api.trace.Span +import java.net.URL + +/** + * Handler for processing nonce from redirect and attaching new prt credential header on web view. + */ +class NonceRedirectHandler( + private val webView: WebView, + private val headers: HashMap, + private val span : Span +) : IChallengeHandler { + private val TAG = NonceRedirectHandler::class.java.simpleName + + override fun processChallenge(input: URL) : Void? { + val nonce = getNonceFromRedirectUrl(input) + if (nonce != null) { + modifyHeadersWithNewRefreshTokenCredential(nonce, input.toString()) + } + webView.loadUrl(input.toString(), headers) + return null + } + + private fun getNonceFromRedirectUrl(url: URL): String? { + val parameters = StringExtensions.getUrlParameters(url.toString()) + return parameters[SSO_NONCE_PARAMETER] + } + + private fun getPrtHeader(requestHeaders: HashMap): String? { + return requestHeaders[AuthenticationConstants.Broker.PRT_RESPONSE_HEADER] + } + + // Updates the headers by attaching a new refresh token credential header (Generated using the new nonce). + private fun modifyHeadersWithNewRefreshTokenCredential( + nonce: String, + url: String + ) { + val methodTag = "$TAG:getHeadersWithNewRefreshTokenCredential" + val prtHeader = getPrtHeader(headers) + if (!prtHeader.isNullOrEmpty()) { + Logger.info(methodTag, "PRT credential header found in headers!") + val username = getUserNameFromWebViewUrl(url) + if (username != null) { + val updatedRefreshTokenCredentialHeader = + CommonRefreshTokenCredentialProvider.getRefreshTokenCredentialUsingNewNonce( + url, username, + nonce + ) + if (updatedRefreshTokenCredentialHeader != null) { + headers[AuthenticationConstants.Broker.PRT_RESPONSE_HEADER] = + updatedRefreshTokenCredentialHeader + span.setAttribute(AttributeName.is_new_refresh_token_cred_header_attached.name, true) + } + } + } + } + + private fun getUserNameFromWebViewUrl(url: String): String? { + val parameters: Map = StringExtensions.getUrlParameters(url) + return parameters["login_hint"] + } +} diff --git a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java index 2a37372f59..cad43e475a 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java +++ b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java @@ -79,6 +79,8 @@ public class AzureActiveDirectoryWebViewClientTest { private static final String TEST_MSA_HEADER_FORWARDING_POSITIVE_URL = "https://login.live.com/oauth20_authorize.srf"; private static final String TEST_MSA_HEADER_FORWARDING_NEGATIVE_URL = "https://login.blah.com/oauth20_authorize.srf"; + private static final String TEST_NONCE_REDIRECT_URL = "https://login.microsoftonline.com/organizations/oAuth2/v2.0/authorize?&sso_nonce=ABCD"; + @Before public void setup() { mContext = ApplicationProvider.getApplicationContext(); @@ -184,5 +186,8 @@ public void testUrlOverrideHandlesHeaderForwardingRequiredUrl() { assertFalse(mWebViewClient.shouldOverrideUrlLoading(mMockWebView, TEST_MSA_HEADER_FORWARDING_NEGATIVE_URL)); } - + @Test + public void testUrlOverrideHandlesNonceRedirectUrl() { + assertTrue(mWebViewClient.shouldOverrideUrlLoading(mMockWebView, TEST_NONCE_REDIRECT_URL)); + } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/broker/CommonRefreshTokenCredentialProvider.kt b/common4j/src/main/com/microsoft/identity/common/java/broker/CommonRefreshTokenCredentialProvider.kt new file mode 100644 index 0000000000..c52594d274 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/broker/CommonRefreshTokenCredentialProvider.kt @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.broker + +import com.microsoft.identity.common.java.interfaces.IRefreshTokenCredentialProvider +import com.microsoft.identity.common.java.logging.Logger + +/** + * Consumer of commons needs to implement [IRefreshTokenCredentialProvider] interface + * and set it using CommonRefreshTokenCredentialProvider.initializeCommonRefreshTokenCredentialProvider(@NonNull refreshTokenCredentialProvider: IRefreshTokenCredentialProvider) + * to provide prtCredentialHolder to common module. + */ +object CommonRefreshTokenCredentialProvider : IRefreshTokenCredentialProvider { + private val TAG = CommonRefreshTokenCredentialProvider::class.java.simpleName + private var mRefreshTokenCredentialProvider: IRefreshTokenCredentialProvider? = null + + // Note : This method should only be invoked by broker module. + fun initializeCommonRefreshTokenCredentialProvider(refreshTokenCredentialProvider: IRefreshTokenCredentialProvider) { + val methodTag = "$TAG:initializeCommonRefreshTokenCredentialProvider" + Logger.info(methodTag, "Initializing common prt credential provider with " + refreshTokenCredentialProvider.javaClass.simpleName) + mRefreshTokenCredentialProvider = refreshTokenCredentialProvider + } + + override fun getRefreshTokenCredentialUsingNewNonce(inputUrl : String, username : String, nonce : String) : String? { + val methodTag = "$TAG:getRefreshTokenCredentialUsingNewNonce"; + if (mRefreshTokenCredentialProvider != null) { + return mRefreshTokenCredentialProvider!!.getRefreshTokenCredentialUsingNewNonce(inputUrl, username, nonce) + } + Logger.warn(methodTag, "mRefreshTokenCredentialHolder is not initialized!") + return null + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java b/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java index 0f150f2b47..3e248b62b3 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java +++ b/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java @@ -83,8 +83,12 @@ public enum CommonFlight implements IFlightConfig { /** * Flight to enable the legacy FIDO security key additional logic. Default is true for common. */ - ENABLE_LEGACY_FIDO_SECURITY_KEY_LOGIC("EnableLegacyFidoSecurityKeyLogic", true); + ENABLE_LEGACY_FIDO_SECURITY_KEY_LOGIC("EnableLegacyFidoSecurityKeyLogic", true), + /** + * Flight to enable the re-attachment of new PRT header logic. Default is true. + */ + ENABLE_ATTACH_NEW_PRT_HEADER_WHEN_NONCE_EXPIRED("EnableAttachNewPrtHeaderWhenNonceExpired", true); private String key; private Object defaultValue; CommonFlight(@NonNull String key, @NonNull Object defaultValue) { diff --git a/common4j/src/main/com/microsoft/identity/common/java/interfaces/IRefreshTokenCredentialProvider.kt b/common4j/src/main/com/microsoft/identity/common/java/interfaces/IRefreshTokenCredentialProvider.kt new file mode 100644 index 0000000000..09d054fad9 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/interfaces/IRefreshTokenCredentialProvider.kt @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.interfaces; + + +/** + * Consumer of commons needs to implement [IRefreshTokenCredentialProvider] interface + * and set it using CommonRefreshTokenCredentialProvider.initializeCommonRefreshTokenCredentialProvider(@NonNull refreshTokenCredentialProvider: IRefreshTokenCredentialProvider) + * to provide prtCredentialHolder to common module. + */ +interface IRefreshTokenCredentialProvider { + + /** + * Gets refresh token credential using nonce retrieved from webview. + */ + fun getRefreshTokenCredentialUsingNewNonce(inputUrl : String, username : String, nonce : String) : String? +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java b/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java index 0439e7b7e0..967a72e627 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java +++ b/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/AttributeName.java @@ -303,4 +303,14 @@ public enum AttributeName { * Indicates the stack trace from a Android KeyStore operation exception. */ keystore_exception_stack_trace, + + /** + * Indicates the new nonce found in the eSTS request. + */ + is_sso_nonce_found_in_ests_request, + + /** + * Indicates the new refresh token credential header attached in the eSTS request. + */ + is_new_refresh_token_cred_header_attached } diff --git a/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/SpanName.java b/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/SpanName.java index 67e306b79f..9e2f360484 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/SpanName.java +++ b/common4j/src/main/com/microsoft/identity/common/java/opentelemetry/SpanName.java @@ -58,4 +58,5 @@ public enum SpanName { OnUpgradeReceiver, UpgradeDeviceRegistration, RemoveBrokerAccount, + ProcessNonceFromEstsRedirect }