Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
vNext
----------

Version 24.2.1-RC1
----------

Version 24.2.1-RC1
----------
- [MINOR] Add additional step ID and blocking error constants for full onboarding telemetry coverage (#3117)
- [MINOR] Add onboarding telemetry blob fields to BrokerRequest/BrokerResult and command parameters for client↔broker IPC transport (#3111)
- [MINOR] Add onboarding telemetry recorder, field keys, and session persistence for mobile onboarding flow (#3088)

Version 24.2.0
----------
- [PATCH] Add support for Authenticator app activation links in WebView, enabling account pairing/MFA flows to launch Microsoft Authenticator directly instead of redirecting to the Play Store (#3090)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}

/**
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
Expand All @@ -523,6 +537,7 @@ public static class Builder {
private List<ICacheRecord> mTenantProfileData;
private boolean mServicedFromCache;
private AadDeviceIdRecord mAadDeviceIdRecord;
private String mOnboardingBlob;

// Exception parameters
private String mErrorCode;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ICacheRecord> tenantProfileCacheRecords = brokerResult.getTenantProfileData();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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.telemetry

import android.content.Context

/**
* SharedPreferences-backed persistence for session correlation IDs.
* Used by OneAuth (via JNI/Djinni SessionCachePersistence adapter).
* Each app (OneAuth host, broker) has its own sandboxed SharedPreferences file;
* the same schema and file name are used across apps for consistency.
* OnboardingTelemetryRecorder also writes to this file on block detection.
*/
class OnboardingSessionCorrelationStore(context: Context) {

private val appContext: Context = context.applicationContext

/**
* Load the persisted session correlation cache JSON string.
* @return JSON string, or empty string if nothing is persisted
*/
fun load(): String {
val prefs = appContext.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)
return prefs.getString(PREFS_FILE, "") ?: ""
}

/**
* Save the session correlation cache JSON string to SharedPreferences.
* @param json The JSON string to persist
*/
fun save(json: String) {
val prefs = appContext.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)
prefs.edit().putString(PREFS_FILE, json).apply()
}

companion object {
private const val PREFS_FILE = "com.microsoft.oneauth.session_correlation_cache"
}
}
Loading
Loading