Skip to content

Commit d8979bd

Browse files
Copilotsomalaya
andauthored
fix: propagate mLoginHint fallback to NonceRedirectHandler for eSTS+sso_nonce redirects
When an eSTS redirect URL contains sso_nonce (a common scenario), isNonceRedirect fires before isEstsCloudHost in the handleUrl() chain. NonceRedirectHandler now accepts an optional fallbackUsername (passed as mLoginHint from the initial authorize URL), so PRT re-attachment with the new nonce succeeds even when login_hint is absent from the redirect URL. Adds NonceRedirectHandlerTest covering the fallback path and the no-fallback guard. Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/cb6db2b2-56e8-4f9d-ac48-45d3d1aaa520 Co-authored-by: somalaya <69237821+somalaya@users.noreply.github.com>
1 parent fa699fd commit d8979bd

3 files changed

Lines changed: 140 additions & 3 deletions

File tree

common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1111,7 +1111,7 @@ private void processNonceAndReAttachHeaders(@NonNull final WebView view, @NonNul
11111111
if (nonceQueryParam != null) {
11121112
final Span span = OTelUtility.createSpanFromParent(SpanName.ProcessNonceFromEstsRedirect.name(), mSpanContext);
11131113
try (final Scope scope = SpanExtension.makeCurrentSpan(span)) {
1114-
final NonceRedirectHandler nonceRedirect = new NonceRedirectHandler(view, mRequestHeaders, span);
1114+
final NonceRedirectHandler nonceRedirect = new NonceRedirectHandler(view, mRequestHeaders, span, mLoginHint);
11151115
nonceRedirect.processChallenge(new URL(url));
11161116
span.setStatus(StatusCode.OK);
11171117
} catch (MalformedURLException e) {

common/src/main/java/com/microsoft/identity/common/internal/ui/webview/challengehandlers/NonceRedirectHandler.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ import java.net.URL
3838
class NonceRedirectHandler(
3939
private val webView: WebView,
4040
private val headers: HashMap<String, String>,
41-
private val span : Span
41+
private val span : Span,
42+
private val fallbackUsername: String? = null
4243
) : IChallengeHandler<URL, Void> {
4344
private val TAG = NonceRedirectHandler::class.java.simpleName
4445

@@ -87,6 +88,6 @@ class NonceRedirectHandler(
8788

8889
private fun getUserNameFromWebViewUrl(url: String): String? {
8990
val parameters: Map<String, String> = StringExtensions.getUrlParameters(url)
90-
return parameters["login_hint"]
91+
return parameters["login_hint"] ?: fallbackUsername
9192
}
9293
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
package com.microsoft.identity.common.internal.ui.webview.challengehandlers
24+
25+
import android.webkit.WebView
26+
import com.microsoft.identity.common.adal.internal.AuthenticationConstants
27+
import com.microsoft.identity.common.java.broker.CommonRefreshTokenCredentialProvider
28+
import com.microsoft.identity.common.java.opentelemetry.AttributeName
29+
import io.mockk.every
30+
import io.mockk.mockkObject
31+
import io.mockk.unmockkAll
32+
import io.opentelemetry.api.trace.Span
33+
import org.junit.After
34+
import org.junit.Before
35+
import org.junit.Test
36+
import org.junit.runner.RunWith
37+
import org.mockito.Mockito.mock
38+
import org.mockito.Mockito.never
39+
import org.mockito.Mockito.verify
40+
import org.robolectric.RobolectricTestRunner
41+
import java.net.URL
42+
43+
@RunWith(RobolectricTestRunner::class)
44+
class NonceRedirectHandlerTest {
45+
46+
private lateinit var webView: WebView
47+
private lateinit var headers: HashMap<String, String>
48+
private lateinit var span: Span
49+
50+
companion object {
51+
private const val ESTS_URL_WITH_NONCE_AND_HINT =
52+
"https://login.microsoftonline.com/common/oauth2/authorize?sso_nonce=testnonce&login_hint=user@example.com"
53+
private const val ESTS_URL_WITH_NONCE_NO_HINT =
54+
"https://login.microsoftonline.com/common/oauth2/authorize?sso_nonce=testnonce"
55+
private const val ESTS_URL_NO_NONCE =
56+
"https://login.microsoftonline.com/common/oauth2/authorize"
57+
private const val FRESH_CREDENTIAL = "freshPrtCredential"
58+
private const val FALLBACK_USERNAME = "fallback@example.com"
59+
}
60+
61+
@Before
62+
fun setUp() {
63+
webView = mock(WebView::class.java)
64+
headers = hashMapOf(
65+
AuthenticationConstants.Broker.PRT_RESPONSE_HEADER to "existingPrtCredential"
66+
)
67+
span = mock(Span::class.java)
68+
}
69+
70+
@After
71+
fun tearDown() {
72+
unmockkAll()
73+
}
74+
75+
@Test
76+
fun `processChallenge attaches PRT credential when login_hint is in URL`() {
77+
mockkObject(CommonRefreshTokenCredentialProvider)
78+
every {
79+
CommonRefreshTokenCredentialProvider.getRefreshTokenCredentialUsingNewNonce(
80+
ESTS_URL_WITH_NONCE_AND_HINT, "user@example.com", "testnonce"
81+
)
82+
} returns FRESH_CREDENTIAL
83+
84+
val handler = NonceRedirectHandler(webView, headers, span)
85+
handler.processChallenge(URL(ESTS_URL_WITH_NONCE_AND_HINT))
86+
87+
verify(span).setAttribute(AttributeName.is_new_refresh_token_cred_header_attached.name, true)
88+
verify(webView).loadUrl(ESTS_URL_WITH_NONCE_AND_HINT, headers)
89+
}
90+
91+
@Test
92+
fun `processChallenge uses fallbackUsername when login_hint absent from URL`() {
93+
mockkObject(CommonRefreshTokenCredentialProvider)
94+
every {
95+
CommonRefreshTokenCredentialProvider.getRefreshTokenCredentialUsingNewNonce(
96+
ESTS_URL_WITH_NONCE_NO_HINT, FALLBACK_USERNAME, "testnonce"
97+
)
98+
} returns FRESH_CREDENTIAL
99+
100+
val handler = NonceRedirectHandler(webView, headers, span, fallbackUsername = FALLBACK_USERNAME)
101+
handler.processChallenge(URL(ESTS_URL_WITH_NONCE_NO_HINT))
102+
103+
verify(span).setAttribute(AttributeName.is_new_refresh_token_cred_header_attached.name, true)
104+
verify(webView).loadUrl(ESTS_URL_WITH_NONCE_NO_HINT, headers)
105+
}
106+
107+
@Test
108+
fun `processChallenge skips PRT attachment when login_hint absent and no fallback provided`() {
109+
val handler = NonceRedirectHandler(webView, headers, span)
110+
handler.processChallenge(URL(ESTS_URL_WITH_NONCE_NO_HINT))
111+
112+
verify(span, never()).setAttribute(AttributeName.is_new_refresh_token_cred_header_attached.name, true)
113+
verify(webView).loadUrl(ESTS_URL_WITH_NONCE_NO_HINT, headers)
114+
}
115+
116+
@Test
117+
fun `processChallenge skips PRT attachment when no sso_nonce in URL`() {
118+
val handler = NonceRedirectHandler(webView, headers, span, fallbackUsername = FALLBACK_USERNAME)
119+
handler.processChallenge(URL(ESTS_URL_NO_NONCE))
120+
121+
verify(span, never()).setAttribute(AttributeName.is_new_refresh_token_cred_header_attached.name, true)
122+
verify(webView).loadUrl(ESTS_URL_NO_NONCE, headers)
123+
}
124+
125+
@Test
126+
fun `processChallenge skips PRT attachment when no PRT header in original headers`() {
127+
val headersWithoutPrt = HashMap<String, String>()
128+
mockkObject(CommonRefreshTokenCredentialProvider)
129+
130+
val handler = NonceRedirectHandler(webView, headersWithoutPrt, span, fallbackUsername = FALLBACK_USERNAME)
131+
handler.processChallenge(URL(ESTS_URL_WITH_NONCE_NO_HINT))
132+
133+
verify(span, never()).setAttribute(AttributeName.is_new_refresh_token_cred_header_attached.name, true)
134+
verify(webView).loadUrl(ESTS_URL_WITH_NONCE_NO_HINT, headersWithoutPrt)
135+
}
136+
}

0 commit comments

Comments
 (0)