Skip to content

Resource Account API only, Fixes AB#3230428 #2640

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 19, 2025
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
vNext
----------
- [MINOR] Add API for resource account provisioning (API only) (#2640)

Version 21.1.0
----------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1626,7 +1626,8 @@ public enum API {
BROKER_RESTORE_MSA_ACCOUNTS_WITH_TRANSFER_TOKENS(BROKER_RESTORE_MSA_ACCOUNTS_WITH_TRANSFER_TOKENS_PATH, BROKER_VERSION_5, null),

WEBAPPS_GET_SUPPORTED_WEB_APPS_CONTRACTS(WEBAPPS_GET_SUPPORTED_WEB_APPS_CONTRACTS_PATH, null, null),
WEBAPPS_EXECUTE_WEB_APPS_REQUEST(WEBAPPS_EXECUTE_WEB_APPS_REQUEST_PATH, null, null);
WEBAPPS_EXECUTE_WEB_APPS_REQUEST(WEBAPPS_EXECUTE_WEB_APPS_REQUEST_PATH, null, null),
PROVISION_RESOURCE_ACCOUNT(PROVISION_RESOURCE_ACCOUNT_PATH, null, null);

/**
* The content provider path that the API exists behind.
Expand Down Expand Up @@ -1805,6 +1806,11 @@ public String getMsalVersion(){
*/
public static final String WEBAPPS_EXECUTE_WEB_APPS_REQUEST_PATH = "/webapp/executeWebAppsRequest";


public static final String PROVISION_RESOURCE_ACCOUNT_PATH = "/provisionResourceAccount";

public static final String GET_AAD_DEVICE_ID_PATH = "/getAadDeviceId";

/**
* BrokerContentProvider URI code constant for MSAL-to-Broker hello request.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ private static final class SerializedNames {
* Scopes for the request. This is expected to be of the format
* "scope 1 scope2 scope3" with space as a delimiter
*/
@NonNull
@SerializedName(SerializedNames.SCOPE)
private String mScope;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ public enum Operation {
BROKER_INDIVIDUAL_LOGS_UPLOAD(API.BROKER_INDIVIDUAL_LOGS_UPLOAD, null),
BROKER_API_RESTORE_MSA_ACCOUNTS_WITH_TRANSFER_TOKENS(API.BROKER_RESTORE_MSA_ACCOUNTS_WITH_TRANSFER_TOKENS, null),
BROKER_WEBAPPS_API_GET_SUPPORTED_WEB_APPS_CONTRACTS(API.WEBAPPS_GET_SUPPORTED_WEB_APPS_CONTRACTS, null),
BROKER_WEBAPPS_API_EXECUTE_WEB_APPS_REQUEST(API.WEBAPPS_EXECUTE_WEB_APPS_REQUEST, null);
BROKER_WEBAPPS_API_EXECUTE_WEB_APPS_REQUEST(API.WEBAPPS_EXECUTE_WEB_APPS_REQUEST, null),
PROVISION_RESOURCE_ACCOUNT(API.PROVISION_RESOURCE_ACCOUNT, null);

final API mContentApi;
final String mAccountManagerOperation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import static com.microsoft.identity.common.internal.broker.ipc.BrokerOperationBundle.Operation.MSAL_REMOVE_ACCOUNT;
import static com.microsoft.identity.common.internal.broker.ipc.BrokerOperationBundle.Operation.MSAL_SIGN_OUT_FROM_SHARED_DEVICE;
import static com.microsoft.identity.common.internal.broker.ipc.BrokerOperationBundle.Operation.MSAL_SSO_TOKEN;
import static com.microsoft.identity.common.internal.broker.ipc.BrokerOperationBundle.Operation.PROVISION_RESOURCE_ACCOUNT;
import static com.microsoft.identity.common.internal.controllers.BrokerOperationExecutor.BrokerOperation;
import static com.microsoft.identity.common.java.AuthenticationConstants.LocalBroadcasterAliases.RETURN_BROKER_INTERACTIVE_ACQUIRE_TOKEN_RESULT;
import static com.microsoft.identity.common.java.AuthenticationConstants.LocalBroadcasterFields.REQUEST_CODE;
Expand Down Expand Up @@ -86,6 +87,7 @@
import com.microsoft.identity.common.java.commands.parameters.GenerateShrCommandParameters;
import com.microsoft.identity.common.java.commands.parameters.InteractiveTokenCommandParameters;
import com.microsoft.identity.common.java.commands.parameters.RemoveAccountCommandParameters;
import com.microsoft.identity.common.java.commands.parameters.ResourceAccountCommandParameters;
import com.microsoft.identity.common.java.commands.parameters.RopcTokenCommandParameters;
import com.microsoft.identity.common.java.commands.parameters.SilentTokenCommandParameters;
import com.microsoft.identity.common.java.commands.parameters.TokenCommandParameters;
Expand Down Expand Up @@ -1181,7 +1183,62 @@ public void putValueInSuccessEvent(@NonNull final ApiEndEvent event,
// TODO Needed?
}
});
}

/**
* Sign in a resource account in broker based on given parameters. Should called by OneAuth/MSAL
* to provision resource account in broker.
* @param parameters a {@link ResourceAccountCommandParameters}
*/
public ICacheRecord provisionResourceAccount(@NonNull final ResourceAccountCommandParameters parameters) throws BaseException {
return getBrokerOperationExecutor().execute(parameters,
new BrokerOperation<ICacheRecord>() {
private String negotiatedBrokerProtocolVersion;

@Override
public void performPrerequisites(final @NonNull IIpcStrategy strategy) throws BaseException {
negotiatedBrokerProtocolVersion = hello(strategy, parameters.getRequiredBrokerProtocolVersion());
}

@Override
@NonNull
public BrokerOperationBundle getBundle() {
return new BrokerOperationBundle(
PROVISION_RESOURCE_ACCOUNT,
mActiveBrokerPackageName,
mRequestAdapter.getRequestBundleForProvisionResourceAccount(
parameters,
negotiatedBrokerProtocolVersion
));
}

@Override
@NonNull
public ICacheRecord extractResultBundle(final @Nullable Bundle resultBundle) throws BaseException {
if (resultBundle == null) {
throw mResultAdapter.getExceptionForEmptyResultBundle();
}
verifyBrokerVersionIsSupported(resultBundle, parameters.getRequiredBrokerProtocolVersion());
return mResultAdapter.resourceAccountRecordFromBundle(resultBundle);
}

@Override
public @NonNull
String getMethodName() {
return ":provisionResourceAccount";
}

@Nullable
@Override
public String getTelemetryApiId() {
return null;
}

@Override
public void putValueInSuccessEvent(@lombok.NonNull ApiEndEvent event, @lombok.NonNull ICacheRecord result) {

}
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import com.microsoft.identity.common.java.commands.parameters.DeviceCodeFlowCommandParameters;
import com.microsoft.identity.common.java.commands.parameters.GenerateShrCommandParameters;
import com.microsoft.identity.common.java.commands.parameters.RemoveAccountCommandParameters;
import com.microsoft.identity.common.java.commands.parameters.ResourceAccountCommandParameters;
import com.microsoft.identity.common.java.opentelemetry.SerializableSpanContext;
import com.microsoft.identity.common.java.opentelemetry.SpanExtension;
import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationResult;
Expand Down Expand Up @@ -264,6 +265,50 @@ public Bundle getRequestBundleForAcquireTokenInteractive(@NonNull final Interact
);
}

/**
* Method to construct a request bundle for broker provisionResourceAccountRequest request.
* @param parameters input parameters of type {@link ResourceAccountCommandParameters}
* @param negotiatedBrokerProtocolVersion protocol version established by broker hello.
* @return request Bundle
*/
public Bundle getRequestBundleForProvisionResourceAccount(
@NonNull final ResourceAccountCommandParameters parameters,
@Nullable final String negotiatedBrokerProtocolVersion
) {
final String methodTag = TAG + ":getRequestBundleForProvisionResourceAccount";

Logger.info(methodTag, "Constructing result bundle from ProvisionResourceAccount.");
final String extraOptions = parameters.getExtraOptions() != null ?
QueryParamsAdapter._toJson(parameters.getExtraOptions()) : null;

final BrokerRequest brokerRequest = BrokerRequest.builder()
.authority(parameters.getAuthority().getAuthorityURL().toString())
.extraOptions(extraOptions)
.homeAccountId(parameters.getHomeAccountId())
.userName(parameters.getLoginHint())
.correlationId(parameters.getCorrelationId())
.clientId(parameters.getClientId())
.redirect(parameters.getRedirectUri())
.applicationName(parameters.getApplicationName())
.applicationVersion(parameters.getApplicationVersion())
.msalVersion(parameters.getSdkVersion())
.sdkType(parameters.getSdkType())
.environment(AzureActiveDirectory.getEnvironment().name())
.powerOptCheckEnabled(parameters.isPowerOptCheckEnabled())
.spanContext(SerializableSpanContext.builder()
.traceId(SpanExtension.current().getSpanContext().getTraceId())
.spanId(SpanExtension.current().getSpanContext().getSpanId())
.traceFlags(SpanExtension.current().getSpanContext().getTraceFlags().asByte())
.build()
)
.build();
return getRequestBundleFromBrokerRequest(
brokerRequest,
negotiatedBrokerProtocolVersion,
parameters.getRequiredBrokerProtocolVersion()
);
}

/**
* Method to construct a request bundle for broker deviceCodeFlowAuthRequest request.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,26 @@ public List<ICacheRecord> getAccountsFromResultBundle(@NonNull final Bundle bund
return JsonExtensions.getICacheRecordListFromJsonString(accountJson);
}

/**
* Get resource account record from the result bundle. If successful, new account
* record part of ICachedRecord is returned.
* @param bundle The result bundle from the broker.
* @throws BaseException
*/
public ICacheRecord resourceAccountRecordFromBundle(@NonNull final Bundle bundle) throws BaseException {
final String methodTag = TAG + ":resourceAccountRecordFromBundle";
final List<ICacheRecord> cacheRecords = getAccountsFromResultBundle(bundle);
if (cacheRecords.isEmpty()) {
Logger.error(methodTag, "No accounts found in the result bundle", null);
throw new ClientException(INVALID_BROKER_BUNDLE, "No accounts found in the result bundle");
}
if (cacheRecords.size() > 1) {
Logger.error(methodTag, "Multiple accounts found in the result bundle", null);
throw new ClientException(INVALID_BROKER_BUNDLE, "Multiple accounts found in the result bundle");
}
return cacheRecords.get(0);
}

public void verifyRemoveAccountResultFromBundle(@Nullable final Bundle bundle) throws BaseException {
final String methodTag = TAG + ":verifyRemoveAccountResultFromBundle";
if (bundle == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,27 @@
// THE SOFTWARE.
package com.microsoft.identity.common.internal.controllers;

import android.content.Context;
import android.os.Build;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.platform.app.InstrumentationRegistry;

import com.google.gson.Gson;
import com.microsoft.identity.common.adal.internal.AuthenticationConstants;
import com.microsoft.identity.common.components.MockPlatformComponentsFactory;
import com.microsoft.identity.common.exception.BrokerCommunicationException;
import com.microsoft.identity.common.internal.broker.ipc.BrokerOperationBundle;
import com.microsoft.identity.common.internal.broker.ipc.IIpcStrategy;
import com.microsoft.identity.common.internal.result.MsalBrokerResultAdapter;
import com.microsoft.identity.common.java.authorities.Authority;
import com.microsoft.identity.common.java.cache.CacheRecord;
import com.microsoft.identity.common.java.cache.ICacheRecord;
import com.microsoft.identity.common.java.commands.AcquirePrtSsoTokenResult;
import com.microsoft.identity.common.java.commands.parameters.AcquirePrtSsoTokenCommandParameters;
import com.microsoft.identity.common.java.commands.parameters.ResourceAccountCommandParameters;
import com.microsoft.identity.common.java.dto.AccountRecord;
import com.microsoft.identity.common.java.interfaces.IPlatformComponents;
import com.microsoft.identity.common.java.request.SdkType;

import org.junit.Assert;
import org.junit.Test;
Expand All @@ -47,7 +51,8 @@
import org.robolectric.annotation.Config;

import java.util.Collections;
import java.util.List;

import lombok.SneakyThrows;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = {Build.VERSION_CODES.N}, shadows = {})
Expand Down Expand Up @@ -125,4 +130,76 @@ public Type getType() {
Assert.assertEquals("x-ms-RefreshTokenCredential", ssoTokenResult.getCookieName());
}

/**
* This test simulates a result calling the ProvisionResourceAccount Api.
*/
@SneakyThrows
@Test
public void testProvisionResourceAccount() {
final String mockHomeAccountId = "mockHomeAccountId";
final String mockCorrelationId = "mockCorrelationId";
final String mockAuthorityStr = "https://login.microsoft.com/mockAuthority";
final Authority mockAuthority = Authority.getAuthorityFromAuthorityUrl(mockAuthorityStr);
final String mockNegotiatedBrokerVersion = "18.0";
final String mockAccountName = "mockAccountName";
final String mockClientId = "mockClientId";
final String mockRedirectUri = "mockRedirectUri";
final AccountRecord mockAccountRecord = new AccountRecord();
mockAccountRecord.setHomeAccountId(mockHomeAccountId);
mockAccountRecord.setUsername(mockAccountName);
final CacheRecord mockCacheRecord = CacheRecord.builder()
.account(mockAccountRecord)
.build();
final MsalBrokerResultAdapter resultAdapter = new MsalBrokerResultAdapter();
final IIpcStrategy strategy = new IIpcStrategy() {
@Override
public Bundle communicateToBroker(@NonNull BrokerOperationBundle bundle) {
Bundle retBundle = new Bundle();
if (bundle.getOperation().equals(BrokerOperationBundle.Operation.MSAL_HELLO)) {
retBundle.putString(AuthenticationConstants.Broker.NEGOTIATED_BP_VERSION_KEY, mockNegotiatedBrokerVersion);
} else if (bundle.getOperation().equals(BrokerOperationBundle.Operation.PROVISION_RESOURCE_ACCOUNT)) {
retBundle = resultAdapter.bundleFromAccounts(Collections.singletonList(mockCacheRecord), mockNegotiatedBrokerVersion);
}
return retBundle;
}

@Override
public boolean isSupportedByTargetedBroker(@NonNull final String targetedBrokerPackageName) {
return true;
}

@Override
@NonNull
public Type getType() {
return Type.CONTENT_PROVIDER;
}
};

final IPlatformComponents components = MockPlatformComponentsFactory.getNonFunctionalBuilder().build();
final ResourceAccountCommandParameters parameters = ResourceAccountCommandParameters.builder()
.platformComponents(components)
.homeAccountId(mockHomeAccountId)
.authority(mockAuthority)
.correlationId(mockCorrelationId)
.applicationName("mockApplicationName")
.applicationVersion("mockApplicationVersion")
.sdkVersion("mockSdkVersion")
.sdkType(SdkType.MSAL)
.clientId(mockClientId)
.redirectUri(mockRedirectUri)
.requiredBrokerProtocolVersion(mockNegotiatedBrokerVersion)
.build();

final BrokerMsalController controller = new BrokerMsalController(
InstrumentationRegistry.getInstrumentation().getContext(),
components,
"aBrokerPackage",
Collections.singletonList(strategy));

final ICacheRecord cacheRecord = controller.provisionResourceAccount(parameters);

// verify the cache record
Assert.assertEquals(mockHomeAccountId, cacheRecord.getAccount().getHomeAccountId());
Assert.assertEquals(mockAccountName, cacheRecord.getAccount().getUsername());
}
}
Loading