Skip to content

Commit fdae003

Browse files
fadidurahCopilot
andcommitted
Wire ClientDataInfo through AcquireTokenResult and BaseException
Propagate ClientDataInfo (parsed from the x-ms-clientdata token response header and the clientdata authorize redirect query parameter) through AcquireTokenResult on success paths and through BaseException on failure paths. Includes broker IPC serialization via BrokerResult so the payload reaches the MSAL caller process for both success and error responses. Adds a raw field to ClientDataInfo preserving the original pipe-delimited string for partner teams that want the unparsed payload. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 16b1bc8 commit fdae003

17 files changed

Lines changed: 372 additions & 8 deletions

File tree

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
vNext
22
----------
3+
- [PATCH] Wire ClientDataInfo through AcquireTokenResult, exceptions (#3109)
34
- [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)
45
- [PATCH] Move Multiple Listening apps check to the authorization layer (#3070)
56
- [PATCH] Edge TB: Fix lookup mode (#3108)

common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerResult.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ private static class SerializedNames {
9797
static final String SPE_RING = "spe_ring";
9898
static final String CLI_TELEM_ERRORCODE = "cli_telem_error_code";
9999
static final String CLI_TELEM_SUB_ERROR_CODE = "cli_telem_suberror_code";
100+
static final String CLIENT_DATA_INFO = "client_data_info";
100101
}
101102

102103
private static final long serialVersionUID = 8606631820514878489L;
@@ -236,6 +237,13 @@ private static class SerializedNames {
236237
@SerializedName(SerializedNames.REFRESH_TOKEN_AGE)
237238
private String mRefreshTokenAge;
238239

240+
/**
241+
* Server client data info from x-ms-clientdata response header (pipe-delimited format).
242+
*/
243+
@Nullable
244+
@SerializedName(SerializedNames.CLIENT_DATA_INFO)
245+
private String mClientDataInfoRaw;
246+
239247
/**
240248
* Boolean to indicate if the request succeeded without exceptions.
241249
*/
@@ -347,6 +355,7 @@ private BrokerResult(@NonNull final Builder builder) {
347355
mCachedAt = builder.mCachedAt;
348356
mSpeRing = builder.mSpeRing;
349357
mRefreshTokenAge = builder.mRefreshTokenAge;
358+
mClientDataInfoRaw = builder.mClientDataInfoRaw;
350359
mSuccess = builder.mSuccess;
351360
mTenantProfileData = builder.mTenantProfileData;
352361
mServicedFromCache = builder.mServicedFromCache;
@@ -426,6 +435,11 @@ public String getSpeRing() {
426435
return mSpeRing;
427436
}
428437

438+
@Nullable
439+
public String getClientDataInfoRaw() {
440+
return mClientDataInfoRaw;
441+
}
442+
429443
public long getCachedAt() {
430444
return mCachedAt;
431445
}
@@ -518,6 +532,7 @@ public static class Builder {
518532
private long mCachedAt;
519533
private String mSpeRing;
520534
private String mRefreshTokenAge;
535+
private String mClientDataInfoRaw;
521536
private boolean mSuccess;
522537
private String mNegotiatedBrokerProtocolVersion;
523538
private List<ICacheRecord> mTenantProfileData;
@@ -631,6 +646,11 @@ public Builder refreshTokenAge(final String refreshTokenAge) {
631646
return this;
632647
}
633648

649+
public Builder clientDataInfoRaw(@Nullable final String clientDataInfoRaw) {
650+
this.mClientDataInfoRaw = clientDataInfoRaw;
651+
return this;
652+
}
653+
634654
public Builder success(boolean success) {
635655
this.mSuccess = success;
636656
return this;

common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import com.microsoft.identity.common.java.providers.RawAuthorizationResult;
6363
import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationRequest;
6464
import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationResponse;
65+
import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationResult;
6566
import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsTokenRequest;
6667
import com.microsoft.identity.common.java.providers.oauth2.AuthorizationRequest;
6768
import com.microsoft.identity.common.java.providers.oauth2.AuthorizationResult;
@@ -168,6 +169,13 @@ public AcquireTokenResult acquireToken(
168169
);
169170
acquireTokenResult.setAuthorizationResult(result);
170171

172+
// Wire ClientDataInfo from the authorization result (authorize endpoint).
173+
if (result instanceof MicrosoftStsAuthorizationResult) {
174+
acquireTokenResult.setClientDataInfo(
175+
((MicrosoftStsAuthorizationResult) result).getClientDataInfo()
176+
);
177+
}
178+
171179
ResultUtil.logResult(TAG, result);
172180

173181
if (result.getAuthorizationStatus().equals(AuthorizationStatus.SUCCESS)) {
@@ -181,6 +189,11 @@ public AcquireTokenResult acquireToken(
181189

182190
acquireTokenResult.setTokenResult(tokenResult);
183191

192+
// Prefer ClientDataInfo from the token endpoint (later, more authoritative call).
193+
if (tokenResult != null && tokenResult.getClientDataInfo() != null) {
194+
acquireTokenResult.setClientDataInfo(tokenResult.getClientDataInfo());
195+
}
196+
184197
if (tokenResult != null && tokenResult.getSuccess()) {
185198
//4) Save tokens in token cache
186199
final List<ICacheRecord> records = saveTokens(
@@ -205,6 +218,11 @@ public AcquireTokenResult acquireToken(
205218
false
206219
)
207220
);
221+
222+
// Set ClientDataInfo on the LocalAuthenticationResult for IPC propagation
223+
final LocalAuthenticationResult localResult =
224+
(LocalAuthenticationResult) acquireTokenResult.getLocalAuthenticationResult();
225+
localResult.setClientDataInfo(acquireTokenResult.getClientDataInfo());
208226
}
209227
}
210228

@@ -742,6 +760,9 @@ public AcquireTokenResult acquireDeviceCodeFlowToken(
742760

743761
// Assign token result
744762
acquireTokenResult.setTokenResult(tokenResult);
763+
if (tokenResult != null) {
764+
acquireTokenResult.setClientDataInfo(tokenResult.getClientDataInfo());
765+
}
745766

746767
// If the token is valid, save it into token cache
747768
final List<ICacheRecord> records = saveTokens(
@@ -764,6 +785,13 @@ public AcquireTokenResult acquireDeviceCodeFlowToken(
764785
false
765786
)
766787
);
788+
789+
// Set ClientDataInfo on the LocalAuthenticationResult for IPC propagation
790+
if (tokenResult != null) {
791+
final LocalAuthenticationResult localResult =
792+
(LocalAuthenticationResult) acquireTokenResult.getLocalAuthenticationResult();
793+
localResult.setClientDataInfo(tokenResult.getClientDataInfo());
794+
}
767795
} catch (Exception error) {
768796
Telemetry.emit(
769797
new ApiEndEvent()

common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
import com.microsoft.identity.common.java.result.GenerateShrResult;
9999
import com.microsoft.identity.common.java.result.ILocalAuthenticationResult;
100100
import com.microsoft.identity.common.java.result.LocalAuthenticationResult;
101+
import com.microsoft.identity.common.java.telemetry.ClientDataInfo;
101102
import com.microsoft.identity.common.java.ui.PreferredAuthMethod;
102103
import com.microsoft.identity.common.java.util.BrokerProtocolVersionUtil;
103104
import com.microsoft.identity.common.java.util.HeaderSerializationUtil;
@@ -284,6 +285,17 @@ public Bundle bundleFromAuthenticationResultForWebApps(@NonNull final ILocalAuth
284285
.success(true)
285286
.servicedFromCache(authenticationResult.isServicedFromCache());
286287

288+
// Serialize ClientDataInfo as raw pipe-delimited string for IPC transfer.
289+
// The raw field is populated by ClientDataInfo.fromPipeDelimited(), the only
290+
// path that populates the parsed fields, so it is safe to ship as-is.
291+
if (authenticationResult instanceof LocalAuthenticationResult) {
292+
final ClientDataInfo clientDataInfo =
293+
((LocalAuthenticationResult) authenticationResult).getClientDataInfo();
294+
if (clientDataInfo != null) {
295+
brokerResultBuilder.clientDataInfoRaw(clientDataInfo.getRaw());
296+
}
297+
}
298+
287299
if (shouldRemoveRefreshTokenFromResult(authenticationResult, negotiatedBrokerProtocolVersion)){
288300
brokerResultBuilder.tenantProfileRecords(
289301
removeRefreshTokenFromCacheRecords(
@@ -411,6 +423,12 @@ public Bundle bundleFromBaseException(@NonNull final BaseException exception,
411423
.speRing(exception.getSpeRing())
412424
.refreshTokenAge(exception.getRefreshTokenAge());
413425

426+
// Serialize ClientDataInfo (server telemetry from x-ms-clientdata) so it
427+
// survives the broker IPC boundary on error paths.
428+
if (exception.getClientDataInfo() != null) {
429+
builder.clientDataInfoRaw(exception.getClientDataInfo().getRaw());
430+
}
431+
414432
if (exception instanceof ServiceException) {
415433
final ServiceException serviceException = (ServiceException) exception;
416434
builder.subErrorCode(serviceException.getSubErrorCode())
@@ -476,12 +494,21 @@ public ILocalAuthenticationResult authenticationResultFromBundle(@NonNull final
476494
throw new ClientException(INVALID_BROKER_BUNDLE, "getTenantProfileData is null.");
477495
}
478496

479-
return new LocalAuthenticationResult(
497+
final LocalAuthenticationResult localAuthResult = new LocalAuthenticationResult(
480498
tenantProfileCacheRecords.get(0),
481499
tenantProfileCacheRecords,
482500
SdkType.MSAL,
483501
brokerResult.isServicedFromCache()
484502
);
503+
504+
// Deserialize ClientDataInfo from the broker result if available
505+
final ClientDataInfo clientDataInfo =
506+
ClientDataInfo.fromPipeDelimited(brokerResult.getClientDataInfoRaw());
507+
if (clientDataInfo != null) {
508+
localAuthResult.setClientDataInfo(clientDataInfo);
509+
}
510+
511+
return localAuthResult;
485512
}
486513

487514
@NonNull
@@ -516,6 +543,14 @@ public BaseException getBaseExceptionFromBundle(@NonNull final Bundle resultBund
516543
baseException.setBrokerPerformanceMetrics(metrics);
517544
}
518545

546+
// Restore ClientDataInfo (server telemetry) from the broker result so callers
547+
// catching the exception can inspect server-side error context.
548+
if (!StringUtil.isNullOrEmpty(brokerResult.getClientDataInfoRaw())) {
549+
baseException.setClientDataInfo(
550+
ClientDataInfo.fromPipeDelimited(brokerResult.getClientDataInfoRaw())
551+
);
552+
}
553+
519554
// Set broker app info if available
520555
if (resultBundle.containsKey(AuthenticationConstants.Broker.BROKER_VERSION)) {
521556
baseException.setBrokerAppVersion(
@@ -994,7 +1029,16 @@ public AcquireTokenResult getDeviceCodeFlowTokenResultFromResultBundle(@NonNull
9941029

9951030
if (resultBundle.getBoolean(AuthenticationConstants.Broker.BROKER_REQUEST_V2_SUCCESS)) {
9961031
final AcquireTokenResult acquireTokenResult = new AcquireTokenResult();
997-
acquireTokenResult.setLocalAuthenticationResult(authenticationResultFromBundle(resultBundle));
1032+
final ILocalAuthenticationResult authResult = authenticationResultFromBundle(resultBundle);
1033+
acquireTokenResult.setLocalAuthenticationResult(authResult);
1034+
1035+
// Propagate ClientDataInfo from LocalAuthenticationResult to AcquireTokenResult
1036+
if (authResult instanceof LocalAuthenticationResult) {
1037+
acquireTokenResult.setClientDataInfo(
1038+
((LocalAuthenticationResult) authResult).getClientDataInfo()
1039+
);
1040+
}
1041+
9981042
span.setStatus(StatusCode.OK);
9991043
return acquireTokenResult;
10001044
} else if (brokerResult.getErrorCode().equals(ErrorStrings.DEVICE_CODE_FLOW_AUTHORIZATION_PENDING_ERROR_CODE)) {
@@ -1018,9 +1062,16 @@ AcquireTokenResult getAcquireTokenResultFromResultBundle(@NonNull final Bundle r
10181062
final MsalBrokerResultAdapter resultAdapter = new MsalBrokerResultAdapter();
10191063
if (resultBundle.getBoolean(AuthenticationConstants.Broker.BROKER_REQUEST_V2_SUCCESS)) {
10201064
final AcquireTokenResult acquireTokenResult = new AcquireTokenResult();
1021-
acquireTokenResult.setLocalAuthenticationResult(
1022-
resultAdapter.authenticationResultFromBundle(resultBundle)
1023-
);
1065+
final ILocalAuthenticationResult authResult =
1066+
resultAdapter.authenticationResultFromBundle(resultBundle);
1067+
acquireTokenResult.setLocalAuthenticationResult(authResult);
1068+
1069+
// Propagate ClientDataInfo from LocalAuthenticationResult to AcquireTokenResult
1070+
if (authResult instanceof LocalAuthenticationResult) {
1071+
acquireTokenResult.setClientDataInfo(
1072+
((LocalAuthenticationResult) authResult).getClientDataInfo()
1073+
);
1074+
}
10241075
// Set broker performance metrics if available
10251076
final BrokerPerformanceMetrics metrics = resultAdapter.getBrokerPerformanceMetricsFromBundle(resultBundle);
10261077
if (metrics != null) {

common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerResultAdapterTests.kt

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.microsoft.identity.common.java.exception.ClientException
3636
import com.microsoft.identity.common.java.exception.UiRequiredException
3737
import com.microsoft.identity.common.java.request.SdkType
3838
import com.microsoft.identity.common.java.result.LocalAuthenticationResult
39+
import com.microsoft.identity.common.java.telemetry.ClientDataInfo
3940
import com.microsoft.identity.common.java.util.SchemaUtil
4041
import com.microsoft.identity.internal.testutils.MockRecords
4142
import lombok.SneakyThrows
@@ -658,4 +659,81 @@ class MsalBrokerResultAdapterTests {
658659
resultString.contains(SchemaUtil.MISSING_FROM_THE_TOKEN_RESPONSE)
659660
)
660661
}
662+
663+
// ==================== ClientDataInfo IPC round-trip tests (PR #3109) ====================
664+
665+
private val clientDataRaw = "m|AADSTS50058|login_required|us|public"
666+
667+
private fun newCacheRecord() = CacheRecord.builder()
668+
.account(MockRecords.getMockAccountRecord_AAD())
669+
.idToken(MockRecords.getMockIdTokenRecord_AAD())
670+
.accessToken(MockRecords.getMockAccessTokenRecord_AAD())
671+
.refreshToken(MockRecords.getMockRefreshTokenRecord_AAD())
672+
.build()
673+
674+
@Test
675+
fun testClientDataInfo_RoundTripsThroughBrokerResult_OnSuccess() {
676+
val cacheRecord = newCacheRecord()
677+
val cacheRecords: MutableList<ICacheRecord> = arrayListOf(cacheRecord)
678+
val authResult = LocalAuthenticationResult(cacheRecord, cacheRecords, SdkType.MSAL, false)
679+
authResult.clientDataInfo = ClientDataInfo.fromPipeDelimited(clientDataRaw)
680+
681+
val brokerResult = getInstance().buildBrokerResultFromAuthenticationResult(authResult, "16.0")
682+
assertEquals("Raw payload should be serialized into BrokerResult", clientDataRaw, brokerResult.clientDataInfoRaw)
683+
}
684+
685+
@Test
686+
fun testClientDataInfo_NullOnLocalAuthResult_ResultsInNullOnBrokerResult() {
687+
val cacheRecord = newCacheRecord()
688+
val cacheRecords: MutableList<ICacheRecord> = arrayListOf(cacheRecord)
689+
val authResult = LocalAuthenticationResult(cacheRecord, cacheRecords, SdkType.MSAL, false)
690+
// No ClientDataInfo set
691+
692+
val brokerResult = getInstance().buildBrokerResultFromAuthenticationResult(authResult, "16.0")
693+
assertNull(brokerResult.clientDataInfoRaw)
694+
}
695+
696+
@Test
697+
fun testClientDataInfo_RoundTripsThroughBaseExceptionBundle() {
698+
val exception = ClientException("invalid_grant", "token failure")
699+
exception.clientDataInfo = ClientDataInfo.fromPipeDelimited(clientDataRaw)
700+
701+
val resultAdapter = MsalBrokerResultAdapter()
702+
val resultBundle = resultAdapter.bundleFromBaseException(exception, null)
703+
val brokerResult = resultAdapter.brokerResultFromBundle(resultBundle)
704+
assertEquals(clientDataRaw, brokerResult.clientDataInfoRaw)
705+
706+
val received = resultAdapter.getBaseExceptionFromBundle(resultBundle)
707+
assertNotNull("ClientDataInfo should be reconstructed on the exception", received.clientDataInfo)
708+
assertEquals("AADSTS50058", received.clientDataInfo!!.error)
709+
assertEquals("login_required", received.clientDataInfo!!.subError)
710+
assertEquals(clientDataRaw, received.clientDataInfo!!.raw)
711+
}
712+
713+
@Test
714+
fun testClientDataInfo_NullOnException_NotInBundle() {
715+
val exception = ClientException("invalid_grant", "token failure")
716+
// No ClientDataInfo set
717+
718+
val resultAdapter = MsalBrokerResultAdapter()
719+
val resultBundle = resultAdapter.bundleFromBaseException(exception, null)
720+
val received = resultAdapter.getBaseExceptionFromBundle(resultBundle)
721+
assertNull(received.clientDataInfo)
722+
}
723+
724+
@Test
725+
fun testClientDataInfo_RoundTripsThroughGetAcquireTokenResultFromResultBundle() {
726+
val cacheRecord = newCacheRecord()
727+
val cacheRecords: MutableList<ICacheRecord> = arrayListOf(cacheRecord)
728+
val authResult = LocalAuthenticationResult(cacheRecord, cacheRecords, SdkType.MSAL, false)
729+
authResult.clientDataInfo = ClientDataInfo.fromPipeDelimited(clientDataRaw)
730+
731+
val resultAdapter = MsalBrokerResultAdapter()
732+
val resultBundle = resultAdapter.bundleFromAuthenticationResult(authResult, "16.0")
733+
734+
val acquireTokenResult = resultAdapter.getAcquireTokenResultFromResultBundle(resultBundle)
735+
assertNotNull("ClientDataInfo should be present on AcquireTokenResult", acquireTokenResult.clientDataInfo)
736+
assertEquals("AADSTS50058", acquireTokenResult.clientDataInfo!!.error)
737+
assertEquals(clientDataRaw, acquireTokenResult.clientDataInfo!!.raw)
738+
}
661739
}

common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ public AcquireTokenResult acquireTokenWithPassword(@NonNull final RopcTokenComma
219219
final TokenResult tokenResult = oAuth2Strategy.requestToken(ropcTokenRequest);
220220

221221
acquireTokenResult.setTokenResult(tokenResult);
222+
if (tokenResult != null) {
223+
acquireTokenResult.setClientDataInfo(tokenResult.getClientDataInfo());
224+
}
222225

223226
@SuppressWarnings(WarningType.rawtype_warning) final OAuth2TokenCache tokenCache = parameters.getOAuth2TokenCache();
224227

@@ -251,6 +254,9 @@ public AcquireTokenResult acquireTokenWithPassword(@NonNull final RopcTokenComma
251254
Telemetry.emit(new CacheEndEvent());
252255
}
253256

257+
// Set server client data info on the authentication result for IPC propagation
258+
authenticationResult.setClientDataInfo(tokenResult.getClientDataInfo());
259+
254260
// Set the AuthenticationResult on the final result object
255261
acquireTokenResult.setLocalAuthenticationResult(authenticationResult);
256262
}
@@ -484,6 +490,7 @@ protected void renewAccessToken(@NonNull final SilentTokenCommandParameters para
484490
);
485491

486492
acquireTokenSilentResult.setTokenResult(tokenResult);
493+
acquireTokenSilentResult.setClientDataInfo(tokenResult.getClientDataInfo());
487494

488495
ResultUtil.logResult(methodTag, tokenResult);
489496

@@ -528,6 +535,9 @@ protected void renewAccessToken(@NonNull final SilentTokenCommandParameters para
528535
Telemetry.emit(new CacheEndEvent());
529536
}
530537

538+
// Set server client data info on the authentication result for IPC propagation
539+
authenticationResult.setClientDataInfo(tokenResult.getClientDataInfo());
540+
531541
// Set the AuthenticationResult on the final result object
532542
acquireTokenSilentResult.setLocalAuthenticationResult(authenticationResult);
533543
} else {

0 commit comments

Comments
 (0)