diff --git a/changelog.txt b/changelog.txt index fe165600f4..0f58630cfd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,7 @@ vNext ---------- - [PATCH] Handle app_link Intent redirection by validating broker install links and rejecting unsupported redirect URIs with appropriate error responses (#3102) +- [MINOR] Add onboarding telemetry blob fields to BrokerRequest/BrokerResult and command parameters for client↔broker IPC transport (#3111) - [PATCH] Extend filter-then-clone optimization to load() and getIdTokensForAccountRecord() in MsalOAuth2TokenCache: when ENABLE_FILTER_THEN_CLONE_IN_MEMORY_CACHE flight is enabled, skip clone-all preload and call direct flight-gated overloads that clone only matching credentials; add new getCredentialsFilteredBy overload with kid support (#3100) - [MINOR] Add onboarding telemetry recorder, field keys, and session persistence for mobile onboarding flow (#3088) - [PATCH] Move Multiple Listening apps check to the authorization layer (#3070) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerRequest.java b/common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerRequest.java index 8f288f6684..991e6fb365 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerRequest.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerRequest.java @@ -87,6 +87,7 @@ private static final class SerializedNames { final static String TENANT_ID = "tenant_id"; final static String REQUEST_TYPE = "request_type"; final static String WEB_APPS_STATE = "web_apps_state"; + final static String ONBOARDING_SEED_JSON = "onboarding_seed_json"; } /** @@ -295,4 +296,17 @@ private static final class SerializedNames { @Nullable @SerializedName(SerializedNames.REQUEST_TYPE) private String mRequestType; + + /** + * Onboarding telemetry seed JSON blob. + * Direction: client → broker (input only). Contains sessionCorrelationId, + * onboarding_mode, and schema_version, supplied by the client (OneAuth/MSAL) + * so the broker can construct an OnboardingTelemetryRecorder using the same + * correlation id. The broker returns the populated blob (with steps and + * blocking errors) via {@link BrokerResult#getOnboardingBlob()}, not via + * this field. + */ + @Nullable + @SerializedName(SerializedNames.ONBOARDING_SEED_JSON) + private String mOnboardingSeedJson; } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerResult.java b/common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerResult.java index 4c0cc5f3c2..a168acefbf 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerResult.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerResult.java @@ -97,6 +97,7 @@ private static class SerializedNames { static final String SPE_RING = "spe_ring"; static final String CLI_TELEM_ERRORCODE = "cli_telem_error_code"; static final String CLI_TELEM_SUB_ERROR_CODE = "cli_telem_suberror_code"; + static final String ONBOARDING_BLOB = "onboarding_blob"; } private static final long serialVersionUID = 8606631820514878489L; @@ -327,6 +328,13 @@ private static class SerializedNames { @SerializedName(SerializedNames.BROKER_AAD_DEVICE_ID_RECORD) private final AadDeviceIdRecord mAadDeviceIdRecord; + /** + * Populated onboarding telemetry blob JSON returned by the broker. + */ + @Nullable + @SerializedName(SerializedNames.ONBOARDING_BLOB) + private final String mOnboardingBlob; + private BrokerResult(@NonNull final Builder builder) { mAccessToken = builder.mAccessToken; mIdToken = builder.mIdToken; @@ -362,6 +370,7 @@ private BrokerResult(@NonNull final Builder builder) { mCliTelemSubErrorCode = builder.mCliTelemSubErrorCode; mExceptionType = builder.mExceptionType; mAadDeviceIdRecord = builder.mAadDeviceIdRecord; + mOnboardingBlob = builder.mOnboardingBlob; } public String getExceptionType() { @@ -498,6 +507,11 @@ public AadDeviceIdRecord getAadDeviceIdRecord() { return mAadDeviceIdRecord; } + @Nullable + public String getOnboardingBlob() { + return mOnboardingBlob; + } + public static class Builder { private String mAccessToken; private String mIdToken; @@ -523,6 +537,7 @@ public static class Builder { private List mTenantProfileData; private boolean mServicedFromCache; private AadDeviceIdRecord mAadDeviceIdRecord; + private String mOnboardingBlob; // Exception parameters private String mErrorCode; @@ -535,7 +550,6 @@ public static class Builder { private String mCliTelemErrorCode; private String mCliTelemSubErrorCode; private String mExceptionType; - public Builder accessToken(@Nullable final String accessToken) { this.mAccessToken = accessToken; return this; @@ -712,6 +726,11 @@ public Builder exceptionType(String exceptionType) { this.mExceptionType = exceptionType; return this; } + + public Builder onboardingBlob(@Nullable final String onboardingBlob) { + this.mOnboardingBlob = onboardingBlob; + return this; + } } } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/request/MsalBrokerRequestAdapter.java b/common/src/main/java/com/microsoft/identity/common/internal/request/MsalBrokerRequestAdapter.java index 3d7c4f568c..109b6190ec 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/request/MsalBrokerRequestAdapter.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/request/MsalBrokerRequestAdapter.java @@ -139,7 +139,8 @@ public BrokerRequest brokerRequestFromAcquireTokenParameters(@NonNull final Inte .preferredBrowser(parameters.getPreferredBrowser()) .preferredAuthMethod(parameters.getPreferredAuthMethod()) .accountTransferToken(parameters.getAccountTransferToken()) - .suppressAccountPicker(parameters.isSuppressBrokerAccountPicker()); + .suppressAccountPicker(parameters.isSuppressBrokerAccountPicker()) + .onboardingSeedJson(parameters.getOnboardingSeedJson()); if (parameters instanceof AndroidInteractiveTokenCommandParameters) { final AndroidInteractiveTokenCommandParameters androidInteractiveTokenCommandParameters = (AndroidInteractiveTokenCommandParameters) parameters; diff --git a/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java b/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java index d9b31430c9..1c40c2c646 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java @@ -465,9 +465,16 @@ public Bundle bundleFromBaseExceptionForWebApps(@NonNull final BaseException exc @NonNull @Override public ILocalAuthenticationResult authenticationResultFromBundle(@NonNull final Bundle resultBundle) throws ClientException { - final String methodTag = TAG + ":authenticationResultFromBundle"; - final BrokerResult brokerResult = brokerResultFromBundle(resultBundle); + return authenticationResultFromBrokerResult(brokerResultFromBundle(resultBundle)); + } + /** + * Overload that builds the authentication result from an already-deserialized + * {@link BrokerResult}. Use this when the caller has the [BrokerResult] in hand + * to avoid a redundant deserialization of the result bundle. + */ + public ILocalAuthenticationResult authenticationResultFromBrokerResult(@NonNull final BrokerResult brokerResult) throws ClientException { + final String methodTag = TAG + ":authenticationResultFromBrokerResult"; Logger.info(methodTag, "Broker Result returned from Bundle, constructing authentication result"); final List tenantProfileCacheRecords = brokerResult.getTenantProfileData(); @@ -565,6 +572,39 @@ public BrokerPerformanceMetrics getBrokerPerformanceMetricsFromBundle(@NonNull f } } + /** + * Extracts the onboarding telemetry blob (JSON string) from the result bundle. + * Best-effort: returns null if the bundle cannot be deserialized into a BrokerResult + * or if no blob is present. Telemetry failures must never fail an otherwise-successful + * auth result. Blob contents are not logged (may carry sessionCorrelationId). + * + * If the caller has already deserialized the [BrokerResult], prefer the overload + * [getOnboardingBlobFromBundle(BrokerResult)] to avoid a second deserialization. + */ + @Nullable + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public String getOnboardingBlobFromBundle(@NonNull final Bundle resultBundle) { + final String methodTag = TAG + ":getOnboardingBlobFromBundle"; + try { + final BrokerResult brokerResult = brokerResultFromBundle(resultBundle); + return brokerResult.getOnboardingBlob(); + } catch (final ClientException e) { + Logger.warn(methodTag, "Failed to extract onboarding blob from broker result: " + e.getErrorCode()); + return null; + } + } + + /** + * Overload that reads the onboarding telemetry blob from a {@link BrokerResult} + * that has already been deserialized by the caller. Use this when you already + * have a [BrokerResult] in hand to avoid a second deserialization of the bundle. + */ + @Nullable + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public String getOnboardingBlobFromBundle(@NonNull final BrokerResult brokerResult) { + return brokerResult.getOnboardingBlob(); + } + @NonNull @Override public AcquirePrtSsoTokenResult getAcquirePrtSsoTokenResultFromBundle(Bundle resultBundle) { @@ -1017,9 +1057,14 @@ public AcquireTokenResult getDeviceCodeFlowTokenResultFromResultBundle(@NonNull AcquireTokenResult getAcquireTokenResultFromResultBundle(@NonNull final Bundle resultBundle) throws BaseException { final MsalBrokerResultAdapter resultAdapter = new MsalBrokerResultAdapter(); if (resultBundle.getBoolean(AuthenticationConstants.Broker.BROKER_REQUEST_V2_SUCCESS)) { + // Deserialize BrokerResult once and reuse for both the local authentication result + // and the onboarding telemetry blob, instead of letting authenticationResultFromBundle + // and getOnboardingBlobFromBundle each pay the deserialization cost. + final BrokerResult brokerResult = resultAdapter.brokerResultFromBundle(resultBundle); + final AcquireTokenResult acquireTokenResult = new AcquireTokenResult(); acquireTokenResult.setLocalAuthenticationResult( - resultAdapter.authenticationResultFromBundle(resultBundle) + resultAdapter.authenticationResultFromBrokerResult(brokerResult) ); // Set broker performance metrics if available final BrokerPerformanceMetrics metrics = resultAdapter.getBrokerPerformanceMetricsFromBundle(resultBundle); @@ -1038,6 +1083,13 @@ AcquireTokenResult getAcquireTokenResultFromResultBundle(@NonNull final Bundle r resultBundle.getString(AuthenticationConstants.Broker.BROKER_PACKAGE_NAME) ); } + + // Set onboarding telemetry blob if present (best-effort; never fails the result). + final String onboardingBlob = resultAdapter.getOnboardingBlobFromBundle(brokerResult); + if (onboardingBlob != null) { + acquireTokenResult.setOnboardingBlob(onboardingBlob); + } + return acquireTokenResult; } diff --git a/common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerRequestAdapterTests.java b/common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerRequestAdapterTests.java index 11a9c9580f..ff9f81a36a 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerRequestAdapterTests.java +++ b/common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerRequestAdapterTests.java @@ -386,4 +386,73 @@ public void testGetRequestBundleForAadDeviceIdRequest() { assertEquals(mockRedirectUri, brokerRequest.getRedirect()); assertEquals(mockTenantId, brokerRequest.getTenantId()); } + + /** + * Verify that {@code onboardingSeedJson} from {@link InteractiveTokenCommandParameters} + * is propagated into {@link BrokerRequest} by + * {@link MsalBrokerRequestAdapter#brokerRequestFromAcquireTokenParameters(InteractiveTokenCommandParameters)}. + */ + @Test + public void test_brokerRequestFromAcquireTokenParameters_PropagatesOnboardingSeedJson() { + final String seedJson = "{\"schema_version\":\"1.0.0\"," + + "\"session_correlation_id\":\"abc-123\"," + + "\"onboarding_mode\":\"brokered\"}"; + final Set scopes = new HashSet<>(); + scopes.add("user.read"); + + final IPlatformComponents components = MockPlatformComponentsFactory.getNonFunctionalBuilder().build(); + final AndroidInteractiveTokenCommandParameters params = AndroidInteractiveTokenCommandParameters.builder() + .platformComponents(components) + .correlationId("987d8962-3f4d-4054-a852-ac0c4b6a602e") + .clientId("aClientId") + .redirectUri("msauth://com.example/foo") + .applicationName("com.example") + .applicationVersion("1.0.0") + .sdkType(SdkType.MSAL) + .sdkVersion("5.4.0") + .authority(new AzureActiveDirectoryAuthority()) + .scopes(scopes) + .authenticationScheme(new BearerAuthenticationSchemeInternal()) + .prompt(OpenIdConnectPromptParameter.LOGIN) + .requiredBrokerProtocolVersion("10.0") + .onboardingSeedJson(seedJson) + .build(); + + final BrokerRequest brokerRequest = + new MsalBrokerRequestAdapter().brokerRequestFromAcquireTokenParameters(params); + + assertEquals(seedJson, brokerRequest.getOnboardingSeedJson()); + } + + /** + * Verify that when {@code onboardingSeedJson} is not set on the parameters, + * the resulting {@link BrokerRequest} carries a null seed (i.e. no accidental default value). + */ + @Test + public void test_brokerRequestFromAcquireTokenParameters_NoSeedJson_IsNull() { + final Set scopes = new HashSet<>(); + scopes.add("user.read"); + + final IPlatformComponents components = MockPlatformComponentsFactory.getNonFunctionalBuilder().build(); + final AndroidInteractiveTokenCommandParameters params = AndroidInteractiveTokenCommandParameters.builder() + .platformComponents(components) + .correlationId("987d8962-3f4d-4054-a852-ac0c4b6a602e") + .clientId("aClientId") + .redirectUri("msauth://com.example/foo") + .applicationName("com.example") + .applicationVersion("1.0.0") + .sdkType(SdkType.MSAL) + .sdkVersion("5.4.0") + .authority(new AzureActiveDirectoryAuthority()) + .scopes(scopes) + .authenticationScheme(new BearerAuthenticationSchemeInternal()) + .prompt(OpenIdConnectPromptParameter.LOGIN) + .requiredBrokerProtocolVersion("10.0") + .build(); + + final BrokerRequest brokerRequest = + new MsalBrokerRequestAdapter().brokerRequestFromAcquireTokenParameters(params); + + assertNull(brokerRequest.getOnboardingSeedJson()); + } } diff --git a/common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerResultAdapterTests.kt b/common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerResultAdapterTests.kt index 09c720ab7a..768333b324 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerResultAdapterTests.kt +++ b/common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerResultAdapterTests.kt @@ -658,4 +658,34 @@ class MsalBrokerResultAdapterTests { resultString.contains(SchemaUtil.MISSING_FROM_THE_TOKEN_RESPONSE) ) } + + @Test + fun testOnboardingBlob_RoundTripsThroughBundle() { + val blobJson = """{"schema_version":"1.0.0","session_correlation_id":"abc-123","onboarding_mode":"brokered","blocking_errors":["BROKER_INSTALLATION_TRIGGERED"]}""" + val brokerResult = BrokerResult.Builder() + .clientId("aClientId") + .correlationId("987d8962-3f4d-4054-a852-ac0c4b6a602e") + .onboardingBlob(blobJson) + .build() + + val adapter = getInstance() + val resultBundle = adapter.bundleFromBrokerResult(brokerResult, "10.0") + val deserialized = adapter.brokerResultFromBundle(resultBundle) + + assertEquals(blobJson, deserialized.onboardingBlob) + } + + @Test + fun testOnboardingBlob_NotSet_DeserializesAsNull() { + val brokerResult = BrokerResult.Builder() + .clientId("aClientId") + .correlationId("987d8962-3f4d-4054-a852-ac0c4b6a602e") + .build() + + val adapter = getInstance() + val resultBundle = adapter.bundleFromBrokerResult(brokerResult, "10.0") + val deserialized = adapter.brokerResultFromBundle(resultBundle) + + assertNull(deserialized.onboardingBlob) + } } \ No newline at end of file diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/InteractiveTokenCommandParameters.java b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/InteractiveTokenCommandParameters.java index e31b9c7e40..22b40fbd2f 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/InteractiveTokenCommandParameters.java +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/InteractiveTokenCommandParameters.java @@ -38,6 +38,8 @@ import lombok.Getter; import lombok.experimental.SuperBuilder; +import edu.umd.cs.findbugs.annotations.Nullable; + @Getter @EqualsAndHashCode(callSuper = true) @SuperBuilder(toBuilder = true) @@ -85,6 +87,13 @@ public class InteractiveTokenCommandParameters extends TokenCommandParameters { */ private final boolean suppressBrokerAccountPicker; + /** + * Onboarding telemetry seed JSON blob. + * Passed through IPC to the broker for step recording and blocking error tracking. + */ + @Nullable + private final String onboardingSeedJson; + public boolean getHandleNullTaskAffinity(){ return handleNullTaskAffinity; } diff --git a/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java b/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java index 28fd66a2a3..5dd7d2622e 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java +++ b/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java @@ -49,6 +49,21 @@ public class AcquireTokenResult implements IBrokerPerformanceMetricsProvider, IB private BrokerPerformanceMetrics mBrokerPerformanceMetrics; + /** + * Populated onboarding telemetry blob JSON returned by the broker. + */ + @Nullable + private String mOnboardingBlob; + + public void setOnboardingBlob(@Nullable final String onboardingBlob) { + this.mOnboardingBlob = onboardingBlob; + } + + @Nullable + public String getOnboardingBlob() { + return this.mOnboardingBlob; + } + public void setLocalAuthenticationResult(ILocalAuthenticationResult result) { this.mLocalAuthenticationResult = result; this.mSucceeded = true; diff --git a/common4j/src/test/com/microsoft/identity/common/java/commands/parameters/InteractiveTokenCommandParametersTest.java b/common4j/src/test/com/microsoft/identity/common/java/commands/parameters/InteractiveTokenCommandParametersTest.java new file mode 100644 index 0000000000..e8b47372d0 --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/commands/parameters/InteractiveTokenCommandParametersTest.java @@ -0,0 +1,57 @@ +// 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.commands.parameters; + +import static org.mockito.Mockito.mock; + +import com.microsoft.identity.common.java.interfaces.IPlatformComponents; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for {@link InteractiveTokenCommandParameters} accessors, including the onboarding + * telemetry seed JSON field carried into the broker IPC request bundle. + */ +public class InteractiveTokenCommandParametersTest { + + @Test + public void onboardingSeedJson_DefaultsToNull() { + final InteractiveTokenCommandParameters params = InteractiveTokenCommandParameters.builder() + .platformComponents(mock(IPlatformComponents.class)) + .build(); + Assert.assertNull(params.getOnboardingSeedJson()); + } + + @Test + public void onboardingSeedJson_RoundTripsThroughBuilder() { + final String seedJson = "{\"schema_version\":\"1.0.0\"," + + "\"session_correlation_id\":\"abc-123\"," + + "\"onboarding_mode\":\"brokered\"}"; + final InteractiveTokenCommandParameters params = InteractiveTokenCommandParameters.builder() + .platformComponents(mock(IPlatformComponents.class)) + .onboardingSeedJson(seedJson) + .build(); + Assert.assertEquals(seedJson, params.getOnboardingSeedJson()); + } +} diff --git a/common4j/src/test/com/microsoft/identity/common/java/result/AcquireTokenResultTest.java b/common4j/src/test/com/microsoft/identity/common/java/result/AcquireTokenResultTest.java new file mode 100644 index 0000000000..dd0bd3d9ab --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/result/AcquireTokenResultTest.java @@ -0,0 +1,57 @@ +// 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.result; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for {@link AcquireTokenResult} accessors, including the onboarding telemetry blob + * field used to convey broker-side onboarding telemetry back to the client. + */ +public class AcquireTokenResultTest { + + @Test + public void onboardingBlob_DefaultsToNull() { + final AcquireTokenResult result = new AcquireTokenResult(); + Assert.assertNull(result.getOnboardingBlob()); + } + + @Test + public void onboardingBlob_RoundTripsThroughSetter() { + final String blobJson = "{\"schema_version\":\"1.0.0\"," + + "\"session_correlation_id\":\"abc-123\"," + + "\"onboarding_mode\":\"brokered\"}"; + final AcquireTokenResult result = new AcquireTokenResult(); + result.setOnboardingBlob(blobJson); + Assert.assertEquals(blobJson, result.getOnboardingBlob()); + } + + @Test + public void onboardingBlob_NullSetterClearsValue() { + final AcquireTokenResult result = new AcquireTokenResult(); + result.setOnboardingBlob("non-null"); + result.setOnboardingBlob(null); + Assert.assertNull(result.getOnboardingBlob()); + } +}