|
| 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.client.msal.automationapp.testpass.broker.concurrent |
| 24 | + |
| 25 | +import com.microsoft.identity.client.AcquireTokenSilentParameters |
| 26 | +import com.microsoft.identity.client.IAuthenticationResult |
| 27 | +import com.microsoft.identity.client.Prompt |
| 28 | +import com.microsoft.identity.client.SilentAuthenticationCallback |
| 29 | +import com.microsoft.identity.client.claims.ClaimsRequest |
| 30 | +import com.microsoft.identity.client.claims.RequestedClaimAdditionalInformation |
| 31 | +import com.microsoft.identity.client.exception.MsalException |
| 32 | +import com.microsoft.identity.client.msal.automationapp.BuildConfig |
| 33 | +import com.microsoft.identity.client.msal.automationapp.R |
| 34 | +import com.microsoft.identity.client.msal.automationapp.sdk.MsalAuthTestParams |
| 35 | +import com.microsoft.identity.client.msal.automationapp.sdk.MsalSdk |
| 36 | +import com.microsoft.identity.client.msal.automationapp.testpass.broker.AbstractMsalBrokerTest |
| 37 | +import com.microsoft.identity.client.ui.automation.TokenRequestTimeout |
| 38 | +import com.microsoft.identity.client.ui.automation.annotations.LongUIAutomationTest |
| 39 | +import com.microsoft.identity.client.ui.automation.annotations.RetryOnFailure |
| 40 | +import com.microsoft.identity.client.ui.automation.annotations.StressTest |
| 41 | +import com.microsoft.identity.client.ui.automation.annotations.SupportedBrokers |
| 42 | +import com.microsoft.identity.client.ui.automation.broker.BrokerMicrosoftAuthenticator |
| 43 | +import com.microsoft.identity.client.ui.automation.interaction.OnInteractionRequired |
| 44 | +import com.microsoft.identity.client.ui.automation.interaction.PromptHandlerParameters |
| 45 | +import com.microsoft.identity.client.ui.automation.interaction.PromptParameter |
| 46 | +import com.microsoft.identity.client.ui.automation.interaction.microsoftsts.AadPromptHandler |
| 47 | +import com.microsoft.identity.client.ui.automation.logging.Logger |
| 48 | +import com.microsoft.identity.common.java.exception.ClientException |
| 49 | +import com.microsoft.identity.common.java.providers.oauth2.IDToken |
| 50 | +import com.microsoft.identity.labapi.utilities.constants.TempUserType |
| 51 | +import com.microsoft.identity.labapi.utilities.constants.UserType |
| 52 | +import org.junit.Assert |
| 53 | +import org.junit.Test |
| 54 | + |
| 55 | +/** |
| 56 | + * Concurrent stress test for `AcquireTokenSilent` through the broker on a |
| 57 | + * Workplace-Joined (WPJ) device: register the device inline via a `deviceid` |
| 58 | + * claim on a single interactive sign-in, then fire [ITERATIONS] barrier- |
| 59 | + * synchronized waves of [CONCURRENT_THREADS] simultaneous `forceRefresh` silent |
| 60 | + * calls and assert none hangs or errors. |
| 61 | + * |
| 62 | + * Each thread uses a distinct scope so the `CommandDispatcher` can't de-duplicate |
| 63 | + * the simultaneous in-flight commands; the WPJ PRT satisfies every scope silently. |
| 64 | + * [CONCURRENT_THREADS] equals the pool size so every concurrent request is unique. |
| 65 | + */ |
| 66 | +@SupportedBrokers(brokers = [BrokerMicrosoftAuthenticator::class]) |
| 67 | +@StressTest |
| 68 | +@LongUIAutomationTest |
| 69 | +class TestCaseConcurrentAcquireTokenSilent : AbstractMsalBrokerTest() { |
| 70 | + |
| 71 | + @Test |
| 72 | + fun test_concurrentAcquireTokenSilent_withBroker() { |
| 73 | + val username = mLabAccount.username |
| 74 | + val password = mLabAccount.password |
| 75 | + |
| 76 | + val msalSdk = MsalSdk() |
| 77 | + |
| 78 | + // Inline WPJ: a deviceid claim on the interactive sign-in registers the |
| 79 | + // device (broker gets a PRT) and establishes the account in one flow. |
| 80 | + val deviceIdClaims = ClaimsRequest().apply { |
| 81 | + requestClaimInIdToken( |
| 82 | + "deviceid", |
| 83 | + RequestedClaimAdditionalInformation().apply { setEssential(true) }, |
| 84 | + ) |
| 85 | + } |
| 86 | + |
| 87 | + val interactiveParams = MsalAuthTestParams.builder() |
| 88 | + .activity(mActivity) |
| 89 | + .loginHint(username) |
| 90 | + .scopes(listOf(*mScopes)) |
| 91 | + .promptParameter(Prompt.LOGIN) |
| 92 | + .claims(deviceIdClaims) |
| 93 | + .msalConfigResourceId(configFileResourceId) |
| 94 | + .build() |
| 95 | + |
| 96 | + val interactiveResult = msalSdk.acquireTokenInteractive( |
| 97 | + interactiveParams, |
| 98 | + OnInteractionRequired { |
| 99 | + val promptHandlerParameters = PromptHandlerParameters.builder() |
| 100 | + .prompt(PromptParameter.LOGIN) |
| 101 | + .loginHint(username) |
| 102 | + .broker(mBroker) |
| 103 | + .sessionExpected(false) |
| 104 | + .registerPageExpected(true) |
| 105 | + .consentPageExpected(false) |
| 106 | + .speedBumpExpected(false) |
| 107 | + .expectingLoginPageAccountPicker(false) |
| 108 | + .build() |
| 109 | + |
| 110 | + AadPromptHandler(promptHandlerParameters).handlePrompt(username, password) |
| 111 | + }, |
| 112 | + TokenRequestTimeout.MEDIUM, |
| 113 | + ) |
| 114 | + |
| 115 | + interactiveResult.assertSuccess() |
| 116 | + |
| 117 | + // Confirm the device actually registered (deviceid present in the token). |
| 118 | + val claims = IDToken.parseJWT(interactiveResult.accessToken) |
| 119 | + Assert.assertNotNull("deviceid claim must be present after inline WPJ", claims["deviceid"]) |
| 120 | + |
| 121 | + val account = msalSdk.getAccount(mActivity, configFileResourceId, username) |
| 122 | + Assert.assertNotNull("Account must not be null after a successful interactive sign-in", account) |
| 123 | + |
| 124 | + // Distinct scope per thread → no command de-duplication. |
| 125 | + val result = ConcurrentAcquireTokenSilentHelper.run( |
| 126 | + CONCURRENT_THREADS, |
| 127 | + ITERATIONS, |
| 128 | + PER_WAVE_TIMEOUT_SECONDS, |
| 129 | + ) { threadIndex, iteration, done, errors -> |
| 130 | + val silentParameters = AcquireTokenSilentParameters.Builder() |
| 131 | + .forAccount(account) |
| 132 | + .fromAuthority(account.authority) |
| 133 | + .withScopes(ConcurrentAcquireTokenSilentHelper.scopesForThread(threadIndex)) |
| 134 | + .forceRefresh(true) |
| 135 | + .withCallback(object : SilentAuthenticationCallback { |
| 136 | + override fun onSuccess(authenticationResult: IAuthenticationResult) { |
| 137 | + done.countDown() |
| 138 | + } |
| 139 | + |
| 140 | + override fun onError(exception: MsalException) { |
| 141 | + errors.add("Thread $threadIndex iter $iteration [${exception.errorCode}]: $exception") |
| 142 | + done.countDown() |
| 143 | + } |
| 144 | + }) |
| 145 | + .build() |
| 146 | + |
| 147 | + mApplication.acquireTokenSilentAsync(silentParameters) |
| 148 | + } |
| 149 | + |
| 150 | + Assert.assertTrue( |
| 151 | + "Concurrent AcquireTokenSilent got stuck: not all $CONCURRENT_THREADS threads" + |
| 152 | + " completed $ITERATIONS waves (per-wave timeout ${PER_WAVE_TIMEOUT_SECONDS}s)", |
| 153 | + result.allCompleted, |
| 154 | + ) |
| 155 | + |
| 156 | + // null_object under concurrency is a known broker issue whose fix (network-token fallback) |
| 157 | + // is gated to MSAL_CPP (OneAuth). See BrokerFlight.USE_NETWORK_TOKEN_FALLBACK_FOR_NULL_OBJECT |
| 158 | + val unexpectedErrors = result.errors.filterNot { it.contains("[${ClientException.NULL_OBJECT}]") } |
| 159 | + |
| 160 | + val toleratedNullObjects = result.errors.size - unexpectedErrors.size |
| 161 | + if (toleratedNullObjects > 0) { |
| 162 | + Logger.w( |
| 163 | + TAG, |
| 164 | + "Tolerated $toleratedNullObjects known null_object error(s);" + |
| 165 | + " UseNetworkTokenFallbackForNullObjectMsalAndroid flight is off", |
| 166 | + ) |
| 167 | + } |
| 168 | + |
| 169 | + Assert.assertTrue( |
| 170 | + "Some concurrent AcquireTokenSilent calls failed: $unexpectedErrors", |
| 171 | + unexpectedErrors.isEmpty(), |
| 172 | + ) |
| 173 | + } |
| 174 | + |
| 175 | + override fun getJsonUserType(): UserType = UserType.BASIC |
| 176 | + |
| 177 | + override fun getTempUserType(): TempUserType? = null |
| 178 | + |
| 179 | + override fun getScopes(): Array<String> = arrayOf("User.read") |
| 180 | + |
| 181 | + override fun getAuthority(): String = |
| 182 | + mApplication.configuration.defaultAuthority.toString() |
| 183 | + |
| 184 | + override fun getConfigFileResourceId(): Int = R.raw.msal_config_default |
| 185 | + |
| 186 | + companion object { |
| 187 | + private val TAG = TestCaseConcurrentAcquireTokenSilent::class.java.simpleName |
| 188 | + |
| 189 | + /** One thread per pooled scope, so every concurrent request is unique. */ |
| 190 | + private val CONCURRENT_THREADS = ConcurrentAcquireTokenSilentHelper.SCOPE_POOL.size |
| 191 | + private const val ITERATIONS = 100 |
| 192 | + private const val PER_WAVE_TIMEOUT_SECONDS = 30L |
| 193 | + } |
| 194 | +} |
0 commit comments