Skip to content
Merged
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
vNext
----------
- [MINOR] Update Broker ATS flow for Resource Account (#2704)
- [PATCH] Handle BadTokenException gracefully in CBA dialogs (#2731)
- [MINOR] Enable ShouldPreserveWebViewFlowOnSslError in Common (#2741)
- [MINOR] Add AAD authority validation for DUNA flow (#2740)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ public class BrokerSilentTokenCommandParameters extends SilentTokenCommandParame
@Expose
private final String homeTenantId;

/**
* Flag to indicate whether the silent token request is for a resource account.
*/
@Expose
private final boolean isRequestForResourceAccount;

@Override
public void validate() throws ArgumentException {
if (callerUid == 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,25 @@ private static boolean shouldBeConvertedToUiRequiredException(final String oAuth
* @param errorResponse
* @return ServiceException, UiRequiredException
*/
public static ServiceException getExceptionFromTokenErrorResponse(@NonNull final TokenErrorResponse errorResponse) {
static ServiceException getExceptionFromTokenErrorResponse(@NonNull final TokenErrorResponse errorResponse) {
return getExceptionFromTokenErrorResponse(errorResponse, true);
}

/**
* Get an exception object from the given oAuth values.
*
* @param errorResponse - TokenErrorResponse containing error information
* @param allowConvertToUiRequiredException - if true, will convert to UiRequiredException if applicable
* @return ServiceException, UiRequiredException
*/
static ServiceException getExceptionFromTokenErrorResponse(
@NonNull final TokenErrorResponse errorResponse,
final boolean allowConvertToUiRequiredException
) {

final ServiceException outErr;

if (shouldBeConvertedToUiRequiredException(errorResponse.getError())) {
if (allowConvertToUiRequiredException && shouldBeConvertedToUiRequiredException(errorResponse.getError())) {
outErr = new UiRequiredException(
errorResponse.getError(),
errorResponse.getErrorDescription());
Expand Down Expand Up @@ -276,33 +290,39 @@ public static ServiceException getExceptionFromTokenErrorResponse(@Nullable fina
@NonNull final TokenErrorResponse errorResponse) {

if (isIntunePolicyRequiredError(errorResponse)) {
if (commandParameters == null || !(isBrokerTokenCommandParameters(commandParameters))) {
Logger.warn(TAG, "In order to properly construct the IntuneAppProtectionPolicyRequiredException we need the command parameters to be supplied. Returning as service exception instead.");
return getExceptionFromTokenErrorResponse(errorResponse);
}
IntuneAppProtectionPolicyRequiredException policyRequiredException;
if (commandParameters instanceof BrokerInteractiveTokenCommandParameters) {
policyRequiredException = new IntuneAppProtectionPolicyRequiredException(
errorResponse.getError(),
errorResponse.getErrorDescription(),
(BrokerInteractiveTokenCommandParameters) commandParameters
);
} else {
policyRequiredException = new IntuneAppProtectionPolicyRequiredException(
errorResponse.getError(),
errorResponse.getErrorDescription(),
(BrokerSilentTokenCommandParameters) commandParameters
);
}
policyRequiredException.setSubErrorCode(errorResponse.getSubError());
setHttpResponseUsingTokenErrorResponse(policyRequiredException, errorResponse);
if (isBrokerTokenCommandParameters(commandParameters)) {
final IntuneAppProtectionPolicyRequiredException policyRequiredException;
if (commandParameters instanceof BrokerInteractiveTokenCommandParameters) {
policyRequiredException = new IntuneAppProtectionPolicyRequiredException(
errorResponse.getError(),
errorResponse.getErrorDescription(),
(BrokerInteractiveTokenCommandParameters) commandParameters
);
} else {
policyRequiredException = new IntuneAppProtectionPolicyRequiredException(
errorResponse.getError(),
errorResponse.getErrorDescription(),
(BrokerSilentTokenCommandParameters) commandParameters
);
}
policyRequiredException.setSubErrorCode(errorResponse.getSubError());
setHttpResponseUsingTokenErrorResponse(policyRequiredException, errorResponse);

return policyRequiredException;
} else {
return policyRequiredException;
}
Logger.warn(TAG, "In order to properly construct the IntuneAppProtectionPolicyRequiredException we need the command parameters to be supplied. Returning as service exception instead.");
return getExceptionFromTokenErrorResponse(errorResponse);
}


if (commandParameters instanceof BrokerSilentTokenCommandParameters) {
final BrokerSilentTokenCommandParameters brokerSilentTokenCommandParameters =
(BrokerSilentTokenCommandParameters) commandParameters;
// For resource account acquire token silent requests, client should not start any interactive requests
// hence not sending UiRequiredException, instead error will be sent using ServiceException.
boolean allowUiRequiredException = !brokerSilentTokenCommandParameters.isRequestForResourceAccount();
return getExceptionFromTokenErrorResponse(errorResponse, allowUiRequiredException);
}
return getExceptionFromTokenErrorResponse(errorResponse);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
// THE SOFTWARE.
package com.microsoft.identity.common.java.jwt

/**
* Constants for JWT key use types to be used with for "use" claim in JWT header.
*/
object JwtKeyUseTypes {
const val RESOURCE_ACCOUNT: String = "resource_account"
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,48 @@

package com.microsoft.identity.common.java.controllers;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.microsoft.identity.common.java.TestUtils;
import com.microsoft.identity.common.java.authorities.Authority;
import com.microsoft.identity.common.java.commands.parameters.BrokerInteractiveTokenCommandParameters;
import com.microsoft.identity.common.java.commands.parameters.BrokerSilentTokenCommandParameters;
import com.microsoft.identity.common.java.constants.OAuth2ErrorCode;
import com.microsoft.identity.common.java.constants.OAuth2SubErrorCode;
import com.microsoft.identity.common.java.exception.BaseException;
import com.microsoft.identity.common.java.exception.ClientException;
import com.microsoft.identity.common.java.exception.IntuneAppProtectionPolicyRequiredException;
import com.microsoft.identity.common.java.exception.ServiceException;
import com.microsoft.identity.common.java.exception.TerminalException;
import com.microsoft.identity.common.java.exception.UiRequiredException;
import com.microsoft.identity.common.java.providers.microsoft.MicrosoftTokenErrorResponse;
import com.microsoft.identity.common.java.providers.oauth2.TokenErrorResponse;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.TimeoutException;

import lombok.SneakyThrows;

@RunWith(JUnit4.class)
public class ExceptionAdapterTests {

@Test
public void testBaseExceptionFromException_TerminalException() throws Exception{
TerminalException t = new TerminalException("errorMsg", ClientException.KEY_RING_WRITE_FAILURE);
BaseException e = ExceptionAdapter.baseExceptionFromException(t);
Assert.assertEquals(e.getErrorCode(), t.getErrorCode());
Assert.assertEquals(e.getCause(), t);
final TerminalException t = new TerminalException("errorMsg", ClientException.KEY_RING_WRITE_FAILURE);
final BaseException e = ExceptionAdapter.baseExceptionFromException(t);
assertEquals(t.getErrorCode(), e.getErrorCode());
assertEquals(t, e.getCause());
}

@Test
Expand All @@ -59,9 +76,38 @@ public void testMFATokenErrorResponse_IsTranslatedToUIRequiredException() {
tokenErrorResponse.setSubError("basic_action");

BaseException e = ExceptionAdapter.getExceptionFromTokenErrorResponse(tokenErrorResponse);
Assert.assertTrue("Expected exception of UiRequiredException type", e instanceof UiRequiredException);
Assert.assertEquals(e.getErrorCode(), tokenErrorResponse.getError());
Assert.assertEquals(e.getMessage(), tokenErrorResponse.getErrorDescription());
assertTrue("Expected exception of UiRequiredException type", e instanceof UiRequiredException);
assertEquals(tokenErrorResponse.getError(), e.getErrorCode());
assertEquals(tokenErrorResponse.getErrorDescription(), e.getMessage());
}

@Test
public void testMFATokenErrorResponse_allowUiRequiredException_True() {
final MicrosoftTokenErrorResponse tokenErrorResponse = new MicrosoftTokenErrorResponse();
tokenErrorResponse.setError("invalid_grant");
tokenErrorResponse.setErrorDescription("AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access '7ae46e1'. Trace ID: 01276277-3a30020d900900 Correlation ID: 6209e18a-f89b-4f14-a05e-0371c6757adb Timestamp: 2024-11-14 13:09:18Z");
tokenErrorResponse.setErrorCodes(new ArrayList<Long>(Arrays.asList(50076L)));
tokenErrorResponse.setSubError("basic_action");

BaseException e = ExceptionAdapter.getExceptionFromTokenErrorResponse(tokenErrorResponse, true);
assertTrue("Expected exception of UiRequiredException type", e instanceof UiRequiredException);
assertEquals(tokenErrorResponse.getError(), e.getErrorCode());
assertEquals(tokenErrorResponse.getErrorDescription(), e.getMessage());
}

@Test
public void testMFATokenErrorResponse_allowUiRequiredException_False() {
final MicrosoftTokenErrorResponse tokenErrorResponse = new MicrosoftTokenErrorResponse();
tokenErrorResponse.setError("invalid_grant");
tokenErrorResponse.setErrorDescription("AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access '7ae46e1'. Trace ID: 01276277-3a30020d900900 Correlation ID: 6209e18a-f89b-4f14-a05e-0371c6757adb Timestamp: 2024-11-14 13:09:18Z");
tokenErrorResponse.setErrorCodes(new ArrayList<Long>(Arrays.asList(50076L)));
tokenErrorResponse.setSubError("basic_action");

BaseException e = ExceptionAdapter.getExceptionFromTokenErrorResponse(tokenErrorResponse, false);
assertFalse("Expected exception of UiRequiredException type", e instanceof UiRequiredException);
assertTrue("Expected exception of UiRequiredException type", e instanceof ServiceException);
assertEquals(tokenErrorResponse.getError(), e.getErrorCode());
assertEquals(tokenErrorResponse.getErrorDescription(), e.getMessage());
}

@Test
Expand All @@ -70,7 +116,7 @@ public void testNativeAuthMFAException_ContainsCorrectDescription() {
ServiceException outErr = new ServiceException("errorCode", description, null);
outErr.setCliTelemErrorCode("50076");
ServiceException result = ExceptionAdapter.convertToNativeAuthException(outErr);
Assert.assertEquals("Multi-factor authentication is required, which can't be fulfilled as part of this flow. Please sign out and perform a new sign in operation. Please see exception details for more information." + description, result.getMessage());
assertEquals("Multi-factor authentication is required, which can't be fulfilled as part of this flow. Please sign out and perform a new sign in operation. Please see exception details for more information." + description, result.getMessage());
}

@Test
Expand All @@ -79,12 +125,91 @@ public void testNativeAuthResetPasswordRequiredException_ContainsCorrectDescript
ServiceException outErr = new ServiceException("errorCode", description, null);
outErr.setCliTelemErrorCode("50142");
ServiceException result = ExceptionAdapter.convertToNativeAuthException(outErr);
Assert.assertEquals("User password change is required, which can't be fulfilled as part of this flow.Please reset the password and perform a new sign in operation. Please see exception details for more information." + description, result.getMessage());
assertEquals("User password change is required, which can't be fulfilled as part of this flow.Please reset the password and perform a new sign in operation. Please see exception details for more information." + description, result.getMessage());
}

@Test
public void testClientExceptionFromException_TimeoutException() {
final TimeoutException t = new TimeoutException();
Assert.assertEquals(ClientException.TIMED_OUT, ExceptionAdapter.clientExceptionFromException(t).getErrorCode());
assertEquals(ClientException.TIMED_OUT, ExceptionAdapter.clientExceptionFromException(t).getErrorCode());
}

@SneakyThrows
@Test
public void testGetExceptionFromTokenErrorResponse_WithBrokerSilentTokenCommandParameters_PolicyProtectionRequired() {
final BrokerSilentTokenCommandParameters commandParameters = mock(BrokerSilentTokenCommandParameters.class);
final Authority authority = mock(Authority.class);
when(authority.getAuthorityURL()).thenReturn(new URL("https://login.microsoftonline.com/organizations"));
when(commandParameters.getAuthority()).thenReturn(authority);
when(commandParameters.isRequestForResourceAccount()).thenReturn(false);
final TokenErrorResponse errorResponse = new TokenErrorResponse();
errorResponse.setError(OAuth2ErrorCode.UNAUTHORIZED_CLIENT);
errorResponse.setSubError(OAuth2SubErrorCode.PROTECTION_POLICY_REQUIRED);
errorResponse.setErrorDescription("Intune policy required.");

ServiceException exception = ExceptionAdapter.getExceptionFromTokenErrorResponse(commandParameters, errorResponse);

assertTrue(exception instanceof IntuneAppProtectionPolicyRequiredException);
assertEquals(OAuth2SubErrorCode.PROTECTION_POLICY_REQUIRED, exception.getSubErrorCode());
assertEquals("Intune policy required.", exception.getMessage());
}

@Test
public void testGetExceptionFromTokenErrorResponse_NullCommandParameters_PolicyProtectionRequired() {
final TokenErrorResponse errorResponse = new TokenErrorResponse();
errorResponse.setError(OAuth2ErrorCode.UNAUTHORIZED_CLIENT);
errorResponse.setSubError(OAuth2SubErrorCode.PROTECTION_POLICY_REQUIRED);
errorResponse.setErrorDescription("Intune policy required.");

ServiceException exception = ExceptionAdapter.getExceptionFromTokenErrorResponse(null, errorResponse);

assertFalse(exception instanceof UiRequiredException);
assertEquals(OAuth2ErrorCode.UNAUTHORIZED_CLIENT, exception.getErrorCode());
assertEquals("Intune policy required.", exception.getMessage());
}

@Test
public void testGetExceptionFromTokenErrorResponse_WithBrokerSilentTokenCommandParameters_ResourceAccount() {
final BrokerSilentTokenCommandParameters commandParameters = mock(BrokerSilentTokenCommandParameters.class);
when(commandParameters.isRequestForResourceAccount()).thenReturn(true);

final TokenErrorResponse errorResponse = new TokenErrorResponse();
errorResponse.setError(OAuth2ErrorCode.INVALID_GRANT);
errorResponse.setErrorDescription("UI required.");

ServiceException exception = ExceptionAdapter.getExceptionFromTokenErrorResponse(commandParameters, errorResponse);

assertFalse(exception instanceof UiRequiredException);
assertEquals(OAuth2ErrorCode.INVALID_GRANT, exception.getErrorCode());
assertEquals("UI required.", exception.getMessage());
}

@Test
public void testGetExceptionFromTokenErrorResponse_WithBrokerSilentTokenCommandParameters() {
final BrokerSilentTokenCommandParameters commandParameters = mock(BrokerSilentTokenCommandParameters.class);
when(commandParameters.isRequestForResourceAccount()).thenReturn(false);

final TokenErrorResponse errorResponse = new TokenErrorResponse();
errorResponse.setError(OAuth2ErrorCode.INVALID_GRANT);
errorResponse.setErrorDescription("UI required.");

ServiceException exception = ExceptionAdapter.getExceptionFromTokenErrorResponse(commandParameters, errorResponse);

assertTrue(exception instanceof UiRequiredException);
assertEquals(OAuth2ErrorCode.INVALID_GRANT, exception.getErrorCode());
assertEquals("UI required.", exception.getMessage());
}

@Test
public void testGetExceptionFromTokenErrorResponse_NullCommandParameters() {
final TokenErrorResponse errorResponse = new TokenErrorResponse();
errorResponse.setError(OAuth2ErrorCode.INVALID_GRANT);
errorResponse.setErrorDescription("UI required.");

ServiceException exception = ExceptionAdapter.getExceptionFromTokenErrorResponse(null, errorResponse);

assertTrue(exception instanceof UiRequiredException);
assertEquals(OAuth2ErrorCode.INVALID_GRANT, exception.getErrorCode());
assertEquals("UI required.", exception.getMessage());
}
}