diff --git a/changelog.txt b/changelog.txt index e6bf60a987..3c3202175f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -21,6 +21,7 @@ Version 23.1.0 - [MINOR] WebApps AccountId Registry (#2787) - [MINOR] Expose WebApps APIs (#2793) - [MINOR] Add domainHint support to authorization request (#2792) +- [MINOR] Add support for WebApps getToken API (#2803) - [PATCH] Fix auth method blocked error handling (#2804) Version 23.0.2 diff --git a/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java b/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java index dd9cb0009c..be134109ee 100644 --- a/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java +++ b/common/src/main/java/com/microsoft/identity/common/adal/internal/AuthenticationConstants.java @@ -1379,22 +1379,32 @@ public static String computeMaxHostBrokerProtocol() { /** * String for broker webapps get contracts result. */ - public static final String BROKER_WEBAPPS_GET_CONTRACTS_RESULT = "contracts"; + public static final String BROKER_WEBAPPS_GET_CONTRACTS_RESULT = "web_apps_contracts"; /** - * String for broker webapps error result. + * String for broker webapps request. */ - public static final String BROKER_WEB_APPS_ERROR = "error"; + public static final String BROKER_WEB_APPS_EXECUTE_REQUEST = "web_apps_execute_request"; /** - * String for broker webapps request. + * String for broker webapps additional required params. */ - public static final String BROKER_WEB_APPS_REQUEST = "request"; + public static final String BROKER_WEB_APPS_ADDITIONAL_REQUIRED_PARAMS = "additional_required_params"; /** * String for broker webapps response. */ - public static final String BROKER_WEB_APPS_RESPONSE = "response"; + public static final String BROKER_WEB_APPS_SUCCESSFUL_RESULT = "web_app_successful_result"; + + /** + * String for compressed broker webapps response. + */ + public static final String BROKER_WEB_APPS_SUCCESSFUL_RESULT_COMPRESSED = "web_app_successful_result_compressed"; + + /** + * String for broker webapps error result. + */ + public static final String BROKER_WEB_APPS_ERROR_RESULT = "web_apps_error_result"; /** * String for generate shr result. @@ -2142,4 +2152,3 @@ public static final class SdkPlatformFields { public static final String VERSION = com.microsoft.identity.common.java.AuthenticationConstants.SdkPlatformFields.VERSION; } } - 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 92bdb5cf90..5762ea0846 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 @@ -84,6 +84,8 @@ private static final class SerializedNames { final static String SIGN_IN_WITH_GOOGLE_CREDENTIAL = "sign_in_with_google_credential"; final static String TENANT_ID = "tenant_id"; + final static String REQUEST_TYPE = "request_type"; + final static String WEB_APPS_STATE = "web_apps_state"; } /** @@ -281,4 +283,15 @@ private static final class SerializedNames { @Nullable @SerializedName(SerializedNames.TENANT_ID) private String mTenantId; + + @Nullable + @SerializedName(SerializedNames.REQUEST_TYPE) + private String mRequestType; + + /** + * State for web apps requests. Make sure not to log this. + */ + @Nullable + @SerializedName(SerializedNames.WEB_APPS_STATE) + private String mWebAppsState; } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/cache/WebAppsAccountIdRegistry.kt b/common/src/main/java/com/microsoft/identity/common/internal/cache/WebAppsAccountIdRegistry.kt index 5a9529aa8b..872d054f6a 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/cache/WebAppsAccountIdRegistry.kt +++ b/common/src/main/java/com/microsoft/identity/common/internal/cache/WebAppsAccountIdRegistry.kt @@ -50,6 +50,7 @@ class WebAppsAccountIdRegistry private constructor( * @param supplier The storage supplier. * @return A new instance of [WebAppsAccountIdRegistry]. */ + @JvmStatic fun create(supplier: IStorageSupplier): WebAppsAccountIdRegistry { val store = supplier.getEncryptedFileStore(WEBAPPS_ACCOUNT_ID_REGISTRY_STORAGE_KEY) return WebAppsAccountIdRegistry(store) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/controllers/BrokerMsalController.java b/common/src/main/java/com/microsoft/identity/common/internal/controllers/BrokerMsalController.java index 5c353ef424..ebff2aad18 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/controllers/BrokerMsalController.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/controllers/BrokerMsalController.java @@ -22,11 +22,13 @@ // THE SOFTWARE. package com.microsoft.identity.common.internal.controllers; +import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_WEB_APPS_ERROR_RESULT; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.CLIENT_ADVERTISED_MAXIMUM_BP_VERSION_KEY; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.CLIENT_CONFIGURED_MINIMUM_BP_VERSION_KEY; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.CLIENT_MAX_PROTOCOL_VERSION; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.MSAL_TO_BROKER_PROTOCOL_NAME; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.NEGOTIATED_BP_VERSION_KEY; +import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.PRT_NONCE; import static com.microsoft.identity.common.internal.broker.ipc.BrokerOperationBundle.Operation.GET_AAD_DEVICE_ID; import static com.microsoft.identity.common.internal.broker.ipc.BrokerOperationBundle.Operation.MSAL_ACQUIRE_TOKEN_DCF; import static com.microsoft.identity.common.internal.broker.ipc.BrokerOperationBundle.Operation.MSAL_ACQUIRE_TOKEN_SILENT; @@ -66,6 +68,10 @@ import com.microsoft.identity.common.internal.broker.ipc.BrokerOperationBundle; import com.microsoft.identity.common.internal.broker.ipc.IIpcStrategy; import com.microsoft.identity.common.internal.broker.ipc.WebAppsAdditionalRequiredParameters; +import com.microsoft.identity.common.internal.util.WebAppsUtil; +import com.microsoft.identity.common.java.commands.webapps.WebAppsGetTokenSubOperationEnvelope; +import com.microsoft.identity.common.java.commands.webapps.WebAppsGetTokenSubOperationRequest; +import com.microsoft.identity.common.java.commands.webapps.WebAppsSupportedContracts; import com.microsoft.identity.common.internal.cache.ActiveBrokerCacheUpdater; import com.microsoft.identity.common.internal.cache.ClientActiveBrokerCache; import com.microsoft.identity.common.internal.cache.HelloCache; @@ -78,13 +84,16 @@ import com.microsoft.identity.common.internal.telemetry.events.ApiEndEvent; import com.microsoft.identity.common.internal.telemetry.events.ApiStartEvent; import com.microsoft.identity.common.java.WarningType; +import com.microsoft.identity.common.java.authorities.Authority; import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryAudience; +import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryAuthority; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeWithClientKeyInternal; import com.microsoft.identity.common.java.cache.ICacheRecord; import com.microsoft.identity.common.java.cache.MsalOAuth2TokenCache; import com.microsoft.identity.common.java.commands.AcquirePrtSsoTokenBatchResult; 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.BrokerInteractiveTokenCommandParameters; import com.microsoft.identity.common.java.commands.parameters.CommandParameters; import com.microsoft.identity.common.java.commands.parameters.DeviceCodeFlowCommandParameters; import com.microsoft.identity.common.java.commands.parameters.GenerateShrCommandParameters; @@ -104,15 +113,19 @@ import com.microsoft.identity.common.java.exception.UnsupportedBrokerException; import com.microsoft.identity.common.java.interfaces.IPlatformComponents; import com.microsoft.identity.common.java.providers.microsoft.MicrosoftRefreshToken; +import com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory.AzureActiveDirectory; import com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory.ClientInfo; import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAccount; import com.microsoft.identity.common.java.providers.oauth2.AuthorizationResult; import com.microsoft.identity.common.java.providers.oauth2.IDToken; +import com.microsoft.identity.common.java.providers.oauth2.OpenIdConnectPromptParameter; +import com.microsoft.identity.common.java.request.BrokerRequestType; import com.microsoft.identity.common.java.request.SdkType; import com.microsoft.identity.common.java.result.AcquireTokenResult; import com.microsoft.identity.common.java.result.GenerateShrResult; import com.microsoft.identity.common.java.ui.PreferredAuthMethod; import com.microsoft.identity.common.java.util.BrokerProtocolVersionUtil; +import com.microsoft.identity.common.java.util.ObjectMapper; import com.microsoft.identity.common.java.util.ResultFuture; import com.microsoft.identity.common.java.util.StringUtil; import com.microsoft.identity.common.java.util.ThreadUtils; @@ -121,9 +134,16 @@ import com.microsoft.identity.common.logging.Logger; import com.microsoft.identity.common.sharedwithoneauth.OneAuthSharedFunctions; +import org.json.JSONException; +import org.json.JSONObject; + import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -350,15 +370,47 @@ private String tryGetNegotiatedProtocolVersionFromHelloCache( @Override public AcquireTokenResult acquireToken(final @NonNull InteractiveTokenCommandParameters parameters) throws BaseException, InterruptedException, ExecutionException { - final String methodTag = TAG + ":acquireToken"; + final AcquireTokenResult result; + try { + final Bundle resultBundle = acquireTokenInternal(parameters); + final String negotiatedBrokerProtocolVersion = resultBundle.getString(NEGOTIATED_BP_VERSION_KEY); + // For MSA Accounts Broker doesn't save the accounts, instead it just passes the result along, + // MSAL needs to save this account locally for future token calls. + // parameters.getOAuth2TokenCache() will be non-null only in case of MSAL native + // If the request is from MSALCPP , OAuth2TokenCache will be null. + if (parameters.getOAuth2TokenCache() != null && !BrokerProtocolVersionUtil.canSupportMsaAccountsInBroker(negotiatedBrokerProtocolVersion)) { + saveMsaAccountToCache(resultBundle, (MsalOAuth2TokenCache) parameters.getOAuth2TokenCache()); + } + verifyBrokerVersionIsSupported(resultBundle, parameters.getRequiredBrokerProtocolVersion()); + result = mResultAdapter.getAcquireTokenResultFromResultBundle(resultBundle); + } catch (final BaseException | ExecutionException e) { + Telemetry.emit( + new ApiEndEvent() + .putException(e) + .putApiId(TelemetryEventStrings.Api.BROKER_ACQUIRE_TOKEN_INTERACTIVE) + ); + throw e; + } + + Telemetry.emit( + new ApiEndEvent() + .putResult(result) + .putApiId(TelemetryEventStrings.Api.BROKER_ACQUIRE_TOKEN_INTERACTIVE) + ); + + return result; + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + protected Bundle acquireTokenInternal(final @NonNull InteractiveTokenCommandParameters parameters) throws BaseException, InterruptedException, ExecutionException { + final String methodTag = TAG + ":acquireTokenInternal"; Telemetry.emit( new ApiStartEvent() .putProperties(parameters) .putApiId(TelemetryEventStrings.Api.BROKER_ACQUIRE_TOKEN_INTERACTIVE) ); - - //Create BrokerResultFuture to block on response from the broker... response will be return as an activity result + //Create BrokerResultFuture to block on response from the broker... //BrokerActivity will receive the result and ask the API dispatcher to complete the request //In completeAcquireToken below we will set the result on the future and unblock the flow. mBrokerResultFuture = new ResultFuture<>(); @@ -418,39 +470,10 @@ public void onReceive(@NonNull PropertyBag propertyBag) { // Start the BrokerActivity using our existing Activity activity.startActivity(brokerActivityIntent); } - - final AcquireTokenResult result; - try { - //Wait to be notified of the result being returned... we could add a timeout here if we want to - final Bundle resultBundle = mBrokerResultFuture.get(); - - final String negotiatedBrokerProtocolVersion = interactiveRequestIntent.getStringExtra(NEGOTIATED_BP_VERSION_KEY); - // For MSA Accounts Broker doesn't save the accounts, instead it just passes the result along, - // MSAL needs to save this account locally for future token calls. - // parameters.getOAuth2TokenCache() will be non-null only in case of MSAL native - // If the request is from MSALCPP , OAuth2TokenCache will be null. - if (parameters.getOAuth2TokenCache() != null && !BrokerProtocolVersionUtil.canSupportMsaAccountsInBroker(negotiatedBrokerProtocolVersion)) { - saveMsaAccountToCache(resultBundle, (MsalOAuth2TokenCache) parameters.getOAuth2TokenCache()); - } - - verifyBrokerVersionIsSupported(resultBundle, parameters.getRequiredBrokerProtocolVersion()); - result = mResultAdapter.getAcquireTokenResultFromResultBundle(resultBundle); - } catch (final BaseException | ExecutionException e) { - Telemetry.emit( - new ApiEndEvent() - .putException(e) - .putApiId(TelemetryEventStrings.Api.BROKER_ACQUIRE_TOKEN_INTERACTIVE) - ); - throw e; - } - - Telemetry.emit( - new ApiEndEvent() - .putResult(result) - .putApiId(TelemetryEventStrings.Api.BROKER_ACQUIRE_TOKEN_INTERACTIVE) - ); - - return result; + //Wait to be notified of the result being returned... we could add a timeout here if we want to + final Bundle resultBundle = mBrokerResultFuture.get(); + resultBundle.putString(NEGOTIATED_BP_VERSION_KEY, interactiveRequestIntent.getStringExtra(NEGOTIATED_BP_VERSION_KEY)); + return resultBundle; } @Override @@ -1425,7 +1448,7 @@ public void putValueInSuccessEvent(@NonNull final ApiEndEvent event, /** * Execute web app request in broker. * - * @param request request string + * @param request request string. * @param minBrokerProtocolVersion minimum broker protocol version the caller requires. * @param additionalRequiredParams additional required parameters for web app request. * @throws BaseException @@ -1433,52 +1456,120 @@ public void putValueInSuccessEvent(@NonNull final ApiEndEvent event, public String executeWebAppRequest(@NonNull final String request, @NonNull final String minBrokerProtocolVersion, @NonNull final WebAppsAdditionalRequiredParameters additionalRequiredParams) throws BaseException { - return getBrokerOperationExecutor().execute(null, - new BrokerOperation() { - private String negotiatedBrokerProtocolVersion; + try { + // Take a peek at the type of request. + final String subMethod = new JSONObject(request).getString(WebAppsGetTokenSubOperationEnvelope.FIELD_METHOD); + final WebAppsGetTokenSubOperationEnvelope envelope; + if (subMethod.equals(WebAppsSupportedContracts.GET_TOKEN)) { + // If get token, we should check to see if we should just start interactive right away. + envelope = ObjectMapper.deserializeJsonStringToObject( + request, + WebAppsGetTokenSubOperationEnvelope.class + ); + final WebAppsGetTokenSubOperationRequest getTokenRequest = envelope.getRequest(); + // If need to do interactive right away, do it now. + // Otherwise, just let the broker handle the silent token acquisition first. + if (shouldForceInteractiveRequestForWebApp(getTokenRequest, additionalRequiredParams.getCanShowUi())) { + AzureActiveDirectory.buildAndValidateAuthorityFromWebAppSender(envelope.getSender()); + final BrokerInteractiveTokenCommandParameters interactiveParams = + buildInteractiveTokenParametersForWebApps(getTokenRequest, additionalRequiredParams, minBrokerProtocolVersion); + final Bundle resultBundle = acquireTokenInternal(interactiveParams); + return mResultAdapter.getExecuteWebAppRequestResultFromBundle(resultBundle); + } + } else { + envelope = null; + } - @Override - public void performPrerequisites(@NonNull final IIpcStrategy strategy) throws BaseException { - negotiatedBrokerProtocolVersion = hello(strategy, minBrokerProtocolVersion); - } + // Silent GetToken, GetCookies, or SignOut. + return getBrokerOperationExecutor().execute(null, + new BrokerOperation() { + private String negotiatedBrokerProtocolVersion; - @NonNull - @Override - public BrokerOperationBundle getBundle() throws ClientException { - return new BrokerOperationBundle( - BrokerOperationBundle.Operation.BROKER_WEBAPPS_API_EXECUTE_WEB_APPS_REQUEST, - mActiveBrokerPackageName, - mRequestAdapter.getRequestBundleForExecuteWebAppRequest(request,negotiatedBrokerProtocolVersion, minBrokerProtocolVersion) - ); - } + @Override + public void performPrerequisites(@NonNull final IIpcStrategy strategy) throws BaseException { + negotiatedBrokerProtocolVersion = hello(strategy, minBrokerProtocolVersion); + } - @NonNull - @Override - public String extractResultBundle(@Nullable final Bundle resultBundle) throws BaseException { - if (resultBundle == null) { - throw mResultAdapter.getExceptionForEmptyResultBundle(); + @NonNull + @Override + public BrokerOperationBundle getBundle() throws ClientException { + final String additionalParamsString = ObjectMapper.serializeObjectToJsonString(additionalRequiredParams); + return new BrokerOperationBundle( + BrokerOperationBundle.Operation.BROKER_WEBAPPS_API_EXECUTE_WEB_APPS_REQUEST, + mActiveBrokerPackageName, + mRequestAdapter.getRequestBundleForExecuteWebAppRequest( + request, + negotiatedBrokerProtocolVersion, + minBrokerProtocolVersion, + additionalParamsString + ) + ); } - verifyBrokerVersionIsSupported(resultBundle, minBrokerProtocolVersion); - return mResultAdapter.getExecuteWebAppRequestResultFromBundle(resultBundle); - } - @NonNull - @Override - public String getMethodName() { - return ":executeWebAppRequest"; - } + @NonNull + @Override + public String extractResultBundle(@Nullable final Bundle resultBundle) throws BaseException { + if (resultBundle == null) { + throw mResultAdapter.getExceptionForEmptyResultBundle(); + } + verifyBrokerVersionIsSupported(resultBundle, minBrokerProtocolVersion); + + final String result = mResultAdapter.getExecuteWebAppRequestResultFromBundle(resultBundle); + if (resultBundle.containsKey(BROKER_WEB_APPS_ERROR_RESULT) + && envelope != null) { + final WebAppsGetTokenSubOperationRequest getTokenRequest = envelope.getRequest(); + if (canFallbackToInteractiveRequestForWebApp(getTokenRequest, additionalRequiredParams.getCanShowUi())) { + // Create params from the request + if (getTokenRequest.isSecurityTokenService()) { + // Validate sender authority (throws if invalid) + AzureActiveDirectory.buildAndValidateAuthorityFromWebAppSender(envelope.getSender()); + } else { + WebAppsUtil.validateMsalJsRedirectOrigin( + getTokenRequest.getRedirectUri(), + envelope.getSender() + ); + } + final BrokerInteractiveTokenCommandParameters interactiveParams = + buildInteractiveTokenParametersForWebApps(getTokenRequest, additionalRequiredParams, minBrokerProtocolVersion); + try { + final Bundle interactiveGetTokenBundle = acquireTokenInternal(interactiveParams); + return mResultAdapter.getExecuteWebAppRequestResultFromBundle(interactiveGetTokenBundle); + } catch (final Throwable t) { + return WebAppsUtil.createErrorResponseString(t, "Error occurred during interactive request fallback"); + } + } + + } + + return result; + } - @Nullable - @Override - public String getTelemetryApiId() { - return null; - } + @NonNull + @Override + public String getMethodName() { + return ":executeWebAppRequest"; + } - @Override - public void putValueInSuccessEvent(@NonNull final ApiEndEvent event, - @NonNull final String result) { - } - }); + @Nullable + @Override + public String getTelemetryApiId() { + return null; + } + + @Override + public void putValueInSuccessEvent(@NonNull final ApiEndEvent event, + @NonNull final String result) { + } + }); + } catch (final UnsupportedBrokerException ex) { + // We want to throw this exception to keep it in line with the other APIs. + throw ex; + } catch (final JSONException jsonException) { + return WebAppsUtil.createErrorResponseString(jsonException, "Error occurred during request parsing"); + } + catch (final Exception ex) { + return WebAppsUtil.createErrorResponseString(ex, "Error occurred during validation or interactive"); + } } /** @@ -1600,4 +1691,140 @@ private void verifyBrokerVersionIsSupported(@Nullable final Bundle resultBundle, "So, this is not likely a broker version supported issue. Continuing."); } } + + /** + * Determines if we should force interactive token acquisition. + * + * @param req The get token sub-operation request. + * @param canShowUI A boolean indicating if UI interaction is allowed. + * @return True if interactive token acquisition should be forced, false otherwise. + * @throws ClientException if prompt is not none and UI is not allowed. + */ + private boolean shouldForceInteractiveRequestForWebApp(final @NonNull WebAppsGetTokenSubOperationRequest req, + final boolean canShowUI) throws ClientException { + // MSAL JS requests will always be silent first. + if (!req.isSecurityTokenService() || !StringUtil.isNullOrEmpty(req.getHomeAccountId())) { + return false; + } + final OpenIdConnectPromptParameter prompt = OpenIdConnectPromptParameter.fromString(req.getPrompt()); + if (prompt == OpenIdConnectPromptParameter.NONE) { + return false; + } + //we need to return a specific Edge error status code if prompt is not none and UI is not allowed by Edge. + if (!canShowUI) { + throw new ClientException( + ErrorStrings.UI_NOT_ALLOWED, + "Interactive token acquisition is required but UI interaction is not allowed." + ); + } + return true; + } + + /** + * Determines if we can fallback to interactive token acquisition. + * + * @param req The get token sub-operation request. + * @param canShowUI A boolean indicating if UI interaction is allowed. + * @return True if we can fallback to interactive token acquisition, false otherwise. + * @throws ClientException if prompt is none or if UI is not allowed when prompt is not none. + */ + private boolean canFallbackToInteractiveRequestForWebApp(@NonNull final WebAppsGetTokenSubOperationRequest req, + final boolean canShowUI) throws ClientException { + final OpenIdConnectPromptParameter prompt = OpenIdConnectPromptParameter.fromString(req.getPrompt()); + if (prompt == OpenIdConnectPromptParameter.NONE) { + return false; + } + // In the case where prompt is something other than none AND UI is not allowed, we need to throw a specific exception. + if (!canShowUI) { + throw new ClientException( + ErrorStrings.UI_NOT_ALLOWED, + "Interactive token acquisition is required but UI interaction is not allowed." + ); + } + return true; + } + + /** + * Build interactive token command parameters from web apps interactive request. + * + * @param webAppsRequest web apps interactive request + * @param requiredParams additional required parameters for web app request. + * @param minBrokerProtocolVersion minimum broker protocol version the caller requires. + * @return BrokerInteractiveTokenCommandParameters + * @throws BaseException + */ + @NonNull + private BrokerInteractiveTokenCommandParameters buildInteractiveTokenParametersForWebApps(@NonNull final WebAppsGetTokenSubOperationRequest webAppsRequest, + @NonNull final WebAppsAdditionalRequiredParameters requiredParams, + @NonNull final String minBrokerProtocolVersion) throws BaseException { + final String authorityUrl = webAppsRequest.getAuthority() != null + ? webAppsRequest.getAuthority() + : WebAppsGetTokenSubOperationRequest.DEFAULT_AUTHORITY; + + if (StringUtil.isNullOrEmpty(authorityUrl)) { + throw new ClientException(ClientException.MISSING_PARAMETER, "Authority is null or empty."); + } + final String clientId = webAppsRequest.getClientId(); + if (StringUtil.isNullOrEmpty(clientId)) { + throw new ClientException(ClientException.MISSING_PARAMETER, "ClientId is null or empty."); + } + final String redirect = webAppsRequest.getRedirectUri(); + if (StringUtil.isNullOrEmpty(redirect)) { + throw new ClientException(ClientException.MISSING_PARAMETER, "Redirect URI is null or empty."); + } + + final String correlationId = webAppsRequest.getCorrelationId() != null + ? webAppsRequest.getCorrelationId() + : UUID.randomUUID().toString(); + + final Set scopeSet; + final String rawScope = webAppsRequest.getScopes(); + if (StringUtil.isNullOrEmpty(rawScope)) { + scopeSet = Collections.emptySet(); + } else { + scopeSet = new HashSet<>(); + for (String s : rawScope.trim().split("\\s+")) { + if (!s.isEmpty()) { + scopeSet.add(s); + } + } + } + + final Authority authority = Authority.getAuthorityFromAuthorityUrl(authorityUrl); + if (authority == null) { + throw new ClientException(ClientException.MISSING_PARAMETER, + "Unable to create Authority from url"); + } + if (authority instanceof AzureActiveDirectoryAuthority) { + ((AzureActiveDirectoryAuthority) authority) + .setMultipleCloudsSupported(webAppsRequest.getInstanceAware()); + } + + Map tempMap = webAppsRequest.getExtraParameters(); + final Map extraQueryParamsMap = tempMap != null + ? tempMap + : Collections.emptyMap(); + final List> queryParams = new ArrayList<>(extraQueryParamsMap.entrySet()); + final OpenIdConnectPromptParameter prompt = + OpenIdConnectPromptParameter.fromString(webAppsRequest.getPrompt()); + + return BrokerInteractiveTokenCommandParameters.builder() + .applicationName(requiredParams.getCallingApplicationName()) + .callerPackageName(requiredParams.getCallingPackageName()) + .applicationVersion(requiredParams.getCallingApplicationVersion()) + .sdkType(requiredParams.getSdkType()) + .sdkVersion(requiredParams.getSdkVersion()) + .platformComponents(mComponents) + .clientId(clientId) + .redirectUri(redirect) + .authority(authority) + .scopes(scopeSet) + .loginHint(webAppsRequest.getLoginHint()) + .correlationId(correlationId) + .requiredBrokerProtocolVersion(minBrokerProtocolVersion) + .prompt(prompt) + .extraQueryStringParameters(queryParams) + .requestType(BrokerRequestType.WEB_APPS) + .build(); + } } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/platform/AndroidPlatformUtil.java b/common/src/main/java/com/microsoft/identity/common/internal/platform/AndroidPlatformUtil.java index 8e7ef868a5..1d1e45d5e5 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/platform/AndroidPlatformUtil.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/platform/AndroidPlatformUtil.java @@ -50,6 +50,7 @@ import com.microsoft.identity.common.java.commands.ICommand; import com.microsoft.identity.common.java.commands.InteractiveTokenCommand; import com.microsoft.identity.common.java.commands.parameters.InteractiveTokenCommandParameters; +import com.microsoft.identity.common.java.exception.ArgumentException; import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.exception.ErrorStrings; import com.microsoft.identity.common.java.flighting.CommonFlight; @@ -161,6 +162,11 @@ public boolean isValidCallingApp(@NonNull String redirectUri, @NonNull String pa return isValidBrokerRedirect; } + @Override + public void isValidCallingAppForWebApps(int callingUid) throws ClientException, UnsupportedOperationException { + // This operation is not supported in non-broker contexts. + throw new UnsupportedOperationException("WebApp APIs are not functional in non-broker scenarios."); + } @Override @Nullable public String getEnrollmentId(@NonNull final String userId, @NonNull final String packageName) { @@ -324,4 +330,8 @@ private boolean isValidHubRedirectURIForNAATests(String redirectUri) { || redirectUri.equals("msauth://com.microsoft.teams/fcg80qvoM1YMKJZibjBwQcDfOno=") || redirectUri.equals("https://login.microsoftonline.com/common/oauth2/nativeclient")); } + + protected Context getContext() { + return mContext; + } } 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 a3d50dd02f..91e4332ea0 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 @@ -29,7 +29,8 @@ import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.AUTH_SCHEME_PARAMS_POP; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_REQUEST_V2; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_REQUEST_V2_COMPRESSED; -import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_WEB_APPS_REQUEST; +import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_WEB_APPS_ADDITIONAL_REQUIRED_PARAMS; +import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_WEB_APPS_EXECUTE_REQUEST; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.CALLER_INFO_UID; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.CAN_FOCI_APPS_CONSTRUCT_ACCOUNTS_FROM_PRT_ID_TOKEN_KEY; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.CLIENT_ADVERTISED_MAXIMUM_BP_VERSION_KEY; @@ -56,6 +57,8 @@ import com.microsoft.identity.common.java.authscheme.INameable; import com.microsoft.identity.common.java.authscheme.PopAuthenticationSchemeInternal; import com.microsoft.identity.common.java.commands.parameters.AcquirePrtSsoTokenCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.BrokerInteractiveTokenCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.BrokerSilentTokenCommandParameters; import com.microsoft.identity.common.java.commands.parameters.CommandParameters; import com.microsoft.identity.common.java.commands.parameters.DeviceCodeFlowCommandParameters; import com.microsoft.identity.common.java.commands.parameters.GenerateShrCommandParameters; @@ -138,6 +141,11 @@ public BrokerRequest brokerRequestFromAcquireTokenParameters(@NonNull final Inte brokerRequestBuilder.signInWithGoogleCredential(androidInteractiveTokenCommandParameters.getSignInWithGoogleCredential()); } + if (parameters instanceof BrokerInteractiveTokenCommandParameters) { + brokerRequestBuilder.requestType(((BrokerInteractiveTokenCommandParameters) parameters).getRequestType().name()); + brokerRequestBuilder.webAppsState(((BrokerInteractiveTokenCommandParameters) parameters).getWebAppsState()); + } + return brokerRequestBuilder.build(); } @@ -178,7 +186,7 @@ public BrokerRequest brokerRequestFromSilentOperationParameters(@NonNull final S final String extraOptions = parameters.getExtraOptions() != null ? QueryParamsAdapter._toJson(parameters.getExtraOptions()) : null; - final BrokerRequest brokerRequest = BrokerRequest.builder() + final BrokerRequest.BrokerRequestBuilder brokerRequestBuilder = BrokerRequest.builder() .authority(parameters.getAuthority().getAuthorityURL().toString()) .scope(TextUtils.join(" ", parameters.getScopes())) .redirect(parameters.getRedirectUri()) @@ -205,10 +213,13 @@ public BrokerRequest brokerRequestFromSilentOperationParameters(@NonNull final S .spanId(SpanExtension.current().getSpanContext().getSpanId()) .traceFlags(SpanExtension.current().getSpanContext().getTraceFlags().asByte()) .build() - ) - .build(); + ); - return brokerRequest; + if (parameters instanceof BrokerSilentTokenCommandParameters) { + brokerRequestBuilder.requestType(((BrokerSilentTokenCommandParameters) parameters).getRequestType().name()); + } + + return brokerRequestBuilder.build(); } public @NonNull Bundle getRequestBundleForSsoToken(final @NonNull AcquirePrtSsoTokenCommandParameters parameters, @@ -597,17 +608,20 @@ public Bundle getRequestBundleForAadDeviceIdRequest( /** * Method to construct a request bundle for broker executeWebAppRequest request. * - * @param request input request + * @param request input request * @param negotiatedBrokerProtocolVersion protocol version returned by broker hello. * @param requiredBrokerProtocolVersion protocol version required by the client. + * @param additionalRequiredParams extra required arguments to be sent to broker. * @return request Bundle */ public Bundle getRequestBundleForExecuteWebAppRequest(@NonNull final String request, @NonNull final String negotiatedBrokerProtocolVersion, - @NonNull final String requiredBrokerProtocolVersion) { + @NonNull final String requiredBrokerProtocolVersion, + @NonNull final String additionalRequiredParams) { final Bundle bundle = new Bundle(); bundle.putString(AuthenticationConstants.Broker.NEGOTIATED_BP_VERSION_KEY, negotiatedBrokerProtocolVersion); - bundle.putString(BROKER_WEB_APPS_REQUEST, request); + bundle.putString(BROKER_WEB_APPS_EXECUTE_REQUEST, request); + bundle.putString(BROKER_WEB_APPS_ADDITIONAL_REQUIRED_PARAMS, additionalRequiredParams); addRequiredBrokerProtocolVersionToRequestBundle(bundle, requiredBrokerProtocolVersion); return bundle; } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/result/AdalBrokerResultAdapter.java b/common/src/main/java/com/microsoft/identity/common/internal/result/AdalBrokerResultAdapter.java index 5b1f929b38..1fcb350892 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/result/AdalBrokerResultAdapter.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/result/AdalBrokerResultAdapter.java @@ -168,12 +168,25 @@ public class AdalBrokerResultAdapter implements IBrokerResultAdapter { return resultBundle; } + @Override + public @NonNull Bundle bundleFromBaseExceptionForWebApps(@NonNull BaseException exception) { + throw new UnsupportedOperationException(); + } + @Override public @NonNull ILocalAuthenticationResult authenticationResultFromBundle(Bundle resultBundle) { throw new UnsupportedOperationException(); } + @NonNull + @Override + public Bundle bundleFromAuthenticationResultForWebApps(@NonNull ILocalAuthenticationResult authenticationResult, + @Nullable String negotiatedBrokerProtocolVersion, + @Nullable String state) throws BaseException { + throw new UnsupportedOperationException(); + } + @Override public @NonNull BaseException getBaseExceptionFromBundle(Bundle resultBundle) { throw new UnsupportedOperationException(); diff --git a/common/src/main/java/com/microsoft/identity/common/internal/result/IBrokerResultAdapter.java b/common/src/main/java/com/microsoft/identity/common/internal/result/IBrokerResultAdapter.java index 7968960c0c..21f4810e24 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/result/IBrokerResultAdapter.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/result/IBrokerResultAdapter.java @@ -43,6 +43,18 @@ public interface IBrokerResultAdapter { @NonNull Bundle bundleFromAuthenticationResult(@NonNull final ILocalAuthenticationResult authenticationResult, @Nullable final String negotiatedBrokerProtocolVersion); + /** + * Returns a success bundle with properties from result for web apps. + * + * @param authenticationResult + * @param negotiatedBrokerProtocolVersion + * @param state + * @return {@link Bundle} + */ + @NonNull Bundle bundleFromAuthenticationResultForWebApps(@NonNull final ILocalAuthenticationResult authenticationResult, + @Nullable final String negotiatedBrokerProtocolVersion, + @Nullable final String state) throws BaseException; + /** * Returns an error bundle with properties from Exception. * @@ -52,6 +64,14 @@ public interface IBrokerResultAdapter { @NonNull Bundle bundleFromBaseException(@NonNull BaseException exception, @Nullable final String negotiatedBrokerProtocolVersion); + /** + * Returns an error bundle with properties from Exception for web apps. + * + * @param exception + * @return {@link Bundle} + */ + @NonNull Bundle bundleFromBaseExceptionForWebApps(@NonNull final BaseException exception); + /** * Returns authentication result from Broker result bundle * 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 e1078559b7..befdfe6c9e 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 @@ -32,8 +32,9 @@ import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_PACKAGE_NAME; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_RESULT_V2_COMPRESSED; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_WEBAPPS_GET_CONTRACTS_RESULT; -import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_WEB_APPS_ERROR; -import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_WEB_APPS_RESPONSE; +import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_WEB_APPS_ERROR_RESULT; +import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_WEB_APPS_SUCCESSFUL_RESULT; +import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.BROKER_WEB_APPS_SUCCESSFUL_RESULT_COMPRESSED; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.HELLO_ERROR_CODE; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.HELLO_ERROR_MESSAGE; import static com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.NEGOTIATED_BP_VERSION_KEY; @@ -57,11 +58,14 @@ import com.microsoft.identity.common.internal.broker.BrokerResult; import com.microsoft.identity.common.internal.request.AuthenticationSchemeTypeAdapter; import com.microsoft.identity.common.internal.util.GzipUtil; +import com.microsoft.identity.common.internal.util.WebAppsUtil; import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryAudience; import com.microsoft.identity.common.java.cache.CacheRecord; import com.microsoft.identity.common.java.cache.ICacheRecord; import com.microsoft.identity.common.java.commands.AcquirePrtSsoTokenBatchResult; import com.microsoft.identity.common.java.commands.AcquirePrtSsoTokenResult; +import com.microsoft.identity.common.java.commands.webapps.WebAppsAccountItem; +import com.microsoft.identity.common.java.commands.webapps.WebAppsGetTokenSubOperationResponse; import com.microsoft.identity.common.java.constants.OAuth2ErrorCode; import com.microsoft.identity.common.java.constants.OAuth2SubErrorCode; import com.microsoft.identity.common.java.dto.AadDeviceIdRecord; @@ -149,6 +153,75 @@ public Bundle bundleFromAuthenticationResult(@NonNull final ILocalAuthentication return resultBundle; } + @NonNull + @Override + public Bundle bundleFromAuthenticationResultForWebApps(@NonNull final ILocalAuthenticationResult authenticationResult, + @Nullable final String negotiatedBrokerProtocolVersion, + @Nullable final String state) throws BaseException { + final String methodTag = TAG + ":bundleFromAuthenticationResultForWebApps"; + final String errorMessagePrefix = "Received a successful interactive result, but: "; + Logger.info(methodTag, "Constructing result bundle from ILocalAuthenticationResult"); + + final Bundle resultBundle = new Bundle(); + + final String homeAccountId = authenticationResult.getUniqueId(); + + final String clientInfo = WebAppsUtil.homeAccountIdToClientInfo(homeAccountId); + if (StringUtil.isNullOrEmpty(clientInfo)) { + throw new ClientException( + ErrorStrings.UNKNOWN_ERROR, + errorMessagePrefix + "clientInfo could not be derived from homeAccountId." + ); + } + // Some parameters can be null, so double checking. + final String username = WebAppsUtil.requireNotNullOrEmpty(authenticationResult.getAccountRecord().getUsername(), WebAppsAccountItem.FIELD_USER_NAME); + final String expiresOn = WebAppsUtil.requireNotNullOrEmpty(authenticationResult.getAccessTokenRecord().getExpiresOn(), WebAppsGetTokenSubOperationResponse.FIELD_EXPIRES_IN); + final String idToken = WebAppsUtil.requireNotNullOrEmpty(authenticationResult.getIdToken(), WebAppsGetTokenSubOperationResponse.FIELD_ID_TOKEN); + final WebAppsAccountItem accountItem = new WebAppsAccountItem(username, homeAccountId, null); + + final WebAppsGetTokenSubOperationResponse getTokenResponse = new WebAppsGetTokenSubOperationResponse( + state, + WebAppsUtil.computeRemainingSeconds(expiresOn), + null, // TODO (AB#3420725): Once design for MATS properties is finalized, populate this field. + clientInfo, + accountItem, + idToken, + authenticationResult.getAccessToken(), + String.join(" ", authenticationResult.getScope()) + ); + final String getTokenJsonString = AuthenticationSchemeTypeAdapter.getGsonInstance().toJson( + getTokenResponse, + WebAppsGetTokenSubOperationResponse.class + ); + if (BrokerProtocolVersionUtil.canCompressBrokerPayloads(negotiatedBrokerProtocolVersion)) { + try { + byte[] compressedBytes = compressString(getTokenJsonString); + Logger.info(methodTag, "GetToken Result, raw payload size:" + + getTokenJsonString.getBytes(AuthenticationConstants.CHARSET_UTF8).length + " ,compressed bytes " + compressedBytes.length + ); + resultBundle.putByteArray( + BROKER_WEB_APPS_SUCCESSFUL_RESULT_COMPRESSED, + compressedBytes + ); + } catch (IOException e) { + Logger.error(methodTag, "Failed to compress GetToken Result, sending as jsonString ", e); + resultBundle.putString( + BROKER_WEB_APPS_SUCCESSFUL_RESULT, + getTokenJsonString + ); + } + } else { + Logger.info(methodTag, "Broker protocol version: " + negotiatedBrokerProtocolVersion + + " lower than compression changes, sending as string" + ); + resultBundle.putString( + BROKER_WEB_APPS_SUCCESSFUL_RESULT, + getTokenJsonString + ); + } + return resultBundle; + } + /** * Constructs a {@link BrokerResult} object from the given {@link ILocalAuthenticationResult} * **/ @@ -365,6 +438,14 @@ public Bundle bundleFromBaseException(@NonNull final BaseException exception, return resultBundle; } + @NonNull + @Override + public Bundle bundleFromBaseExceptionForWebApps(@NonNull final BaseException exception) { + final String methodTag = TAG + ":bundleFromBaseExceptionForWebApps"; + Logger.info(methodTag, "Constructing webapps result bundle from ClientException"); + return WebAppsUtil.createErrorResponseBundle(exception, "Error occurred during interactive request"); + } + @NonNull @Override public ILocalAuthenticationResult authenticationResultFromBundle(@NonNull final Bundle resultBundle) throws ClientException { @@ -1063,22 +1144,42 @@ public String getSupportedWebAppsContractFromBundle(@NonNull final Bundle result /** * Gets the execute web app request result string from the result bundle. + * * @param resultBundle The result bundle from the broker. + * @return The result string from the web app execution. */ @NonNull public String getExecuteWebAppRequestResultFromBundle(@NonNull final Bundle resultBundle) throws ClientException { - // Expect either success payload or error fields reused from BrokerResult - if (resultBundle.containsKey(BROKER_WEB_APPS_ERROR)) { - final String result = resultBundle.getString(BROKER_WEB_APPS_ERROR); + final String methodTag = TAG + ":getExecuteWebAppRequestResultFromBundle"; + final String errorMessage = "For interactive request, WebApps entry in bundle null for "; + // Expect success payload or error fields reused from BrokerResult + if (resultBundle.containsKey(BROKER_WEB_APPS_SUCCESSFUL_RESULT_COMPRESSED)) { + byte[] compressedBytes = resultBundle.getByteArray(BROKER_WEB_APPS_SUCCESSFUL_RESULT_COMPRESSED); + if (compressedBytes != null) { + try { + return GzipUtil.decompressBytesToString(compressedBytes); + } catch (final IOException e) { + // We should never hit this ideally unless the string/bytes are malformed for some unknown reason. + Logger.error(methodTag, "Failed to decompress broker result :", e); + throw new ClientException(INVALID_BROKER_BUNDLE, "Failed to decompress broker result", e); + } + } else { + throw new ClientException(INVALID_BROKER_BUNDLE, errorMessage + BROKER_WEB_APPS_SUCCESSFUL_RESULT_COMPRESSED); + } + } else if (resultBundle.containsKey(BROKER_WEB_APPS_SUCCESSFUL_RESULT)) { + final String result = resultBundle.getString(BROKER_WEB_APPS_SUCCESSFUL_RESULT); if (result == null) { - throw new ClientException(INVALID_BROKER_BUNDLE, WEBAPPS_ENTRY_IS_NULL_ERROR + " for " + BROKER_WEB_APPS_ERROR); + throw new ClientException(INVALID_BROKER_BUNDLE, errorMessage + BROKER_WEB_APPS_SUCCESSFUL_RESULT); } return result; + } else if (resultBundle.containsKey(BROKER_WEB_APPS_ERROR_RESULT)) { + final String result = resultBundle.getString(BROKER_WEB_APPS_ERROR_RESULT); + if (result == null) { + throw new ClientException(INVALID_BROKER_BUNDLE, errorMessage + BROKER_WEB_APPS_ERROR_RESULT); + } + return result; + } else { + throw new ClientException(INVALID_BROKER_BUNDLE, "For interactive request, no response or error found in bundle."); } - final String result = resultBundle.getString(BROKER_WEB_APPS_RESPONSE); - if (result == null) { - throw new ClientException(INVALID_BROKER_BUNDLE, WEBAPPS_ENTRY_IS_NULL_ERROR + " for " + BROKER_WEB_APPS_RESPONSE); - } - return result; } } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/util/WebAppsUtil.kt b/common/src/main/java/com/microsoft/identity/common/internal/util/WebAppsUtil.kt new file mode 100644 index 0000000000..d522381fc9 --- /dev/null +++ b/common/src/main/java/com/microsoft/identity/common/internal/util/WebAppsUtil.kt @@ -0,0 +1,180 @@ +// 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.util + +import android.os.Bundle +import com.microsoft.identity.common.java.commands.webapps.WebAppError +import com.microsoft.identity.common.adal.internal.AuthenticationConstants +import com.microsoft.identity.common.java.base64.Base64Util +import com.microsoft.identity.common.java.exception.ClientException +import com.microsoft.identity.common.java.exception.ErrorStrings +import com.microsoft.identity.common.java.util.ObjectMapper +import com.microsoft.identity.common.logging.Logger +import java.net.URI + +/** + * Utility class for Web Apps related operations. + */ +class WebAppsUtil { + companion object { + private val TAG = WebAppsUtil::class.simpleName + + const val DEFAULT_AUTHORITY = "https://login.microsoftonline.com/common" + + /** + * Validates that the redirect URI origin matches the sender origin for MSAL JS requests. + * + * @param redirectUri The redirect URI from the request. + * @param senderUri The sender URI to compare against the redirect URI. + * @throws ClientException if the redirect URI origin does not match the sender origin. + */ + @JvmStatic + fun validateMsalJsRedirectOrigin(redirectUri: String, + senderUri: String) { + if (!hasSameSchemeAndHost(senderUri, redirectUri)) { + throw ClientException( + ErrorStrings.INVALID_REQUEST, + "The redirect URI origin does not match the sender origin." + ) + } + } + + /** + * Create a [Bundle] containing a successful response object. + * + * @param responseObject The response object to include in the bundle. + * @return A [Bundle] containing the response object. + */ + @JvmStatic + fun getResponseBundle(responseObject: Any): Bundle { + return Bundle().apply { + putString( + AuthenticationConstants.Broker.BROKER_WEB_APPS_SUCCESSFUL_RESULT, + ObjectMapper.serializeObjectToJsonString(responseObject) + ) + } + } + + + /** + * Create a [Bundle] containing an error response from a [Throwable] and optional description. + * + * @param t The throwable to create the error response from. + * @param description An optional description to include in the error response. + * @return A [Bundle] containing the error response. + */ + @JvmStatic + fun createErrorResponseBundle(t: Throwable, description: String?): Bundle { + return Bundle().apply { + putString( + AuthenticationConstants.Broker.BROKER_WEB_APPS_ERROR_RESULT, + createErrorResponseString(t, description) + ) + } + } + + @JvmStatic + fun createErrorResponseString(t: Throwable, description: String?): String { + val errorDescription = if (!description.isNullOrBlank()) { + "$description: ${t.javaClass.simpleName}: ${t.message}" + } else { + "Error occurred during operation: ${t.javaClass.simpleName}: ${t.message}" + } + return ObjectMapper.serializeObjectToJsonString(WebAppError(t, errorDescription)) + } + + /** + * Utility method to require a non-null and non-empty value, throwing a ClientException if null or empty. + * + * @param value The value to check. + * @param name The name of the parameter (for error message). + * @return The non-null, non-empty value. + * @throws ClientException if the value is null or empty. + */ + @JvmStatic + @Throws(ClientException::class) + fun requireNotNullOrEmpty(value: T?, name: String): T { + if (value == null) { + throw ClientException(ClientException.MISSING_PARAMETER, "$name is null.") + } + if (value is CharSequence && value.isEmpty()) { + throw ClientException(ClientException.MISSING_PARAMETER, "$name is empty.") + } + if (value is Collection<*> && value.isEmpty()) { + throw ClientException(ClientException.MISSING_PARAMETER, "$name is empty.") + } + return value + } + + /** + * Computes the remaining seconds until the target epoch time. + * + * @param epochSecondsStr The target epoch time in seconds as a string. + * @return The remaining seconds until the target time, or 0 if the target time has passed or is invalid. + */ + @JvmStatic + fun computeRemainingSeconds(epochSecondsStr: String?): Long { + if (epochSecondsStr.isNullOrBlank()) return 0L + return try { + val target = epochSecondsStr.toLong() + val now = System.currentTimeMillis() / 1000L + val delta = target - now + if (delta > 0) delta else 0L + } catch (e: NumberFormatException) { + Logger.warn("$TAG:computeRemainingSeconds", "Invalid epoch seconds: $epochSecondsStr") + 0L + } + } + + /** + * Converts a homeAccountId of the form uid.utid into the raw client_info string + * (Base64URL encoded JSON: {"uid":"","utid":""}). + * + * @param homeAccountId The home account id (uid.utid). + * @return Base64URL (unpadded) encoded client_info or null if input invalid. + */ + @JvmStatic + fun homeAccountIdToClientInfo(homeAccountId: String?): String? { + if (homeAccountId.isNullOrBlank()) return null + val parts = homeAccountId.split(".") + if (parts.size != 2 || parts[0].isBlank() || parts[1].isBlank()) return null + val json = "{\"uid\":\"${parts[0]}\",\"utid\":\"${parts[1]}\"}" + return Base64Util.encodeUrlSafeString(json) + } + + @JvmStatic + fun getSchemeAndHost(url: String): String { + val uri = try { URI(url.trim()) } catch (e: Exception) { + throw IllegalArgumentException("Failed to parse URL for scheme and host validation in WebApps. The URL is invalid.", e) + } + val scheme = uri.scheme ?: throw IllegalArgumentException("Failed to parse URL for scheme and host validation in WebApps. The URL is invalid.") + val host = uri.host ?: throw IllegalArgumentException("URL must include a host for WebApps validation.") + return "${scheme.lowercase()}://${host.lowercase()}" + } + + @JvmStatic + fun hasSameSchemeAndHost(urlA: String, urlB: String): Boolean { + return getSchemeAndHost(urlA) == getSchemeAndHost(urlB) + } + } +} diff --git a/common/src/test/java/com/microsoft/identity/common/internal/controllers/BrokerMsalControllerTest.java b/common/src/test/java/com/microsoft/identity/common/internal/controllers/BrokerMsalControllerTest.java index 872a301d57..14827320e3 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/controllers/BrokerMsalControllerTest.java +++ b/common/src/test/java/com/microsoft/identity/common/internal/controllers/BrokerMsalControllerTest.java @@ -22,6 +22,9 @@ // THE SOFTWARE. package com.microsoft.identity.common.internal.controllers; +import static com.microsoft.identity.common.java.exception.ErrorStrings.UI_NOT_ALLOWED; + +import android.content.Intent; import android.os.Build; import android.os.Bundle; @@ -33,6 +36,7 @@ import com.microsoft.identity.common.components.MockPlatformComponentsFactory; import com.microsoft.identity.common.internal.broker.ipc.BrokerOperationBundle; import com.microsoft.identity.common.internal.broker.ipc.IIpcStrategy; +import com.microsoft.identity.common.internal.broker.ipc.WebAppsAdditionalRequiredParameters; import com.microsoft.identity.common.internal.result.MsalBrokerResultAdapter; import com.microsoft.identity.common.java.authorities.Authority; import com.microsoft.identity.common.java.cache.CacheRecord; @@ -40,10 +44,15 @@ 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.commands.webapps.WebAppsGetTokenSubOperationEnvelope; +import com.microsoft.identity.common.java.commands.webapps.WebAppsGetTokenSubOperationRequest; 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 com.microsoft.identity.common.java.util.ObjectMapper; +import com.microsoft.identity.common.shadows.ShadowAcquireTokenInternalBrokerMsalController; +import org.json.JSONObject; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -51,12 +60,18 @@ import org.robolectric.annotation.Config; import java.util.Collections; +import java.util.Locale; import lombok.SneakyThrows; @RunWith(RobolectricTestRunner.class) @Config(sdk = {Build.VERSION_CODES.N}, shadows = {}) public class BrokerMsalControllerTest { + + private static final String NEGOTIATED_VERSION = "19.0"; + private static final String EXPECTED_RESULT = "{\"status\":\"ok\",\"payload\":\"test\"}"; + private static final String EXPECTED_ERROR_RESULT = "Error from the broker side"; + /** * This test simulates a result calling the PrtSsoToken Api where everything goes well talking * to the broker. @@ -202,4 +217,420 @@ public Type getType() { Assert.assertEquals(mockHomeAccountId, cacheRecord.getAccount().getHomeAccountId()); Assert.assertEquals(mockAccountName, cacheRecord.getAccount().getUsername()); } + + @Test + public void testExecuteWebAppRequest_SignOut_Success() throws Exception { + final BrokerMsalController controller = createController(buildStrategyForSilentSuccess()); + final WebAppsAdditionalRequiredParameters addParams = buildAdditionalParams(false); + + // Minimal envelope for sign out; BrokerMsalController needs to just pass the request through. + final String requestJson = "{\"method\":\"SignOut\"}"; + + final String result = controller.executeWebAppRequest( + requestJson, + NEGOTIATED_VERSION, + addParams + ); + + Assert.assertEquals(EXPECTED_RESULT, result); + } + + @Test + public void testExecuteWebAppRequest_GetCookies_Success() throws Exception { + final BrokerMsalController controller = createController(buildStrategyForSilentSuccess()); + final WebAppsAdditionalRequiredParameters addParams = buildAdditionalParams(false); + + // Minimal envelope for get cookies; BrokerMsalController needs to just pass the request through. + final String requestJson = "{\"method\":\"GetCookies\"}"; + + final String result = controller.executeWebAppRequest( + requestJson, + NEGOTIATED_VERSION, + addParams + ); + + Assert.assertEquals(EXPECTED_RESULT, result); + } + + @Test + public void testExecuteWebAppRequest_SilentSuccess_MSALJS() throws Exception { + final BrokerMsalController controller = createController(buildStrategyForSilentSuccess()); + final String requestJson = buildStrictlySilentGetTokenRequestJson(false); + final WebAppsAdditionalRequiredParameters addParams = buildAdditionalParams(false); + + final String result = controller.executeWebAppRequest( + requestJson, + "19.0", + addParams + ); + + Assert.assertEquals(EXPECTED_RESULT, result); + } + + @Test + public void testExecuteWebAppRequest_SilentSuccess_ESTS() throws Exception { + final BrokerMsalController controller = createController(buildStrategyForSilentSuccess()); + final String requestJson = buildStrictlySilentGetTokenRequestJson(true); + final WebAppsAdditionalRequiredParameters addParams = buildAdditionalParams(false); + + final String result = controller.executeWebAppRequest( + requestJson, + "19.0", + addParams + ); + + Assert.assertEquals(EXPECTED_RESULT, result); + } + + @Test + public void testExecuteWebAppRequest_SilentError_FromBroker() throws Exception { + final BrokerMsalController controller = createController(buildStrategyForSilentErrorFromBroker()); + final String requestJson = buildStrictlySilentGetTokenRequestJson(false); + final WebAppsAdditionalRequiredParameters addParams = buildAdditionalParams(false); + + final String result = controller.executeWebAppRequest( + requestJson, + "19.0", + addParams + ); + + Assert.assertTrue(result.contains(EXPECTED_ERROR_RESULT)); + } + + @Test + public void testExecuteWebAppRequest_SilentError_FromController_Parsing() throws Exception { + final BrokerMsalController controller = createController(buildStrategyForSilentErrorFromBroker()); + final WebAppsAdditionalRequiredParameters addParams = buildAdditionalParams(false); + final String malformedRequestJson = new JSONObject() + .put("request", "malformed_request") + .toString(); + final String result = controller.executeWebAppRequest( + malformedRequestJson, + "19.0", + addParams + ); + + Assert.assertTrue(result.contains("Error occurred during request parsing")); + } + + @Test + public void testExecuteWebAppRequest_SilentError_UiNotAllowed() throws Exception { + final BrokerMsalController controller = createController(buildStrategyForSilentErrorFromBroker()); + final WebAppsGetTokenSubOperationRequest request = new WebAppsGetTokenSubOperationRequest( + "account-id", + "clientId", + "https://login.microsoftonline.com/common", + "User.Read", + "https://redirect", + "corr-id", + "login", // Setting to login + false, + null, + null, + null, + false, + null + ); + + WebAppsGetTokenSubOperationEnvelope envelope = + new WebAppsGetTokenSubOperationEnvelope( + "GetToken", + request, + "https://login.microsoftonline.com" // sender + ); + + final String requestJson = ObjectMapper.serializeObjectToJsonString(envelope); + final WebAppsAdditionalRequiredParameters addParams = buildAdditionalParams(false); + + final String result = controller.executeWebAppRequest( + requestJson, + "19.0", + addParams + ); + + Assert.assertTrue(result.contains(UI_NOT_ALLOWED.toUpperCase(Locale.ROOT))); + } + + @Test + public void testExecuteWebAppRequest_SilentError_UiNotAllowed_ESTS() throws Exception { + final BrokerMsalController controller = createController(buildStrategyForSilentErrorFromBroker()); + final WebAppsGetTokenSubOperationRequest request = new WebAppsGetTokenSubOperationRequest( + "account-id", + "clientId", + "https://login.microsoftonline.com/common", + "User.Read", + "https://redirect", + "corr-id", + "login", // Setting to login + true, + null, + null, + null, + false, + null + ); + + WebAppsGetTokenSubOperationEnvelope envelope = + new WebAppsGetTokenSubOperationEnvelope( + "GetToken", + request, + "https://login.microsoftonline.com" // sender + ); + + final String requestJson = ObjectMapper.serializeObjectToJsonString(envelope); + final WebAppsAdditionalRequiredParameters addParams = buildAdditionalParams(false); + + final String result = controller.executeWebAppRequest( + requestJson, + "19.0", + addParams + ); + + Assert.assertTrue(result.contains(UI_NOT_ALLOWED.toUpperCase(Locale.ROOT))); + } + + + @Test + @Config(sdk = {Build.VERSION_CODES.N}, shadows = {ShadowAcquireTokenInternalBrokerMsalController.class}) + public void testExecuteWebAppRequest_ForceInteractive_Success() throws Exception { + + final BrokerMsalController controller = createController(buildStrategyForInteractiveSuccessFromBroker()); + final String requestJson = buildInteractiveGetTokenRequestJson(true); + + WebAppsAdditionalRequiredParameters addParams = new WebAppsAdditionalRequiredParameters( + true, // canShowUi + "test.app.package", + "Mock App", + "1.2.3", + SdkType.MSAL_CPP, + "1.2.3" + ); + + // Queue interactive result for shadowed acquireTokenInternal. + Bundle interactiveBundle = new Bundle(); + interactiveBundle.putString(AuthenticationConstants.Broker.BROKER_WEB_APPS_SUCCESSFUL_RESULT, EXPECTED_RESULT); + ShadowAcquireTokenInternalBrokerMsalController.enqueueResult(interactiveBundle); + + String result = controller.executeWebAppRequest(requestJson, "19.0", addParams); + + Assert.assertEquals(EXPECTED_RESULT, result); + } + + @Test + @Config(sdk = {Build.VERSION_CODES.N}, shadows = {ShadowAcquireTokenInternalBrokerMsalController.class}) + public void testExecuteWebAppRequest_SilentFallbackToInteractive_MSALJS() throws Exception { + + final BrokerMsalController controller = createController(buildStrategyForSilentErrorFromBroker()); + final String requestJson = buildFallbackSilentGetTokenRequestJson(false); + + WebAppsAdditionalRequiredParameters addParams = new WebAppsAdditionalRequiredParameters( + true, // canShowUi + "test.app.package", + "Mock App", + "1.2.3", + SdkType.MSAL_CPP, + "1.2.3" + ); + + // Queue interactive result for shadowed acquireTokenInternal. + Bundle interactiveBundle = new Bundle(); + interactiveBundle.putString(AuthenticationConstants.Broker.BROKER_WEB_APPS_SUCCESSFUL_RESULT, EXPECTED_RESULT); + ShadowAcquireTokenInternalBrokerMsalController.enqueueResult(interactiveBundle); + + String result = controller.executeWebAppRequest(requestJson, "19.0", addParams); + + Assert.assertEquals(EXPECTED_RESULT, result); + } + + @NonNull + private static String buildInteractiveGetTokenRequestJson(final boolean isSts) { + WebAppsGetTokenSubOperationRequest req = new WebAppsGetTokenSubOperationRequest( + null, // homeAccountId (null -> STS flow) + "clientId", + "https://login.microsoftonline.com/common", + "User.Read", + "https://demoapp.com/", + "corr-id", + "login", // prompt forces interactive + isSts, // isSecurityTokenService + null, + null, + null, + false, + null + ); + + WebAppsGetTokenSubOperationEnvelope envelope = new WebAppsGetTokenSubOperationEnvelope( + "GetToken", + req, + isSts ? "https://login.microsoftonline.com" : "https://demoapp.com" + ); + return ObjectMapper.serializeObjectToJsonString(envelope); + } + + private String buildStrictlySilentGetTokenRequestJson(final boolean isSts) throws Exception { + WebAppsGetTokenSubOperationRequest request = new WebAppsGetTokenSubOperationRequest( + "account-id", // homeAccountId + "clientId", // clientId (required) + "https://login.microsoftonline.com/common", // authority + "User.Read", // scopes + "https://demoapp.com/", // redirectUri (required) + "corr-id", // correlationId (optional) + "none", // prompt ("none" for silent) + isSts, // isSecurityTokenService + null, // nonce + null, // state + null, // loginHint + false, // instanceAware + null // extraParameters + ); + + WebAppsGetTokenSubOperationEnvelope envelope = + new WebAppsGetTokenSubOperationEnvelope( + "GetToken", + request, + isSts ? "https://login.microsoftonline.com" : "https://demoapp.com" // sender + ); + + return ObjectMapper.serializeObjectToJsonString(envelope); + } + + private String buildFallbackSilentGetTokenRequestJson(final boolean isSts) throws Exception { + WebAppsGetTokenSubOperationRequest request = new WebAppsGetTokenSubOperationRequest( + "account-id", // homeAccountId + "clientId", // clientId (required) + "https://login.microsoftonline.com/common", // authority + "User.Read", // scopes + "https://demoapp.com/", // redirectUri (required) + "corr-id", // correlationId (optional) + "select_account", // prompt + isSts, // isSecurityTokenService + null, // nonce + null, // state + null, // loginHint + false, // instanceAware + null // extraParameters + ); + + WebAppsGetTokenSubOperationEnvelope envelope = + new WebAppsGetTokenSubOperationEnvelope( + "GetToken", + request, + isSts ? "https://login.microsoftonline.com" : "https://demoapp.com" // sender + ); + + return ObjectMapper.serializeObjectToJsonString(envelope); + } + + private WebAppsAdditionalRequiredParameters buildAdditionalParams(boolean canShowUi) { + // Replace with real builder/factory as needed + return new WebAppsAdditionalRequiredParameters( + canShowUi, + "test.app.package", + "Mock App", + "1.2.3", + SdkType.MSAL_CPP, + "1.2.3" + ); + } + + private BrokerMsalController createController(IIpcStrategy strategy) { + IPlatformComponents components = MockPlatformComponentsFactory.getNonFunctionalBuilder().build(); + return new BrokerMsalController( + InstrumentationRegistry.getInstrumentation().getContext(), + components, + "test.app.package", + java.util.Collections.singletonList(strategy) + ); + } + + private IIpcStrategy buildStrategyForSilentSuccess() { + return new IIpcStrategy() { + @Override + public Bundle communicateToBroker(@NonNull BrokerOperationBundle bundle) { + Bundle out = new Bundle(); + if (bundle.getOperation() == BrokerOperationBundle.Operation.MSAL_HELLO) { + out.putString(AuthenticationConstants.Broker.NEGOTIATED_BP_VERSION_KEY, NEGOTIATED_VERSION); + } else if (bundle.getOperation() == BrokerOperationBundle.Operation.BROKER_WEBAPPS_API_EXECUTE_WEB_APPS_REQUEST) { + // Simulate successful broker execution response + out.putString(AuthenticationConstants.Broker.NEGOTIATED_BP_VERSION_KEY, NEGOTIATED_VERSION); + // Adapter will read whatever key it expects (replace with actual key if different) + out.putString(AuthenticationConstants.Broker.BROKER_WEB_APPS_SUCCESSFUL_RESULT, EXPECTED_RESULT); + } + return out; + } + + @Override + public boolean isSupportedByTargetedBroker(@NonNull String targetedBrokerPackageName) { + return true; + } + + @NonNull + @Override + public Type getType() { + return Type.CONTENT_PROVIDER; + } + }; + } + + private IIpcStrategy buildStrategyForSilentErrorFromBroker() { + return new IIpcStrategy() { + @Override + public Bundle communicateToBroker(@NonNull BrokerOperationBundle bundle) { + Bundle out = new Bundle(); + if (bundle.getOperation() == BrokerOperationBundle.Operation.MSAL_HELLO) { + out.putString(AuthenticationConstants.Broker.NEGOTIATED_BP_VERSION_KEY, NEGOTIATED_VERSION); + } else if (bundle.getOperation() == BrokerOperationBundle.Operation.BROKER_WEBAPPS_API_EXECUTE_WEB_APPS_REQUEST) { + out.putString(AuthenticationConstants.Broker.NEGOTIATED_BP_VERSION_KEY, NEGOTIATED_VERSION); + out.putString(AuthenticationConstants.Broker.BROKER_WEB_APPS_ERROR_RESULT, EXPECTED_ERROR_RESULT); + } else if (bundle.getOperation() == BrokerOperationBundle.Operation.MSAL_GET_INTENT_FOR_INTERACTIVE_REQUEST) { + out.putString(AuthenticationConstants.Broker.NEGOTIATED_BP_VERSION_KEY, NEGOTIATED_VERSION); + // Minimal placeholder intent + Intent interactive = new Intent("com.microsoft.identity.test.INTERACTIVE"); + out.putParcelable("intent", interactive); + } + return out; + } + + @Override + public boolean isSupportedByTargetedBroker(@NonNull String targetedBrokerPackageName) { + return true; + } + + @NonNull + @Override + public Type getType() { + return Type.CONTENT_PROVIDER; + } + }; + } + + private IIpcStrategy buildStrategyForInteractiveSuccessFromBroker() { + return new IIpcStrategy() { + @Override + public Bundle communicateToBroker(@NonNull BrokerOperationBundle bundle) { + Bundle out = new Bundle(); + if (bundle.getOperation() == BrokerOperationBundle.Operation.MSAL_HELLO) { + out.putString(AuthenticationConstants.Broker.NEGOTIATED_BP_VERSION_KEY, NEGOTIATED_VERSION); + } else if (bundle.getOperation() == BrokerOperationBundle.Operation.MSAL_GET_INTENT_FOR_INTERACTIVE_REQUEST) { + out.putString(AuthenticationConstants.Broker.NEGOTIATED_BP_VERSION_KEY, NEGOTIATED_VERSION); + // Minimal placeholder intent + Intent interactive = new Intent("com.microsoft.identity.test.INTERACTIVE"); + out.putParcelable("intent", interactive); + } + return out; + } + + @Override + public boolean isSupportedByTargetedBroker(@NonNull String targetedBrokerPackageName) { + return true; + } + + @NonNull + @Override + public Type getType() { + return Type.CONTENT_PROVIDER; + } + }; + } } diff --git a/common/src/test/java/com/microsoft/identity/common/shadows/ShadowAcquireTokenInternalBrokerMsalController.kt b/common/src/test/java/com/microsoft/identity/common/shadows/ShadowAcquireTokenInternalBrokerMsalController.kt new file mode 100644 index 0000000000..af9bd107a5 --- /dev/null +++ b/common/src/test/java/com/microsoft/identity/common/shadows/ShadowAcquireTokenInternalBrokerMsalController.kt @@ -0,0 +1,69 @@ +// 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.shadows + +import android.os.Bundle +import com.microsoft.identity.common.adal.internal.AuthenticationConstants +import com.microsoft.identity.common.adal.internal.AuthenticationConstants.Broker.NEGOTIATED_BP_VERSION_KEY +import com.microsoft.identity.common.internal.controllers.BrokerMsalController +import com.microsoft.identity.common.java.commands.parameters.InteractiveTokenCommandParameters +import com.microsoft.identity.common.java.exception.BaseException +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements +import java.util.concurrent.ExecutionException + +@Implements(BrokerMsalController::class) +class ShadowAcquireTokenInternalBrokerMsalController { + companion object { + @JvmStatic + private var nextResult: Bundle? = null + + /** + * Queue a Bundle to be returned by the next acquireTokenInternal call. + * After being consumed it is cleared. + */ + @JvmStatic + fun enqueueResult(bundle: Bundle) { + nextResult = bundle + } + } + + /** + * Shadow of BrokerMsalController.acquireTokenInternal. + * Skips IPC/UI and returns a queued or synthesized success Bundle. + */ + @Implementation + @Throws(BaseException::class, InterruptedException::class, ExecutionException::class) + protected fun acquireTokenInternal(parameters: InteractiveTokenCommandParameters): Bundle { + val result = (nextResult ?: Bundle()).apply { + if (!containsKey(AuthenticationConstants.Broker.BROKER_REQUEST_V2_SUCCESS)) { + putBoolean(AuthenticationConstants.Broker.BROKER_REQUEST_V2_SUCCESS, true) + } + if (!containsKey(NEGOTIATED_BP_VERSION_KEY)) { + putString(NEGOTIATED_BP_VERSION_KEY, parameters.requiredBrokerProtocolVersion) + } + } + nextResult = null + return result + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/BrokerDeviceCodeFlowCommandParameters.java b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/BrokerDeviceCodeFlowCommandParameters.java index 85aa4deffc..be11a83902 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/BrokerDeviceCodeFlowCommandParameters.java +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/BrokerDeviceCodeFlowCommandParameters.java @@ -26,6 +26,7 @@ import com.microsoft.identity.common.java.broker.IBrokerAccount; import com.microsoft.identity.common.java.cache.BrokerOAuth2TokenCache; import com.microsoft.identity.common.java.exception.ArgumentException; +import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.request.BrokerRequestType; import com.microsoft.identity.common.java.util.StringUtil; @@ -57,7 +58,7 @@ public class BrokerDeviceCodeFlowCommandParameters extends DeviceCodeFlowCommand private final int callerUid; @Override - public void validate() throws ArgumentException { + public void validate() throws ArgumentException, ClientException { super.validate(); if (getAuthority() == null) { throw new ArgumentException( diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/BrokerInteractiveTokenCommandParameters.java b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/BrokerInteractiveTokenCommandParameters.java index 0e4de0d412..3e076935ea 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/BrokerInteractiveTokenCommandParameters.java +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/BrokerInteractiveTokenCommandParameters.java @@ -26,7 +26,11 @@ import com.microsoft.identity.common.java.broker.IBrokerAccount; import com.microsoft.identity.common.java.cache.BrokerOAuth2TokenCache; import com.microsoft.identity.common.java.exception.ArgumentException; +import com.microsoft.identity.common.java.exception.ClientException; +import com.microsoft.identity.common.java.flighting.CommonFlight; +import com.microsoft.identity.common.java.flighting.CommonFlightsManager; import com.microsoft.identity.common.java.request.BrokerRequestType; +import com.microsoft.identity.common.java.util.IPlatformUtil; import com.microsoft.identity.common.java.util.StringUtil; import java.util.Map; @@ -79,8 +83,11 @@ public class BrokerInteractiveTokenCommandParameters extends InteractiveTokenCom // Parameter representing if this broker request is an Account Transfer request private final boolean isAccountTransferRequest; + // Optional field to persist state for WebApps interactive token requests. + private final String webAppsState; + @Override - public void validate() throws ArgumentException { + public void validate() throws ArgumentException, ClientException { super.validate(); if (getAuthority() == null) { throw new ArgumentException( @@ -118,7 +125,14 @@ public void validate() throws ArgumentException { "OAuth2Cache not an instance of BrokerOAuth2TokenCache" ); } - if (!getPlatformComponents().getPlatformUtil().isValidCallingApp(getRedirectUri(), getCallerPackageName())) { + final IPlatformUtil platformUtil = getPlatformComponents().getPlatformUtil(); + if (!CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.DISABLE_WEB_APPS_API) + && getRequestType() == BrokerRequestType.WEB_APPS) { + // For web apps, we have a different redirect URI from our standard Android one. + platformUtil.isValidCallingAppForWebApps(getCallerUid()); + return; + } + if (!platformUtil.isValidCallingApp(getRedirectUri(), getCallerPackageName())) { throw new ArgumentException( ArgumentException.ACQUIRE_TOKEN_OPERATION_NAME, ArgumentException.REDIRECT_URI_ARGUMENT_NAME, "The redirect URI doesn't match the uri" + diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/BrokerSilentTokenCommandParameters.java b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/BrokerSilentTokenCommandParameters.java index 3bd82e5e0a..4c9a8a36c5 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/BrokerSilentTokenCommandParameters.java +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/BrokerSilentTokenCommandParameters.java @@ -26,7 +26,11 @@ import com.microsoft.identity.common.java.broker.IBrokerAccount; import com.microsoft.identity.common.java.cache.BrokerOAuth2TokenCache; import com.microsoft.identity.common.java.exception.ArgumentException; +import com.microsoft.identity.common.java.exception.ClientException; +import com.microsoft.identity.common.java.flighting.CommonFlight; +import com.microsoft.identity.common.java.flighting.CommonFlightsManager; import com.microsoft.identity.common.java.request.BrokerRequestType; +import com.microsoft.identity.common.java.util.IPlatformUtil; import com.microsoft.identity.common.java.util.StringUtil; import lombok.EqualsAndHashCode; @@ -75,7 +79,7 @@ public boolean isRequestForResourceAccount() { } @Override - public void validate() throws ArgumentException { + public void validate() throws ArgumentException, ClientException { if (callerUid == 0) { throw new ArgumentException( ArgumentException.ACQUIRE_TOKEN_SILENT_OPERATION_NAME, @@ -100,15 +104,6 @@ public void validate() throws ArgumentException { "mClientId", "Client Id is not set" ); } - - if (!getPlatformComponents().getPlatformUtil().isValidCallingApp(getRedirectUri(), getCallerPackageName())) { - throw new ArgumentException( - ArgumentException.ACQUIRE_TOKEN_SILENT_OPERATION_NAME, - "mRedirectUri", "The redirect URI doesn't match the uri" + - " generated with caller package name and signature" - ); - } - if (!(getOAuth2TokenCache() instanceof BrokerOAuth2TokenCache)) { throw new ArgumentException( ArgumentException.ACQUIRE_TOKEN_SILENT_OPERATION_NAME, @@ -122,6 +117,19 @@ public void validate() throws ArgumentException { "mCallerPackageName", "Broker Account is null" ); } - + final IPlatformUtil platformUtil = getPlatformComponents().getPlatformUtil(); + if (!CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.DISABLE_WEB_APPS_API) + && getRequestType() == BrokerRequestType.WEB_APPS) { + // For web apps, we have a different redirect URI from our standard Android one. + platformUtil.isValidCallingAppForWebApps(getCallerUid()); + return; + } + if (!platformUtil.isValidCallingApp(getRedirectUri(), getCallerPackageName())) { + throw new ArgumentException( + ArgumentException.ACQUIRE_TOKEN_SILENT_OPERATION_NAME, + "mRedirectUri", "The redirect URI doesn't match the uri" + + " generated with caller package name and signature" + ); + } } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/RopcTokenCommandParameters.java b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/RopcTokenCommandParameters.java index 96c5d680ce..32c2bad406 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/RopcTokenCommandParameters.java +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/RopcTokenCommandParameters.java @@ -23,6 +23,7 @@ package com.microsoft.identity.common.java.commands.parameters; import com.microsoft.identity.common.java.exception.ArgumentException; +import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.util.StringUtil; import lombok.EqualsAndHashCode; @@ -39,7 +40,7 @@ public class RopcTokenCommandParameters extends TokenCommandParameters { private final String mPassword; @Override - public void validate() throws ArgumentException { + public void validate() throws ArgumentException, ClientException { if (StringUtil.isNullOrEmpty(mUsername)) { throw new ArgumentException( ArgumentException.ACQUIRE_TOKEN_WITH_PASSWORD_OPERATION_NAME, diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/SilentTokenCommandParameters.java b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/SilentTokenCommandParameters.java index 8c46f33fc0..e88657e00b 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/SilentTokenCommandParameters.java +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/SilentTokenCommandParameters.java @@ -45,7 +45,7 @@ public class SilentTokenCommandParameters extends TokenCommandParameters { private static final Object sLock = new Object(); @Override - public void validate() throws ArgumentException { + public void validate() throws ArgumentException, ClientException { super.validate(); if (getAccount() == null) { diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/TokenCommandParameters.java b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/TokenCommandParameters.java index 46d3c61f24..fa3d4f36c8 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/TokenCommandParameters.java +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/parameters/TokenCommandParameters.java @@ -27,6 +27,7 @@ import com.microsoft.identity.common.java.authorities.Authority; import com.microsoft.identity.common.java.authscheme.AbstractAuthenticationScheme; import com.microsoft.identity.common.java.dto.IAccountRecord; +import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.logging.Logger; import com.microsoft.identity.common.java.util.StringUtil; @@ -81,7 +82,7 @@ public String getMamEnrollmentId(){ return mamEnrollmentId; } - public void validate() throws ArgumentException { + public void validate() throws ArgumentException, ClientException { final String methodName = ":validate"; Logger.verbose( diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/MatsProperties.kt b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/MatsProperties.kt new file mode 100644 index 0000000000..ea171d50c3 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/MatsProperties.kt @@ -0,0 +1,39 @@ +// 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.webapps + +import com.google.gson.annotations.SerializedName + +/** + * Holds the MATS data blob which will be passed to MSAL JS for telemetry purposes. + * Note: This class isn't going to be used until the design for the MATS blob is complete. + * Once that is complete, we will need to pass this data for each error object and getToken suboperation. + */ +data class MatsProperties( + @SerializedName(FIELD_MATS_DATA) + val matsData: String +) { + companion object { + const val FIELD_MATS_DATA = "MATS" + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppBrokerErrorCode.kt b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppBrokerErrorCode.kt new file mode 100644 index 0000000000..5ab87f2adb --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppBrokerErrorCode.kt @@ -0,0 +1,122 @@ +// 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.webapps + +import com.google.gson.JsonParseException +import com.google.gson.JsonSyntaxException +import com.microsoft.identity.common.java.exception.ClientException +import com.microsoft.identity.common.java.exception.ErrorStrings +import com.microsoft.identity.common.java.exception.ServiceException +import com.microsoft.identity.common.java.exception.UiRequiredException +import com.microsoft.identity.common.java.exception.UnsupportedBrokerException +import com.microsoft.identity.common.java.exception.UserCancelException +import org.json.JSONException +import java.io.IOException + +/** + * Error codes for WebApps operations. + */ +enum class WebAppBrokerErrorCode { + // Any unexpected error + UNEXPECTED, + // The request was cancelled by the user. + USER_CANCEL, + // User interaction is required to complete the request. This option is only applicable to requests made silently. + USER_INTERACTION_REQUIRED, + // This is Edge-specific and will be returned when the platform API needs to show UI, + // but is not allowed to do so because of the canShowUI flag being false when API is called by Edge + UI_NOT_ALLOWED, + // Network is unavailable and request cannot be completed. + NO_NETWORK, + // Errors indicating operation can be re-tried + TRANSIENT_ERROR, + // Errors indicating operation cannot be re-tried. For example, when provided authority is not recognized/supported by broker + PERSISTENT_ERROR, + // Account is not found in the cache. + ACCOUNT_UNAVAILABLE, + // Platform broker invocation is disabled and cannot be performed. + DISABLED, + // There were too many requests in a short period of time and the current request is throttled. + THROTTLED; + + companion object { + + /** + * Map a Throwable to a WebAppBrokerErrorCode. + * + * @param t The Throwable to map. + * @return The corresponding WebAppBrokerErrorCode. + */ + fun fromThrowable(t: Throwable): WebAppBrokerErrorCode { + when (t) { + is UserCancelException -> return USER_CANCEL + is UiRequiredException -> return USER_INTERACTION_REQUIRED + is IOException -> return NO_NETWORK + is JsonParseException, + is JsonSyntaxException, + is JSONException, + is IllegalStateException, + is UnsupportedBrokerException, + is NullPointerException -> return PERSISTENT_ERROR + } + + // ClientException specific mapping + if (t is ClientException) { + val code = t.errorCode?.lowercase() + return mapClientErrorCode(code) + } + + // ServiceException mapping (HTTP based) + if (t is ServiceException) { + val http = t.httpStatusCode + if (http == 429) return THROTTLED + return PERSISTENT_ERROR + } + + return UNEXPECTED + } + + /** + * Map ClientException error codes to WebAppBrokerErrorCode. + * + * @param code The ClientException error code. + * @return The corresponding WebAppBrokerErrorCode. + */ + private fun mapClientErrorCode(code: String?): WebAppBrokerErrorCode { + return when (code) { + ClientException.DEVICE_NETWORK_NOT_AVAILABLE, + ErrorStrings.NO_NETWORK_CONNECTION_POWER_OPTIMIZATION -> NO_NETWORK + ClientException.MALFORMED_URL, + ClientException.MISSING_PARAMETER, + ErrorStrings.INVALID_REQUEST -> PERSISTENT_ERROR + ErrorStrings.SOCKET_TIMEOUT, + ErrorStrings.IO_ERROR -> TRANSIENT_ERROR + ErrorStrings.UNSUPPORTED_BROKER_VERSION_ERROR_CODE, + ErrorStrings.FLIGHT_DISABLED -> DISABLED + ClientException.ACCOUNT_NOT_FOUND -> ACCOUNT_UNAVAILABLE + ErrorStrings.UI_NOT_ALLOWED -> UI_NOT_ALLOWED + else -> UNEXPECTED + } + } + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppError.kt b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppError.kt new file mode 100644 index 0000000000..f41f576a51 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppError.kt @@ -0,0 +1,80 @@ +// 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.webapps + +import com.google.gson.annotations.SerializedName + +/** + * This class represents an error that occurs during WebApps operations. + */ +data class WebAppError( + @SerializedName(FIELD_ERROR) + val errorCode: String? = BROKER_ERROR_CODE, + + @SerializedName(FIELD_DESCRIPTION) + val description: String, + + @SerializedName(FIELD_EXTRA) + val extra: WebAppErrorDetails +) { + companion object { + const val FIELD_ERROR = "error" + const val FIELD_DESCRIPTION = "description" + const val FIELD_EXTRA = "ext" + const val BROKER_ERROR_CODE = "OSError" // Per the protocol, this is the dedicated error code for broker-related errors. + } + + /** + * Secondary constructor that creates a WebAppError from a Throwable. + * We try to determine the corresponding error status code from the throwable. + * + * @param throwable The Throwable that caused the error. + * @param description A description of the error. + */ + constructor(throwable: Throwable, description: String) : this( + errorCode = BROKER_ERROR_CODE, + description = description, + extra = WebAppErrorDetails( + error = 0, + status = WebAppBrokerErrorCode.fromThrowable(throwable).name + ) + ) + + /** + * Secondary constructor that creates a WebAppError from a Throwable and MatsProperties. + * We try to determine the corresponding error status code from the throwable. + * + * @param throwable The Throwable that caused the error. + * @param description A description of the error. + * @param matsProperties Additional properties related to the error context. + */ + constructor(throwable: Throwable, description: String, matsProperties: MatsProperties) : this( + errorCode = BROKER_ERROR_CODE, + description = description, + extra = WebAppErrorDetails( + error = 0, + status = WebAppBrokerErrorCode.fromThrowable(throwable).name, + properties = matsProperties + ) + ) +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppErrorDetails.kt b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppErrorDetails.kt new file mode 100644 index 0000000000..8b608dff2c --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppErrorDetails.kt @@ -0,0 +1,49 @@ +// 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.webapps + +import com.google.gson.annotations.SerializedName + +/** + * Extra error details for WebApp operations + */ +data class WebAppErrorDetails( + @SerializedName(FIELD_ERROR) + val error: Int? = 0, + + @SerializedName(FIELD_PROTOCOL_ERROR) + val protocolError: String? = null, + + @SerializedName(FIELD_STATUS) + val status: String, + + @SerializedName(FIELD_PROPERTIES) + val properties: MatsProperties? = null +) { + companion object { + const val FIELD_ERROR = "error" + const val FIELD_PROTOCOL_ERROR = "protocol_error" + const val FIELD_STATUS = "status" + const val FIELD_PROPERTIES = "properties" + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsAccountItem.kt b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsAccountItem.kt new file mode 100644 index 0000000000..a92d2f21c0 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsAccountItem.kt @@ -0,0 +1,48 @@ +// 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.webapps + +import com.google.gson.annotations.SerializedName + +/** + * Represents an account item in Web Apps broker communication. + */ +data class WebAppsAccountItem( + // Required; UPN + @SerializedName(FIELD_USER_NAME) + val userName: String, + + // Required. + @SerializedName(FIELD_HOME_ACCOUNT_ID) + val homeAccountId: String, + + // Optional; we will most likely not use this field, as we will send the properties one level up. + @SerializedName(FIELD_PROPERTIES) + val properties: MatsProperties? = null +) { + companion object { + const val FIELD_USER_NAME = "userName" + const val FIELD_HOME_ACCOUNT_ID = "id" + const val FIELD_PROPERTIES = "properties" + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationEnvelope.kt b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationEnvelope.kt new file mode 100644 index 0000000000..7d2d921fc9 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationEnvelope.kt @@ -0,0 +1,46 @@ +// 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.webapps + +import com.google.gson.annotations.SerializedName + +/** + * Envelope for WebAppsGetTokenSubOperation requests. + */ +data class WebAppsGetTokenSubOperationEnvelope( + + @SerializedName(FIELD_METHOD) + val method: String, + + @SerializedName(FIELD_REQUEST) + val request: WebAppsGetTokenSubOperationRequest, + + @SerializedName(FIELD_SENDER) + val sender: String +) { + companion object { + const val FIELD_METHOD = "method" + const val FIELD_REQUEST = "request" + const val FIELD_SENDER = "sender" + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationRequest.kt b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationRequest.kt new file mode 100644 index 0000000000..697af54240 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationRequest.kt @@ -0,0 +1,100 @@ +// 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.webapps + +import com.google.gson.annotations.SerializedName + +/** + * Request parameters for WebAppsGetTokenSubOperation. + */ +data class WebAppsGetTokenSubOperationRequest( + // If from MSAL JS, this is required. If from ESTS, this is optional. + @SerializedName(FIELD_HOME_ACCOUNT_ID) + val homeAccountId: String? = null, + + // Required. + @SerializedName(FIELD_CLIENT_ID) + val clientId: String, + + // Optional. If not passed, broker will use the default common authority. + @SerializedName(FIELD_AUTHORITY) + val authority: String? = DEFAULT_AUTHORITY, + + // Required; of type List. + @SerializedName(FIELD_SCOPES) + val scopes: String, + + // Required. + @SerializedName(FIELD_REDIRECT_URI) + val redirectUri: String, + + // Optional. + @SerializedName(FIELD_CORRELATION_ID) + val correlationId: String? = null, + + // Optional; possible values are "login", "consent", "select_account", "none". + @SerializedName(FIELD_PROMPT) + val prompt: String? = null, + + // If not provided, we assume this is false. + @SerializedName(FIELD_IS_SECURITY_TOKEN_SERVICE) + val isSecurityTokenService: Boolean = false, + + // Optional. + @SerializedName(FIELD_NONCE) + val nonce : String? = null, + + // Optional; OAuth protocol "state" parameter. We pass it back as-is in the response. + @SerializedName(FIELD_STATE) + val state: String? = null, + + // Optional. + @SerializedName(FIELD_LOGIN_HINT) + val loginHint: String? = null, + + // Optional. + @SerializedName(FIELD_INSTANCE_AWARE) + val instanceAware : Boolean = false, + + // Optional; additional extra query parameters to include in the token request. + // Note: PoP token parameters will come through here. + @SerializedName(FIELD_EXTRA_PARAMETERS) + val extraParameters: Map? = null +) { + companion object { + const val FIELD_HOME_ACCOUNT_ID = "accountId" + const val FIELD_CLIENT_ID = "clientId" + const val FIELD_AUTHORITY = "authority" + const val FIELD_SCOPES = "scope" + const val FIELD_REDIRECT_URI = "redirectUri" + const val FIELD_CORRELATION_ID = "correlationId" + const val FIELD_PROMPT = "prompt" + const val FIELD_IS_SECURITY_TOKEN_SERVICE = "isSts" + const val FIELD_NONCE = "nonce" + const val FIELD_STATE = "state" + const val FIELD_LOGIN_HINT = "loginHint" + const val FIELD_INSTANCE_AWARE = "instanceAware" + const val FIELD_EXTRA_PARAMETERS = "extraParameters" + const val DEFAULT_AUTHORITY = "https://login.microsoftonline.com/common" + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationResponse.kt b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationResponse.kt new file mode 100644 index 0000000000..c4420cbb0a --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsGetTokenSubOperationResponse.kt @@ -0,0 +1,74 @@ +// 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.webapps + +import com.google.gson.annotations.SerializedName + +/** + * Request parameters for WebAppsGetTokenSubOperation + */ +data class WebAppsGetTokenSubOperationResponse( + + // Optional; state passed in the request + @SerializedName(FIELD_STATE) + val state: String? = null, + + // Required. + @SerializedName(FIELD_EXPIRES_IN) + val expiresIn: Long, + + // Optional for now, but will be required once schema design is finalized. + @SerializedName(FIELD_PROPERTIES) + val properties: MatsProperties? = null, + + // Required; base64 string containing uid and utid. + @SerializedName(FIELD_CLIENT_INFO) + val clientInfo: String, + + // Required. + @SerializedName(FIELD_ACCOUNT) + val account: WebAppsAccountItem, + + // Required. + @SerializedName(FIELD_ID_TOKEN) + val idToken: String, + + // Required. + @SerializedName(FIELD_ACCESS_TOKEN) + val accessToken: String, + + // Required. + @SerializedName(FIELD_SCOPES) + val scopes: String +) { + companion object { + const val FIELD_STATE = "state" + const val FIELD_EXPIRES_IN = "expires_in" + const val FIELD_PROPERTIES = "properties" + const val FIELD_CLIENT_INFO = "client_info" + const val FIELD_ACCOUNT = "account" + const val FIELD_ID_TOKEN = "id_token" + const val FIELD_ACCESS_TOKEN = "access_token" + const val FIELD_SCOPES = "scope" + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsSupportedContracts.kt b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsSupportedContracts.kt new file mode 100644 index 0000000000..2824cc9eb0 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/commands/webapps/WebAppsSupportedContracts.kt @@ -0,0 +1,45 @@ +// 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.webapps + +import java.io.Serializable + +/** + * Supported contracts for Web Apps Sub Operations + */ +data class WebAppsSupportedContracts( + val contracts: List = listOf(GET_TOKEN, SIGN_OUT, GET_COOKIES) +) : Serializable { + companion object { + const val GET_TOKEN = "GetToken" + const val SIGN_OUT = "SignOut" + const val GET_COOKIES = "GetCookies" + } + + override fun toString(): String { + return contracts.joinToString( + separator = ", ", + prefix = "[", + postfix = "]") + } +} diff --git a/common4j/src/main/com/microsoft/identity/common/java/eststelemetry/PublicApiId.java b/common4j/src/main/com/microsoft/identity/common/java/eststelemetry/PublicApiId.java index d3bb0ed47e..64bfbd75d4 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/eststelemetry/PublicApiId.java +++ b/common4j/src/main/com/microsoft/identity/common/java/eststelemetry/PublicApiId.java @@ -164,4 +164,12 @@ public final class PublicApiId { public static final String NATIVE_AUTH_JIT_CHALLENGE_AUTH_METHOD = "255"; public static final String NATIVE_AUTH_JIT_SUBMIT_CHALLENGE = "256"; //endregion + + // region WebApps APIs + + // WebAppsGetTokenSubOperation + //============================================================================================== + public static final String WEBAPPS_GET_TOKEN_SILENT = "311"; + + //endregion } diff --git a/common4j/src/main/com/microsoft/identity/common/java/exception/ErrorStrings.java b/common4j/src/main/com/microsoft/identity/common/java/exception/ErrorStrings.java index 053f0f9adf..c837d4f63e 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/exception/ErrorStrings.java +++ b/common4j/src/main/com/microsoft/identity/common/java/exception/ErrorStrings.java @@ -475,6 +475,11 @@ private ErrorStrings() { */ public static final String ALL_WEBAPP_SIGN_OUTS_FAILED = "all_webapp_sign_outs_failed"; + /** + * A specific web app error for Edge. + */ + public static final String UI_NOT_ALLOWED = "ui_not_allowed"; + /** * The requested feature flight is disabled. */ diff --git a/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java b/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java index 3efcdb01b8..1116294113 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java +++ b/common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java @@ -182,6 +182,11 @@ public enum CommonFlight implements IFlightConfig { */ ENABLE_OPENID_ISSUER_VALIDATION_REPORTING("EnableOpenIdIssuerValidationReporting", true), + /** + * Flight to disable Web Apps API. + */ + DISABLE_WEB_APPS_API("DisableWebAppsApi", false), + /** * Flight to control whether or not to use in memory cache for accounts and credentials. */ diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectory.java b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectory.java index 533a4ea183..a041d6f8c8 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectory.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/azureactivedirectory/AzureActiveDirectory.java @@ -314,4 +314,52 @@ private static List deserializeClouds(final String js return new Gson().fromJson(jsonCloudArray, listType); } + /** + * Builds and validates the authority from the WebApp sender URL. + * + * @param senderUrl The WebApp sender URL. + * @return The normalized authority URL string. + * @throws ClientException If the URL is malformed or the host is not recognized/validated. + */ + public static String buildAndValidateAuthorityFromWebAppSender(final String senderUrl) throws ClientException { + final String methodTag = TAG + ":buildAndValidateAuthorityFromWebAppSender"; + try { + final URI uri = new URI(senderUrl); + final String scheme = uri.getScheme(); + if (scheme == null) { + throw new ClientException(ClientException.MALFORMED_URL, "Missing scheme in sender url"); + } + final String host = uri.getHost(); + if (host == null) { + throw new ClientException(ClientException.MALFORMED_URL, "Missing host in sender url"); + } + + final URI normalized = new URI(scheme + "://" + host + "/common"); + + ensureCloudDiscoveryComplete(); + final URL authorityUrl = normalized.toURL(); + + if (!hasCloudHost(authorityUrl)) { + Logger.warn(methodTag, "Host not found in known AAD clouds: " + host); + throw new ClientException( + ClientException.MALFORMED_URL, + "Unrecognized AAD cloud host: " + host + ); + } + + if (!isValidCloudHost(authorityUrl)) { + Logger.warn(methodTag, "Host not validated as AAD cloud: " + host); + throw new ClientException( + ClientException.MALFORMED_URL, + "AAD cloud host not validated: " + host + ); + } + + return normalized.toString(); + } catch (final URISyntaxException e) { + throw new ClientException(ClientException.MALFORMED_URL, "Invalid sender url syntax", e); + } catch (final MalformedURLException e) { + throw new ClientException(ClientException.MALFORMED_URL, "Invalid authority URL formed", e); + } + } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/OpenIdConnectPromptParameter.java b/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/OpenIdConnectPromptParameter.java index 1161f39304..e7bcd54664 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/OpenIdConnectPromptParameter.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/OpenIdConnectPromptParameter.java @@ -77,6 +77,32 @@ public String toString() { return this.name().toLowerCase(Locale.ROOT); } + /** + * Utility method to map a string to an OpenIdConnectPromptParameter enum value. + * + * @param promptParameterString the string representation of the prompt parameter (case-insensitive) + * @return the corresponding OpenIdConnectPromptParameter enum value, or UNSET if the input is null, empty, or unrecognized + */ + public static OpenIdConnectPromptParameter fromString(@Nullable final String promptParameterString) { + if (promptParameterString == null || promptParameterString.isEmpty()) { + return UNSET; + } + switch (promptParameterString.toLowerCase(Locale.ROOT)) { + case "none": + return NONE; + case "select_account": + return SELECT_ACCOUNT; + case "login": + return LOGIN; + case "consent": + return CONSENT; + case "create": + return CREATE; + default: + return UNSET; + } + } + /** * Utility method to map Adal PromptBehavior with OpenIdConnectPromptParameter diff --git a/common4j/src/main/com/microsoft/identity/common/java/request/BrokerRequestType.java b/common4j/src/main/com/microsoft/identity/common/java/request/BrokerRequestType.java index f58d824f41..68534c0a4e 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/request/BrokerRequestType.java +++ b/common4j/src/main/com/microsoft/identity/common/java/request/BrokerRequestType.java @@ -15,6 +15,11 @@ public enum BrokerRequestType { /** * Request type indicates a token request which is performed during an interrupt flow. */ - RESOLVE_INTERRUPT + RESOLVE_INTERRUPT, + + /** + * Request type indicates a token request for web apps. + */ + WEB_APPS } diff --git a/common4j/src/main/com/microsoft/identity/common/java/util/IPlatformUtil.java b/common4j/src/main/com/microsoft/identity/common/java/util/IPlatformUtil.java index e16a7599b9..e4de9e473c 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/util/IPlatformUtil.java +++ b/common4j/src/main/com/microsoft/identity/common/java/util/IPlatformUtil.java @@ -67,6 +67,15 @@ public interface IPlatformUtil { */ boolean isValidCallingApp(@NonNull final String redirectUri, @NonNull final String packageName); + /** + * Validates that the calling uid belongs to an acceptable app for web apps. + * + * @param callingUid the calling uid to validate + * @throws ClientException if the calling uid is not valid + * @throws UnsupportedOperationException if the instance does not support this operation + */ + void isValidCallingAppForWebApps(int callingUid) throws ClientException, UnsupportedOperationException; + /** * Retrieve the Intune MAM enrollment id for the given user and package from * the Intune Company Portal, if available. diff --git a/common4j/src/test/com/microsoft/identity/common/java/providers/oauth2/OpenIdConnectPromptParameterTest.java b/common4j/src/test/com/microsoft/identity/common/java/providers/oauth2/OpenIdConnectPromptParameterTest.java index c9df371964..9a1645ba95 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/providers/oauth2/OpenIdConnectPromptParameterTest.java +++ b/common4j/src/test/com/microsoft/identity/common/java/providers/oauth2/OpenIdConnectPromptParameterTest.java @@ -60,4 +60,56 @@ public void testPromptBehaviorForcePrompt(){ final OpenIdConnectPromptParameter promptParameter = OpenIdConnectPromptParameter._fromPromptBehavior("FORCE_PROMPT"); assertEquals(promptParameter, OpenIdConnectPromptParameter.LOGIN); } + + @Test + public void testFromStringNull() { + assertEquals(OpenIdConnectPromptParameter.UNSET, + OpenIdConnectPromptParameter.fromString(null)); + } + + @Test + public void testFromStringEmpty() { + assertEquals(OpenIdConnectPromptParameter.UNSET, + OpenIdConnectPromptParameter.fromString("")); + } + + @Test + public void testFromStringNoneCaseInsensitive() { + assertEquals(OpenIdConnectPromptParameter.NONE, + OpenIdConnectPromptParameter.fromString("none")); + assertEquals(OpenIdConnectPromptParameter.NONE, + OpenIdConnectPromptParameter.fromString("NONE")); + assertEquals(OpenIdConnectPromptParameter.NONE, + OpenIdConnectPromptParameter.fromString("NoNe")); + } + + @Test + public void testFromStringSelectAccount() { + assertEquals(OpenIdConnectPromptParameter.SELECT_ACCOUNT, + OpenIdConnectPromptParameter.fromString("select_account")); + } + + @Test + public void testFromStringLogin() { + assertEquals(OpenIdConnectPromptParameter.LOGIN, + OpenIdConnectPromptParameter.fromString("login")); + } + + @Test + public void testFromStringConsent() { + assertEquals(OpenIdConnectPromptParameter.CONSENT, + OpenIdConnectPromptParameter.fromString("consent")); + } + + @Test + public void testFromStringCreate() { + assertEquals(OpenIdConnectPromptParameter.CREATE, + OpenIdConnectPromptParameter.fromString("create")); + } + + @Test + public void testFromStringUnrecognized() { + assertEquals(OpenIdConnectPromptParameter.UNSET, + OpenIdConnectPromptParameter.fromString("foobar")); + } } diff --git a/common4j/src/testFixtures/java/com/microsoft/identity/common/components/MockPlatformComponentsFactory.java b/common4j/src/testFixtures/java/com/microsoft/identity/common/components/MockPlatformComponentsFactory.java index f1b716ae47..ea49745315 100644 --- a/common4j/src/testFixtures/java/com/microsoft/identity/common/components/MockPlatformComponentsFactory.java +++ b/common4j/src/testFixtures/java/com/microsoft/identity/common/components/MockPlatformComponentsFactory.java @@ -151,6 +151,10 @@ public boolean isValidCallingApp(@NonNull String redirectUri, @NonNull String pa throw new UnsupportedOperationException(); } + @Override + public void isValidCallingAppForWebApps(int callingUid) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } @Nullable @Override public String getEnrollmentId(@NonNull String userId, @NonNull String packageName) {