From 0768b6e76bd4884b558e504c668b237ac46e93e0 Mon Sep 17 00:00:00 2001 From: Saurabh Gautam Date: Tue, 10 Oct 2023 01:01:52 +0100 Subject: [PATCH 01/12] Code for NativeAuth Ciam --- common | 2 +- .../INativeAuthPublicClientApplication.kt | 17 +- .../NativeAuthPublicClientApplication.kt | 451 +++++++++++++++- ...uthPublicClientApplicationConfiguration.kt | 6 +- .../exception/MsalUserNotFoundException.java | 32 ++ .../internal/CommandParametersAdapter.java | 274 ++++++++++ .../identity/client/statemachine/Error.kt | 32 +- .../statemachine/results/BaseResults.kt | 61 +++ .../statemachine/results/SignInResult.kt | 178 +++++++ .../client/statemachine/states/BaseStates.kt | 286 +++++++++++ .../client/statemachine/states/Callback.kt | 31 ++ .../statemachine/states/SignInStates.kt | 485 ++++++++++++++++++ .../nativeauth/NativeAuthCIAMAuthorityTest.kt | 154 ++++++ .../NativeAuthOAuth2ConfigurationTest.kt | 47 ++ ...ublicClientApplicationConfigurationTest.kt | 272 ++++++++++ 15 files changed, 2305 insertions(+), 23 deletions(-) create mode 100644 msal/src/main/java/com/microsoft/identity/client/exception/MsalUserNotFoundException.java create mode 100644 msal/src/main/java/com/microsoft/identity/client/statemachine/results/BaseResults.kt create mode 100644 msal/src/main/java/com/microsoft/identity/client/statemachine/results/SignInResult.kt create mode 100644 msal/src/main/java/com/microsoft/identity/client/statemachine/states/BaseStates.kt create mode 100644 msal/src/main/java/com/microsoft/identity/client/statemachine/states/Callback.kt create mode 100644 msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt create mode 100644 msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthCIAMAuthorityTest.kt create mode 100644 msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthOAuth2ConfigurationTest.kt create mode 100644 msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthPublicClientApplicationConfigurationTest.kt diff --git a/common b/common index 58cedea91..4e10557f2 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 58cedea91a1f9a6dc1d95d0b6c2807596822f06d +Subproject commit 4e10557f205354cb1a29971b136eba4a49341c64 diff --git a/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt index 743220fcc..a2697b899 100644 --- a/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt @@ -23,9 +23,12 @@ package com.microsoft.identity.client import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.statemachine.results.SignInResult +import com.microsoft.identity.client.statemachine.results.SignInUsingPasswordResult +import com.microsoft.identity.client.statemachine.states.AccountResult /** - * INativeAuthPublicClientApplication provides top level interface that is used by dpp developers + * INativeAuthPublicClientApplication provides top level interface that is used by app developers * to use Native Auth methods. */ interface INativeAuthPublicClientApplication : IPublicClientApplication { @@ -43,4 +46,16 @@ interface INativeAuthPublicClientApplication : IPublicClientApplication { */ fun onError(exception: MsalException) } + + suspend fun getCurrentAccount(): AccountResult? + + fun getCurrentAccount(callback: NativeAuthPublicClientApplication.GetCurrentAccountCallback) + + suspend fun signIn(username: String, scopes: List? = null): SignInResult + + fun signIn(username: String, scopes: List? = null, callback: NativeAuthPublicClientApplication.SignInCallback) + + suspend fun signInUsingPassword(username: String, password: String, scopes: List? = null): SignInUsingPasswordResult + + fun signInUsingPassword(username: String, password: String, scopes: List? = null, callback: NativeAuthPublicClientApplication.SignInUsingPasswordCallback) } diff --git a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt index de028ec2e..0e333ab40 100644 --- a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt @@ -35,8 +35,16 @@ import com.microsoft.identity.client.statemachine.InvalidPasswordError import com.microsoft.identity.client.statemachine.PasswordIncorrectError import com.microsoft.identity.client.statemachine.UserAlreadyExistsError import com.microsoft.identity.client.statemachine.UserNotFoundError +import com.microsoft.identity.client.statemachine.results.SignInResult +import com.microsoft.identity.client.statemachine.results.SignInUsingPasswordResult +import com.microsoft.identity.client.statemachine.states.AccountResult +import com.microsoft.identity.client.statemachine.states.Callback +import com.microsoft.identity.client.statemachine.states.SignInCodeRequiredState +import com.microsoft.identity.client.statemachine.states.SignInPasswordRequiredState import com.microsoft.identity.common.crypto.AndroidAuthSdkStorageEncryptionManager import com.microsoft.identity.common.internal.cache.SharedPreferencesFileManager +import com.microsoft.identity.common.internal.commands.GetCurrentAccountCommand +import com.microsoft.identity.common.internal.commands.SignInStartCommand import com.microsoft.identity.common.internal.controllers.LocalMSALController import com.microsoft.identity.common.internal.controllers.NativeAuthMsalController import com.microsoft.identity.common.internal.net.cache.HttpCache @@ -45,10 +53,12 @@ import com.microsoft.identity.common.java.cache.ICacheRecord import com.microsoft.identity.common.java.commands.CommandCallback import com.microsoft.identity.common.java.controllers.CommandDispatcher import com.microsoft.identity.common.java.controllers.results.ICommandResult +import com.microsoft.identity.common.java.controllers.results.SignInCommandResult +import com.microsoft.identity.common.java.controllers.results.SignInStartCommandResult import com.microsoft.identity.common.java.eststelemetry.PublicApiId import com.microsoft.identity.common.java.exception.BaseException import com.microsoft.identity.common.java.logging.LogSession -import com.microsoft.identity.common.java.logging.Logger.LogLevel +import com.microsoft.identity.common.java.logging.Logger import com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory.AzureActiveDirectory import com.microsoft.identity.common.java.util.ResultFuture import com.microsoft.identity.common.java.util.checkAndWrapCommandResultType @@ -86,6 +96,73 @@ class NativeAuthPublicClientApplication( // To avoid duplicating the code, callback methods are routed through their // coroutine-equivalent through this CoroutineScope. val pcaScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + fun getCurrentAccountInternal(config: NativeAuthPublicClientApplicationConfiguration): IAccount? { + LogSession.logMethodCall(TAG, "${TAG}.getCurrentAccountInternal") + + val params = CommandParametersAdapter.createCommandParameters( + config, + config.oAuth2TokenCache + ) + + val command = GetCurrentAccountCommand( + params, + LocalMSALController(), + object : CommandCallback?, BaseException?> { + override fun onTaskCompleted(result: List?) { + // Do nothing, handled by CommandDispatcher.submitSilentReturningFuture() + } + + override fun onError(error: BaseException?) { + // Do nothing, handled by CommandDispatcher.submitSilentReturningFuture() + } + + override fun onCancel() { + // Not required + } + }, + PublicApiId.NATIVE_AUTH_GET_ACCOUNT + ) + val result = CommandDispatcher.submitSilentReturningFuture(command) + .get().result as List? + + // To simplify the logic, if more than one account is returned, the first account will be picked. + // We do not support switching from MULTIPLE to SINGLE. + return getAccountFromICacheRecordsList(result) + } + + /** + * Get an IAccount from a list of ICacheRecord. + * + * @param cacheRecords list of cache record that belongs to an account. + * If the list can be converted to multiple accounts, only the first one will be returned. + */ + private fun getAccountFromICacheRecordsList(cacheRecords: List?): IAccount? { + LogSession.logMethodCall(TAG, "${TAG}.getAccountFromICacheRecordsList") + if (cacheRecords.isNullOrEmpty()) { + return null + } + val account = AccountAdapter.adapt(cacheRecords) + if (account.isNullOrEmpty()) { + Logger.error( + TAG, + "Returned cacheRecords were adapted into empty or null IAccount list. " + + "This is unexpected in native auth mode." + + "Returning null.", + null + ) + return null + } + if (account.size != 1) { + Logger.warn( + TAG, + "Returned cacheRecords were adapted into multiple IAccount. " + + "This is unexpected in native auth mode." + + "Returning the first adapted account." + ) + } + return account[0] + } } @Throws(MsalClientException::class) @@ -111,5 +188,375 @@ class NativeAuthPublicClientApplication( NATIVE_AUTH_CREDENTIAL_SHARED_PREFERENCES, AndroidAuthSdkStorageEncryptionManager(context) ) - } + } + + interface GetCurrentAccountCallback : Callback + + /** + * Retrieve the current signed in account from cache; callback variant. + * + * @param callback [com.microsoft.identity.client.NativeAuthPublicClientApplication.GetCurrentAccountCallback] to receive the result. + * @return [com.microsoft.identity.client.statemachine.states.AccountResult] if there is a signed in account, null otherwise. + */ + override fun getCurrentAccount(callback: GetCurrentAccountCallback) { + pcaScope.launch { + try { + val result = getCurrentAccount() + callback.onResult(result) + } catch (e: MsalException) { + callback.onError(e) + } + } + } + + /** + * Retrieve the current signed in account from cache; Kotlin coroutines variant. + * + * @return [com.microsoft.identity.client.statemachine.states.AccountResult] if there is a signed in account, null otherwise. + */ + override suspend fun getCurrentAccount(): AccountResult? { + return withContext(Dispatchers.IO) { + val account = getCurrentAccountInternal(nativeAuthConfig) + return@withContext if (account != null) { + AccountResult.createFromAccountResult( + account = account, + config = nativeAuthConfig + ) + } else { + null + } + } + } + + interface SignInCallback : Callback + + /** + * Sign in a user with a given username; callback variant. + * + * @param username username of the account to sign in. + * @param scopes (Optional) scopes to request during the sign in. + * @param callback [com.microsoft.identity.client.NativeAuthPublicClientApplication.SignInCallback] to receive the result. + * @return [com.microsoft.identity.client.statemachine.results.SignInResult] see detailed possible return state under the object. + * @throws [MsalException] if an account is already signed in. + */ + override fun signIn(username: String, scopes: List?, callback: SignInCallback) { + LogSession.logMethodCall(TAG, "${TAG}.signIn(username: String, scopes: List?, callback: SignInCallback)") + pcaScope.launch { + try { + val result = signIn(username, scopes) + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in signIn", e) + callback.onError(e) + } + } + } + + /** + * Sign in a user with a given username; Kotlin coroutines variant. + * + * @param username username of the account to sign in. + * @param scopes (Optional) scopes to request during the sign in. + * @return [com.microsoft.identity.client.statemachine.results.SignInResult] see detailed possible return state under the object. + * @throws [MsalException] if an account is already signed in. + */ + override suspend fun signIn( + username: String, + scopes: List? + ): SignInResult { + return withContext(Dispatchers.IO) { + LogSession.logMethodCall(TAG, "${TAG}.signIn") + + val doesAccountExist = checkForPersistedAccount().get() + if (doesAccountExist) { + Logger.error( + TAG, + "An account is already signed in.", + null + ) + throw MsalClientException( + MsalClientException.INVALID_PARAMETER, + "An account is already signed in." + ) + } + + val params = CommandParametersAdapter.createSignInStartCommandParameters( + nativeAuthConfig, + nativeAuthConfig.oAuth2TokenCache, + username + ) + + val command = SignInStartCommand( + params, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_SIGN_IN_WITH_EMAIL + ) + + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is SignInCommandResult.CodeRequired -> { + SignInResult.CodeRequired( + nextState = SignInCodeRequiredState( + flowToken = result.credentialToken, + scopes = scopes, + config = nativeAuthConfig + ), + codeLength = result.codeLength, + sentTo = result.challengeTargetLabel, + channel = result.challengeChannel + ) + } + is ICommandResult.UnknownError -> { + SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + errorCodes = result.errorCodes, + exception = result.exception + ) + ) + } + is SignInCommandResult.UserNotFound -> { + SignInResult.UserNotFound( + error = UserNotFoundError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + errorCodes = result.errorCodes + ) + ) + } + is SignInCommandResult.PasswordRequired -> { + SignInResult.PasswordRequired( + nextState = SignInPasswordRequiredState( + flowToken = result.credentialToken, + scopes = scopes, + config = nativeAuthConfig + ) + ) + } + is SignInCommandResult.InvalidCredentials -> { + Logger.warn( + TAG, + "Unexpected result $result" + ) + SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = "Unexpected state", + error = "unexpected_state", + correlationId = result.correlationId, + errorCodes = result.errorCodes + ) + ) + } + is SignInCommandResult.Complete -> { + Logger.warn( + TAG, + "Unexpected result $result" + ) + SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = "Unexpected state", + error = "unexpected_state", + correlationId = "UNSET" + ) + ) + } + is ICommandResult.Redirect -> { + SignInResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + } + } + } + + interface SignInUsingPasswordCallback : Callback + + /** + * Sign in the account using username and password; callback variant. + * + * @param username username of the account to sign in. + * @param password password of the account to sign in. + * @param scopes (Optional) list of scopes to request. + * @param callback [com.microsoft.identity.client.NativeAuthPublicClientApplication.SignInUsingPasswordCallback] to receive the result. + * @return [com.microsoft.identity.client.statemachine.results.SignInUsingPasswordResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + override fun signInUsingPassword( + username: String, + password: String, + scopes: List?, + callback: SignInUsingPasswordCallback + ) { + LogSession.logMethodCall(TAG, "${TAG}.signIn(username: String, password: String, scopes: List?, callback: SignInUsingPasswordCallback)") + pcaScope.launch { + try { + val result = signInUsingPassword(username, password, scopes) + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in signInUsingPassword", e) + callback.onError(e) + } + } + } + + /** + * Sign in the account using username and password; Kotlin coroutines variant. + * + * @param username username of the account to sign in. + * @param password password of the account to sign in. + * @param scopes (Optional) list of scopes to request. + * @return [com.microsoft.identity.client.statemachine.results.SignInUsingPasswordResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + override suspend fun signInUsingPassword( + username: String, + password: String, + scopes: List? + ): SignInUsingPasswordResult { + LogSession.logMethodCall(TAG, "${TAG}.signInUsingPassword") + return withContext(Dispatchers.IO) { + LogSession.logMethodCall(TAG, "${TAG}.signInUsingPassword.withContext") + + val doesAccountExist = checkForPersistedAccount().get() + if (doesAccountExist) { + Logger.error( + TAG, + "An account is already signed in.", + null + ) + throw MsalClientException( + MsalClientException.INVALID_PARAMETER, + "An account is already signed in." + ) + } + + val params = + CommandParametersAdapter.createSignInStartUsingPasswordCommandParameters( + nativeAuthConfig, + nativeAuthConfig.oAuth2TokenCache, + username, + password, + scopes + ) + + val command = SignInStartCommand( + params, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_SIGN_IN_WITH_EMAIL_PASSWORD + ) + + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is SignInCommandResult.Complete -> { + val authenticationResult = + AuthenticationResultAdapter.adapt(result.authenticationResult) + + SignInResult.Complete( + resultValue = AccountResult.createFromAuthenticationResult( + authenticationResult, + nativeAuthConfig + ) + ) + } + is SignInCommandResult.CodeRequired -> { + Logger.warn( + TAG, + "Sign in with password flow was started, but server requires" + + "a code. Password was not sent to the API; switching to code " + + "authentication." + ) + SignInResult.CodeRequired( + nextState = SignInCodeRequiredState( + flowToken = result.credentialToken, + scopes = scopes, + config = nativeAuthConfig + ), + codeLength = result.codeLength, + sentTo = result.challengeTargetLabel, + channel = result.challengeChannel + ) + } + is SignInCommandResult.UserNotFound -> { + SignInResult.UserNotFound( + error = UserNotFoundError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + errorCodes = result.errorCodes + ) + ) + } + + is SignInCommandResult.InvalidCredentials -> { + SignInResult.InvalidCredentials( + error = PasswordIncorrectError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + errorCodes = result.errorCodes + ) + ) + } + + is ICommandResult.Redirect -> { + SignInResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + + is ICommandResult.UnknownError -> { + SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + errorCodes = result.errorCodes, + exception = result.exception + ) + ) + } + is SignInCommandResult.PasswordRequired -> { + Logger.warn( + TAG, + "Unexpected result $result" + ) + SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = "Unexpected state", + error = "unexpected_state", + correlationId = "UNSET" + ) + ) + } + } + } + } + + private fun checkForPersistedAccount(): ResultFuture { + LogSession.logMethodCall(TAG, "${TAG}.checkForPersistedAccount") + val future = ResultFuture() + getCurrentAccount(object : GetCurrentAccountCallback { + override fun onResult(result: AccountResult?) { + future.setResult(result != null) + } + + override fun onError(exception: BaseException) { + Logger.error(TAG, "Exception thrown in checkForPersistedAccount", exception) + future.setException(exception) + } + }) + + return future + } } diff --git a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplicationConfiguration.kt b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplicationConfiguration.kt index 5a2d8337b..0020861a6 100644 --- a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplicationConfiguration.kt +++ b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplicationConfiguration.kt @@ -46,8 +46,8 @@ class NativeAuthPublicClientApplicationConfiguration : Serializable { companion object { private val TAG = NativeAuthPublicClientApplicationConfiguration::class.java.simpleName - private val VALID_CHALLENGE_TYPES = listOf(NativeAuthConstants.GrantType.PASSWORD, - NativeAuthConstants.GrantType.OOB, NativeAuthConstants.GrantType.REDIRECT) + private val VALID_CHALLENGE_TYPES = listOf(NativeAuthConstants.ChallengeType.PASSWORD, + NativeAuthConstants.ChallengeType.OOB, NativeAuthConstants.ChallengeType.REDIRECT) } private object NativeAuthSerializedNames { @@ -57,7 +57,7 @@ class NativeAuthPublicClientApplicationConfiguration : } //List of challenge types supported by the client. - //For a complete list of challenge types see [NativeAuthConstants.GrantType] + //For a complete list of challenge types see [NativeAuthConstants.ChallengeType] @SerializedName(NativeAuthSerializedNames.CHALLENGE_TYPES) private var challengeTypes: List? = null diff --git a/msal/src/main/java/com/microsoft/identity/client/exception/MsalUserNotFoundException.java b/msal/src/main/java/com/microsoft/identity/client/exception/MsalUserNotFoundException.java new file mode 100644 index 000000000..e162f72c0 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/exception/MsalUserNotFoundException.java @@ -0,0 +1,32 @@ +// 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.client.exception; + +public class MsalUserNotFoundException extends MsalException { + private final static String USER_NOT_FOUND_ERROR_CODE = "user_not_found"; + + public MsalUserNotFoundException() { + super(USER_NOT_FOUND_ERROR_CODE, "No user found in cache"); + } +} diff --git a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java index fc0a6913c..175d4e1b7 100644 --- a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java +++ b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java @@ -1,3 +1,25 @@ +// 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.client.internal; import android.content.Context; @@ -13,6 +35,7 @@ import com.microsoft.identity.client.IAccount; import com.microsoft.identity.client.ITenantProfile; import com.microsoft.identity.client.MultiTenantAccount; +import com.microsoft.identity.client.NativeAuthPublicClientApplicationConfiguration; import com.microsoft.identity.client.PoPAuthenticationScheme; import com.microsoft.identity.client.PublicClientApplication; import com.microsoft.identity.client.PublicClientApplicationConfiguration; @@ -24,6 +47,7 @@ import com.microsoft.identity.common.java.authorities.Authority; import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryAuthority; import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryB2CAuthority; +import com.microsoft.identity.common.java.authorities.NativeAuthCIAMAuthority; import com.microsoft.identity.common.java.authscheme.AbstractAuthenticationScheme; import com.microsoft.identity.common.java.authscheme.AuthenticationSchemeFactory; import com.microsoft.identity.common.java.authscheme.BearerAuthenticationSchemeInternal; @@ -35,6 +59,13 @@ import com.microsoft.identity.common.java.commands.parameters.InteractiveTokenCommandParameters; import com.microsoft.identity.common.java.commands.parameters.RemoveAccountCommandParameters; import com.microsoft.identity.common.java.commands.parameters.SilentTokenCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.AcquireTokenNoFixedScopesCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignInResendCodeCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignInStartCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignInStartUsingPasswordCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignInSubmitCodeCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignInSubmitPasswordCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignInWithSLTCommandParameters; import com.microsoft.identity.common.java.dto.AccountRecord; import com.microsoft.identity.common.java.providers.oauth2.OAuth2TokenCache; import com.microsoft.identity.common.java.providers.oauth2.OpenIdConnectPromptParameter; @@ -51,6 +82,8 @@ import java.util.List; import java.util.Map; +import androidx.annotation.Nullable; + public class CommandParametersAdapter { private static final String TAG = CommandParametersAdapter.class.getSimpleName(); @@ -250,6 +283,37 @@ public static DeviceCodeFlowCommandParameters createDeviceCodeFlowWithClaimsComm return commandParameters; } + public static AcquireTokenNoFixedScopesCommandParameters createAcquireTokenNoFixedScopesCommandParameters( + @NonNull final PublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final AccountRecord accountRecord, + @NonNull final Boolean forceRefresh) throws ClientException { + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + final AbstractAuthenticationScheme authenticationScheme = new BearerAuthenticationSchemeInternal(); + + final AcquireTokenNoFixedScopesCommandParameters commandParameters = AcquireTokenNoFixedScopesCommandParameters + .builder() + .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) + .applicationName(configuration.getAppContext().getPackageName()) + .applicationVersion(getPackageVersion(configuration.getAppContext())) + .clientId(configuration.getClientId()) + .isSharedDevice(configuration.getIsSharedDevice()) + .oAuth2TokenCache(tokenCache) + .redirectUri(configuration.getRedirectUri()) + .requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion()) + .sdkType(SdkType.MSAL) + .sdkVersion(PublicClientApplication.getSdkVersion()) + .authority(authority) + .authenticationScheme(authenticationScheme) + .forceRefresh(forceRefresh) + .account(accountRecord) + .powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled()) + .build(); + + return commandParameters; + } + public static DeviceCodeFlowCommandParameters createDeviceCodeFlowCommandParameters( @NonNull final PublicClientApplicationConfiguration configuration, @NonNull final OAuth2TokenCache tokenCache, @@ -281,6 +345,216 @@ public static DeviceCodeFlowCommandParameters createDeviceCodeFlowCommandParamet return commandParameters; } + public static SignInStartCommandParameters createSignInStartCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String username) throws ClientException { + final AbstractAuthenticationScheme authenticationScheme = AuthenticationSchemeFactory.createScheme( + AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext()), + null + ); + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + final SignInStartCommandParameters commandParameters = SignInStartCommandParameters.builder() + .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) + .applicationName(configuration.getAppContext().getPackageName()) + .applicationVersion(getPackageVersion(configuration.getAppContext())) + .clientId(configuration.getClientId()) + .isSharedDevice(configuration.getIsSharedDevice()) + .redirectUri(configuration.getRedirectUri()) + .oAuth2TokenCache(tokenCache) + .requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion()) + .sdkType(SdkType.MSAL) + .sdkVersion(PublicClientApplication.getSdkVersion()) + .powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled()) + .authority(authority) + .username(username) + .authenticationScheme(authenticationScheme) + .clientId(configuration.getClientId()) + .challengeType(configuration.getChallengeTypes()) + .build(); + + return commandParameters; + } + + public static SignInStartUsingPasswordCommandParameters createSignInStartUsingPasswordCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String username, + @NonNull final String password, + final List scopes) throws ClientException { + final AbstractAuthenticationScheme authenticationScheme = AuthenticationSchemeFactory.createScheme( + AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext()), + null + ); + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + final SignInStartUsingPasswordCommandParameters commandParameters = SignInStartUsingPasswordCommandParameters.builder() + .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) + .applicationName(configuration.getAppContext().getPackageName()) + .applicationVersion(getPackageVersion(configuration.getAppContext())) + .clientId(configuration.getClientId()) + .isSharedDevice(configuration.getIsSharedDevice()) + .redirectUri(configuration.getRedirectUri()) + .oAuth2TokenCache(tokenCache) + .requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion()) + .sdkType(SdkType.MSAL) + .sdkVersion(PublicClientApplication.getSdkVersion()) + .powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled()) + .authority(authority) + .username(username) + .password(password) + .authenticationScheme(authenticationScheme) + .clientId(configuration.getClientId()) + .challengeType(configuration.getChallengeTypes()) + .scopes(scopes) + .build(); + + return commandParameters; + } + + public static SignInWithSLTCommandParameters createSignInWithSLTCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @Nullable final String signInSLT, + @Nullable final String username, + final List scopes) throws ClientException { + final AbstractAuthenticationScheme authenticationScheme = AuthenticationSchemeFactory.createScheme( + AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext()), + null + ); + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + final SignInWithSLTCommandParameters commandParameters = SignInWithSLTCommandParameters.builder() + .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) + .applicationName(configuration.getAppContext().getPackageName()) + .applicationVersion(getPackageVersion(configuration.getAppContext())) + .clientId(configuration.getClientId()) + .isSharedDevice(configuration.getIsSharedDevice()) + .redirectUri(configuration.getRedirectUri()) + .oAuth2TokenCache(tokenCache) + .requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion()) + .sdkType(SdkType.MSAL) + .sdkVersion(PublicClientApplication.getSdkVersion()) + .powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled()) + .authority(authority) + .signInSLT(signInSLT) + .username(username) + .challengeType(configuration.getChallengeTypes()) + .authenticationScheme(authenticationScheme) + .scopes(scopes) + .build(); + + return commandParameters; + } + + public static SignInSubmitCodeCommandParameters createSignInSubmitCodeCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String code, + @NonNull final String credentialToken, + final List scopes) throws ClientException { + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + final AbstractAuthenticationScheme authenticationScheme = AuthenticationSchemeFactory.createScheme( + AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext()), + null + ); + + final SignInSubmitCodeCommandParameters commandParameters = SignInSubmitCodeCommandParameters.builder() + .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) + .applicationName(configuration.getAppContext().getPackageName()) + .applicationVersion(getPackageVersion(configuration.getAppContext())) + .clientId(configuration.getClientId()) + .isSharedDevice(configuration.getIsSharedDevice()) + .redirectUri(configuration.getRedirectUri()) + .oAuth2TokenCache(tokenCache) + .requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion()) + .sdkType(SdkType.MSAL) + .sdkVersion(PublicClientApplication.getSdkVersion()) + .powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled()) + .authority(authority) + .credentialToken(credentialToken) + .authenticationScheme(authenticationScheme) + .challengeType(configuration.getChallengeTypes()) + .code(code) + .scopes(scopes) + .build(); + + return commandParameters; + } + + public static SignInResendCodeCommandParameters createSignInResendCodeCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String credentialToken) { + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + final SignInResendCodeCommandParameters commandParameters = SignInResendCodeCommandParameters.builder() + .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) + .applicationName(configuration.getAppContext().getPackageName()) + .applicationVersion(getPackageVersion(configuration.getAppContext())) + .clientId(configuration.getClientId()) + .isSharedDevice(configuration.getIsSharedDevice()) + .redirectUri(configuration.getRedirectUri()) + .oAuth2TokenCache(tokenCache) + .requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion()) + .sdkType(SdkType.MSAL) + .sdkVersion(PublicClientApplication.getSdkVersion()) + .powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled()) + .authority(authority) + .challengeType(configuration.getChallengeTypes()) + .credentialToken(credentialToken) + .challengeType(configuration.getChallengeTypes()) + .build(); + + return commandParameters; + } + + public static SignInSubmitPasswordCommandParameters createSignInSubmitPasswordCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String credentialToken, + @NonNull final String password, + final List scopes) throws ClientException { + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + final AbstractAuthenticationScheme authenticationScheme = AuthenticationSchemeFactory.createScheme( + AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext()), + null + ); + + final SignInSubmitPasswordCommandParameters commandParameters = + SignInSubmitPasswordCommandParameters.builder() + .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) + .applicationName(configuration.getAppContext().getPackageName()) + .applicationVersion(getPackageVersion(configuration.getAppContext())) + .clientId(configuration.getClientId()) + .isSharedDevice(configuration.getIsSharedDevice()) + .redirectUri(configuration.getRedirectUri()) + .oAuth2TokenCache(tokenCache) + .requiredBrokerProtocolVersion(configuration.getRequiredBrokerProtocolVersion()) + .sdkType(SdkType.MSAL) + .sdkVersion(PublicClientApplication.getSdkVersion()) + .powerOptCheckEnabled(configuration.isPowerOptCheckForEnabled()) + .authority(authority) + .authenticationScheme(authenticationScheme) + .credentialToken(credentialToken) + .password(password) + .scopes(scopes) + .challengeType(configuration.getChallengeTypes()) + .build(); + + return commandParameters; + } + + private static String getPackageVersion(@NonNull final Context context) { final String packageName = context.getPackageName(); try { diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/Error.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/Error.kt index d5773fe75..1fc3d5251 100644 --- a/msal/src/main/java/com/microsoft/identity/client/statemachine/Error.kt +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/Error.kt @@ -34,14 +34,14 @@ sealed class Error( /** * GeneralError is a base class for all errors present in the Native Auth. */ -sealed class GeneralError( - open var error: String? = null, - open val errorMessage: String? = "An unexpected error happened", - open val correlationId: String, - open val errorCodes: List? = null, - open var exception: Exception? = null -) - +class GeneralError( + override var error: String? = null, + override val errorMessage: String? = "An unexpected error happened", + override val correlationId: String, + val details: List>? = null, + override val errorCodes: List? = null, + override var exception: Exception? = null +) : Error(errorMessage = errorMessage, error = error, correlationId = correlationId, exception = exception) /** * BrowserRequiredError occurs when authentication cannot be performed via means of Native Auth and @@ -51,7 +51,7 @@ class BrowserRequiredError( override var error: String? = null, override val errorMessage: String = "The client's authentication capabilities are insufficient. Please redirect to the browser to complete authentication", override val correlationId: String -) : GeneralError(errorMessage = errorMessage, error = error, correlationId = correlationId) +) : Error(errorMessage = errorMessage, error = error, correlationId = correlationId) /** * IncorrectCodeError occurs when the user has provided incorrect code for out of band authentication. @@ -61,7 +61,7 @@ class IncorrectCodeError( override val errorMessage: String, override val correlationId: String, override val errorCodes: List? = null -) : GeneralError(errorMessage = errorMessage, error = error, correlationId = correlationId, errorCodes = errorCodes) +) : Error(errorMessage = errorMessage, error = error, correlationId = correlationId, errorCodes = errorCodes) /** * UserNotFoundError occurs when the user could not be located in the given the username. The authentication @@ -72,7 +72,7 @@ class UserNotFoundError( override val errorMessage: String, override val correlationId: String, override val errorCodes: List? = null -) : GeneralError(errorMessage = errorMessage, error = error, correlationId = correlationId, errorCodes = errorCodes) +) : Error(errorMessage = errorMessage, error = error, correlationId = correlationId, errorCodes = errorCodes) /** * PasswordIncorrectError occurs when the user has provided incorrect password for signin. @@ -82,7 +82,7 @@ class PasswordIncorrectError( override val errorMessage: String, override val correlationId: String, override val errorCodes: List -) : GeneralError(errorMessage = errorMessage, error = error, correlationId = correlationId, errorCodes = errorCodes) +) : Error(errorMessage = errorMessage, error = error, correlationId = correlationId, errorCodes = errorCodes) /** * UserAlreadyExistsError has used a username to create an exists for which there is a pre-existing @@ -92,7 +92,7 @@ class UserAlreadyExistsError( override var error: String? = null, override val errorMessage: String, override val correlationId: String -) : GeneralError(errorMessage = errorMessage, error = error, correlationId = correlationId) +) : Error(errorMessage = errorMessage, error = error, correlationId = correlationId) /** * InvalidPasswordError is seen in Signup process when a user provides a password that does not @@ -102,7 +102,7 @@ class InvalidPasswordError( override var error: String? = null, override val errorMessage: String, override val correlationId: String -) : GeneralError(errorMessage = errorMessage, error = error, correlationId = correlationId) +) : Error(errorMessage = errorMessage, error = error, correlationId = correlationId) /** * InvalidPasswordError is seen in Signup process when a user provides a email address that is not @@ -112,7 +112,7 @@ class InvalidEmailError( override var error: String? = null, override val errorMessage: String, override val correlationId: String -) : GeneralError(errorMessage = errorMessage, error = error, correlationId = correlationId) +) : Error(errorMessage = errorMessage, error = error, correlationId = correlationId) /** * InvalidAttributesError is seen in Signup process when a user provides attributes that are not @@ -122,4 +122,4 @@ class InvalidAttributesError( override var error: String? = null, override val errorMessage: String, override val correlationId: String -) : GeneralError(errorMessage = errorMessage, error = error, correlationId = correlationId) +) : Error(errorMessage = errorMessage, error = error, correlationId = correlationId) diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/results/BaseResults.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/results/BaseResults.kt new file mode 100644 index 000000000..fb37de3b9 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/results/BaseResults.kt @@ -0,0 +1,61 @@ +// ktlint-disable filename + +// 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.client.statemachine.results + +import com.microsoft.identity.client.statemachine.Error +import com.microsoft.identity.client.statemachine.states.State + +interface Result { + open class SuccessResult(open val nextState: State) : Result + open class ErrorResult(open val error: Error) : Result + open class CompleteResult(open val resultValue: Any? = null) : Result + open class CompleteWithNextStateResult(override val resultValue: Any? = null, open val nextState: State?) : CompleteResult(resultValue = resultValue) + + fun isSuccess(): Boolean = this is SuccessResult + fun isError(): Boolean = this is ErrorResult + fun isComplete(): Boolean = this is CompleteResult +} + +/** + * Sign out: removes account from cache. Does not perform single sign-out. + */ +sealed interface SignOutResult : Result { + /** + * CompleteResult Result, which indicates the sign out flow completed successfully. + * i.e. the user account has been removed from persistence. + */ + object Complete : + Result.CompleteResult(resultValue = null), + SignOutResult + + /** + * UnexpectedError ErrorResult, which indicates that an unexpected error occurred during sign out. + * + * @param error [com.microsoft.identity.client.statemachine.Error] + */ + class UnexpectedError(override val error: Error) : + Result.ErrorResult(error = error), + SignOutResult +} diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/results/SignInResult.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/results/SignInResult.kt new file mode 100644 index 000000000..ee5001d49 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/results/SignInResult.kt @@ -0,0 +1,178 @@ +// 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.client.statemachine.results + +import com.microsoft.identity.client.statemachine.BrowserRequiredError +import com.microsoft.identity.client.statemachine.Error +import com.microsoft.identity.client.statemachine.IncorrectCodeError +import com.microsoft.identity.client.statemachine.PasswordIncorrectError +import com.microsoft.identity.client.statemachine.UserNotFoundError +import com.microsoft.identity.client.statemachine.states.AccountResult +import com.microsoft.identity.client.statemachine.states.SignInCodeRequiredState +import com.microsoft.identity.client.statemachine.states.SignInPasswordRequiredState + +/** + * Results for native sign in flows. + * TODO add documentation & links to learn.microsoft.com + */ + +/** + * Sign in result, produced by + * [com.microsoft.identity.client.INativeAuthPublicClientApplication.signIn] + */ +sealed interface SignInResult : Result { + + /** + * Complete Result, which indicates the sign in flow is complete, + * i.e. tokens are retrieved for the provided scopes (if any). + * + * @param resultValue an [com.microsoft.identity.client.statemachine.states.AccountResult] object containing account information and account related methods. + */ + class Complete(override val resultValue: AccountResult) : + Result.CompleteResult(resultValue = resultValue), + SignInResult, + SignInSubmitCodeResult, + SignInUsingPasswordResult, + SignInSubmitPasswordResult + + /** + * BrowserRequired ErrorResult, which indicates that the server requires more/different authentication mechanisms than the client is configured to provide. + * The flow should be restarted with a browser, by calling [com.microsoft.identity.client.IPublicClientApplication.acquireToken] + * + * @param error [com.microsoft.identity.client.statemachine.BrowserRequiredError]. + */ + class BrowserRequired(override val error: BrowserRequiredError) : + Result.ErrorResult(error = error), + SignInResult, + SignInUsingPasswordResult, + SignInSubmitCodeResult, + SignInResendCodeResult, + SignInSubmitPasswordResult + + /** + * UnexpectedError ErrorResult is a general error wrapper which indicates an unexpected error occurred during the flow. + * If this occurs, the flow should be restarted. + * + * @param error [com.microsoft.identity.client.statemachine.Error]. + */ + class UnexpectedError(override val error: Error) : + Result.ErrorResult(error = error), + SignInResult, + SignInUsingPasswordResult, + SignInResendCodeResult, + SignInSubmitCodeResult, + SignInSubmitPasswordResult + + /** + * CodeRequired Result, which indicates a verification code is required from the user to continue. + * + * @param nextState [com.microsoft.identity.client.statemachine.states.SignInCodeRequiredState] the current state of the flow with follow-on methods. + * @param codeLength the length of the code required by the server. + * @param sentTo the email/phone number the code was sent to. + * @param channel the channel(email/phone) the code was sent through. + */ + class CodeRequired( + override val nextState: SignInCodeRequiredState, + val codeLength: Int, + val sentTo: String, + val channel: String, + ) : Result.SuccessResult(nextState = nextState), SignInResult, SignInUsingPasswordResult, SignInSubmitPasswordResult + + /** + * InvalidCredentials ErrorResult, which indicates credentials provided by the users are not acceptable to the server. + * The flow should be restarted or the password should be re-submitted, as appropriate. + * + * @param error [com.microsoft.identity.client.statemachine.PasswordIncorrectError]. + */ + class InvalidCredentials( + override val error: PasswordIncorrectError + ) : Result.ErrorResult(error = error), SignInUsingPasswordResult, SignInSubmitPasswordResult + + /** + * UserNotFound ErrorResult, which indicates there was no account found with the provided email. + * The flow should be restarted. + * + * @param error [com.microsoft.identity.client.statemachine.UserNotFoundError]. + */ + class UserNotFound( + override val error: UserNotFoundError + ) : SignInResult, SignInUsingPasswordResult, Result.ErrorResult(error = error) + + /** + * PasswordRequired Result, which indicates that the valid password is required from the user to continue. + * + * @param nextState [com.microsoft.identity.client.statemachine.states.SignInPasswordRequiredState] the current state of the flow with follow-on methods. + */ + class PasswordRequired( + override val nextState: SignInPasswordRequiredState + ) : SignInResult, Result.SuccessResult(nextState = nextState) +} + +/** + * Sign in with password result, produced by + * [com.microsoft.identity.client.INativeAuthPublicClientApplication.signInUsingPassword] + */ +sealed interface SignInUsingPasswordResult : Result + +/** + * Sign in submit code result, produced by + * [com.microsoft.identity.client.statemachine.states.SignInCodeRequiredState.submitCode] + */ +sealed interface SignInSubmitCodeResult : Result { + /** + * CodeIncorrect ErrorResult, which indicates the verification code provided by user is incorrect. + * The code should be re-submitted. + * + * @param error [com.microsoft.identity.client.statemachine.IncorrectCodeError]. + */ + class CodeIncorrect( + override val error: IncorrectCodeError + ) : SignInSubmitCodeResult, Result.ErrorResult(error = error) +} + +/** + * Sign in submit password result, produced by + * [com.microsoft.identity.client.statemachine.states.SignInPasswordRequiredState.submitPassword] + */ +sealed interface SignInSubmitPasswordResult : Result + +/** + * Sign in resend code result, produced by + * [com.microsoft.identity.client.statemachine.states.SignInCodeRequiredState.resendCode] + */ +sealed interface SignInResendCodeResult : Result { + /** + * Success Result, which indicates a new verification code was successfully resent. + * + * @param codeLength the length of the code required by the server. + * @param sentTo the email/phone number the code was sent to. + * @param channel channel(email/phone) the code was sent through. + */ + class Success( + override val nextState: SignInCodeRequiredState, + val codeLength: Int, + val sentTo: String, + val channel: String, + ) : SignInResendCodeResult, Result.SuccessResult(nextState = nextState) +} diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/BaseStates.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/BaseStates.kt new file mode 100644 index 000000000..bddc92e27 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/BaseStates.kt @@ -0,0 +1,286 @@ +// 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.client.statemachine.states + +import com.microsoft.identity.client.Account +import com.microsoft.identity.client.AcquireTokenSilentParameters +import com.microsoft.identity.client.AuthenticationResultAdapter +import com.microsoft.identity.client.IAccount +import com.microsoft.identity.client.IAuthenticationResult +import com.microsoft.identity.client.NativeAuthPublicClientApplication +import com.microsoft.identity.client.NativeAuthPublicClientApplicationConfiguration +import com.microsoft.identity.client.PublicClientApplication +import com.microsoft.identity.client.exception.MsalClientException +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.internal.CommandParametersAdapter +import com.microsoft.identity.client.statemachine.results.SignOutResult +import com.microsoft.identity.common.internal.commands.AcquireTokenNoFixedScopesCommand +import com.microsoft.identity.common.internal.commands.RemoveCurrentAccountCommand +import com.microsoft.identity.common.internal.controllers.LocalMSALController +import com.microsoft.identity.common.internal.controllers.NativeAuthMsalController +import com.microsoft.identity.common.java.commands.CommandCallback +import com.microsoft.identity.common.java.controllers.CommandDispatcher +import com.microsoft.identity.common.java.controllers.ExceptionAdapter +import com.microsoft.identity.common.java.dto.AccountRecord +import com.microsoft.identity.common.java.eststelemetry.PublicApiId +import com.microsoft.identity.common.java.exception.BaseException +import com.microsoft.identity.common.java.exception.ServiceException +import com.microsoft.identity.common.java.logging.LogSession +import com.microsoft.identity.common.java.logging.Logger +import com.microsoft.identity.common.java.logging.Logger.LogLevel +import com.microsoft.identity.common.java.result.ILocalAuthenticationResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.Serializable + +sealed interface State + +abstract class BaseState(internal open val flowToken: String?) + +/** + * AcccountResult returned as part of a successful completion of sign in flow [com.microsoft.identity.client.statemachine.results.SignInResult.Complete]. + */ +class AccountResult private constructor( + private val account: IAccount, + private val config: NativeAuthPublicClientApplicationConfiguration +) : Serializable { + + companion object { + + private val TAG = NativeAuthPublicClientApplication::class.java.simpleName + + fun createFromAuthenticationResult( + authenticationResult: IAuthenticationResult, + config: NativeAuthPublicClientApplicationConfiguration + ): AccountResult { + return AccountResult( + account = authenticationResult.account, + config = config + ) + } + + fun createFromAccountResult( + account: IAccount, + config: NativeAuthPublicClientApplicationConfiguration + ): AccountResult { + return AccountResult( + account = account, + config = config + ) + } + } + + interface SignOutCallback : Callback + + /** + * Remove the current account from the cache; callback variant. + * + * @param callback [com.microsoft.identity.client.statemachine.states.AccountResult.SignOutCallback] to receive the result on. + */ + fun signOut(callback: SignOutCallback) { + LogSession.logMethodCall(TAG, "${TAG}.signOut") + NativeAuthPublicClientApplication.pcaScope.launch { + try { + val result = signOut() + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in signOut", e) + callback.onError(e) + } + } + } + + /** + * Remove the current account from the cache; Kotlin coroutines variant. + */ + suspend fun signOut(): SignOutResult { + return withContext(Dispatchers.IO) { + LogSession.logMethodCall(TAG, "${TAG}.signOut.withContext") + + val account: IAccount = + NativeAuthPublicClientApplication.getCurrentAccountInternal(config) + ?: throw MsalClientException( + MsalClientException.NO_CURRENT_ACCOUNT, + MsalClientException.NO_CURRENT_ACCOUNT_ERROR_MESSAGE + ) + + val requestAccountRecord = AccountRecord() + requestAccountRecord.environment = (account as Account).environment + requestAccountRecord.homeAccountId = account.homeAccountId + + val params = CommandParametersAdapter.createRemoveAccountCommandParameters( + config, + config.oAuth2TokenCache, + requestAccountRecord + ) + + val removeCurrentAccountCommandParameters = RemoveCurrentAccountCommand( + params, + LocalMSALController(), + object : CommandCallback { + override fun onError(error: BaseException?) { + // Do nothing, handled by CommandDispatcher.submitSilentReturningFuture() + } + + override fun onTaskCompleted(result: Boolean?) { + // Do nothing, handled by CommandDispatcher.submitSilentReturningFuture() + } + + override fun onCancel() { + // Do nothing + } + }, + PublicApiId.NATIVE_AUTH_ACCOUNT_SIGN_OUT + ) + + val result = CommandDispatcher.submitSilentReturningFuture(removeCurrentAccountCommandParameters) + .get().result as Boolean + + return@withContext if (result) { + SignOutResult.Complete + } else { + Logger.error( + TAG, + "Unexpected error during signOut.", + null + ) + throw MsalClientException( + MsalClientException.UNKNOWN_ERROR, + "Unexpected error during signOut." + ) + } + } + } + + /** + * Gets the current account. + * + * @return account [com.microsoft.identity.client.IAccount]. + */ + fun getAccount(): IAccount { + return account + } + + /** + * Gets the current account's ID token (if present). + * + * @return idToken [String]. + */ + fun getIdToken(): String? { + return account.idToken + } + + /** + * Gets the claims associated with the current account. + * + * @return A Map of claims. + */ + fun getClaims(): Map? { + return account.claims + } + + interface GetAccessTokenCallback : Callback + + /** + * Retrieves the access token for the currently signed in account from the cache. + * If the access token is expired, it will be attempted to be refreshed using the refresh token that's stored in the cache; + * callback variant. + * + * @return [com.microsoft.identity.client.IAuthenticationResult] If successful. + * @throws [MsalClientException] If the the account doesn't exist in the cache. + * @throws [ServiceException] If the refresh token doesn't exist in the cache/is expired, or the refreshing fails. + */ + fun getAccessToken(forceRefresh: Boolean = false, callback: GetAccessTokenCallback) { + LogSession.logMethodCall(TAG, "${TAG}.getAccessToken") + NativeAuthPublicClientApplication.pcaScope.launch { + try { + val result = getAccessToken(forceRefresh) + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in getAccessToken", e) + callback.onError(e) + } + } + } + + /** + * Retrieves the access token for the currently signed in account from the cache. + * If the access token is expired, it will be attempted to be refreshed using the refresh token that's stored in the cache; + * Kotlin coroutines variant. + * + * @return [com.microsoft.identity.client.IAuthenticationResult] If successful. + * @throws [MsalClientException] If the the account doesn't exist in the cache. + * @throws [ServiceException] If the refresh token doesn't exist in the cache/is expired, or the refreshing fails. + */ + suspend fun getAccessToken(forceRefresh: Boolean = false): IAuthenticationResult? { + LogSession.logMethodCall(TAG, "${TAG}.getAccessToken(forceRefresh: Boolean)") + return withContext(Dispatchers.IO) { + val account = + NativeAuthPublicClientApplication.getCurrentAccountInternal(config) as? Account + ?: throw MsalClientException( + MsalClientException.NO_CURRENT_ACCOUNT, + MsalClientException.NO_CURRENT_ACCOUNT_ERROR_MESSAGE + ) + + val acquireTokenSilentParameters = AcquireTokenSilentParameters.Builder() + .forAccount(account) + .fromAuthority(account.authority) + .build() + + val accountToBeUsed = PublicClientApplication.selectAccountRecordForTokenRequest( + config, + acquireTokenSilentParameters + ) + + val params = CommandParametersAdapter.createAcquireTokenNoFixedScopesCommandParameters( + config, + config.oAuth2TokenCache, + accountToBeUsed, + forceRefresh + ) + + val command = AcquireTokenNoFixedScopesCommand( + params, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_ACCOUNT_GET_ACCESS_TOKEN + ) + + val commandResult = CommandDispatcher.submitSilentReturningFuture(command) + .get().result + + when (commandResult) { + is ServiceException -> { + throw ExceptionAdapter.convertToNativeAuthException(commandResult) + } + is Exception -> { + throw commandResult + } + else -> { + return@withContext AuthenticationResultAdapter.adapt(commandResult as ILocalAuthenticationResult) + } + } + } + } +} diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/Callback.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/Callback.kt new file mode 100644 index 000000000..ce502db70 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/Callback.kt @@ -0,0 +1,31 @@ +// 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.client.statemachine.states + +import com.microsoft.identity.common.java.exception.BaseException + +interface Callback { + fun onResult(result: T) + fun onError(exception: BaseException) +} diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt new file mode 100644 index 000000000..3be070309 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt @@ -0,0 +1,485 @@ +// 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.client.statemachine.states + +import com.microsoft.identity.client.AuthenticationResultAdapter +import com.microsoft.identity.client.NativeAuthPublicClientApplication +import com.microsoft.identity.client.NativeAuthPublicClientApplicationConfiguration +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.internal.CommandParametersAdapter +import com.microsoft.identity.client.statemachine.BrowserRequiredError +import com.microsoft.identity.client.statemachine.GeneralError +import com.microsoft.identity.client.statemachine.IncorrectCodeError +import com.microsoft.identity.client.statemachine.PasswordIncorrectError +import com.microsoft.identity.client.statemachine.results.SignInResendCodeResult +import com.microsoft.identity.client.statemachine.results.SignInResult +import com.microsoft.identity.client.statemachine.results.SignInSubmitCodeResult +import com.microsoft.identity.client.statemachine.results.SignInSubmitPasswordResult +import com.microsoft.identity.common.internal.commands.SignInResendCodeCommand +import com.microsoft.identity.common.internal.commands.SignInSubmitCodeCommand +import com.microsoft.identity.common.internal.commands.SignInSubmitPasswordCommand +import com.microsoft.identity.common.internal.commands.SignInWithSLTCommand +import com.microsoft.identity.common.internal.controllers.NativeAuthMsalController +import com.microsoft.identity.common.java.controllers.CommandDispatcher +import com.microsoft.identity.common.java.controllers.results.ICommandResult +import com.microsoft.identity.common.java.controllers.results.SignInCommandResult +import com.microsoft.identity.common.java.controllers.results.SignInResendCodeCommandResult +import com.microsoft.identity.common.java.controllers.results.SignInSubmitCodeCommandResult +import com.microsoft.identity.common.java.controllers.results.SignInSubmitPasswordCommandResult +import com.microsoft.identity.common.java.controllers.results.SignInWithSLTCommandResult +import com.microsoft.identity.common.java.eststelemetry.PublicApiId +import com.microsoft.identity.common.java.logging.LogSession +import com.microsoft.identity.common.java.logging.Logger +import com.microsoft.identity.common.java.logging.Logger.LogLevel +import com.microsoft.identity.common.java.util.checkAndWrapCommandResultType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.Serializable + +class SignInCodeRequiredState internal constructor( + override val flowToken: String, + private val scopes: List?, + private val config: NativeAuthPublicClientApplicationConfiguration +) : BaseState(flowToken), State, Serializable { + private val TAG: String = SignInCodeRequiredState::class.java.simpleName + + interface SubmitCodeCallback : Callback + + /** + * Submits the verification code received to the server; callback variant. + * + * @param code The code to submit. + * @param callback [com.microsoft.identity.client.statemachine.states.SignInCodeRequiredState.SubmitCodeCallback] to receive the result on. + * @return The results of the submit code action. + */ + fun submitCode(code: String, callback: SubmitCodeCallback) { + LogSession.logMethodCall(TAG, "${TAG}.submitCode") + NativeAuthPublicClientApplication.pcaScope.launch { + try { + val result = submitCode(code) + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in submitCode", e) + callback.onError(e) + } + } + } + + /** + * Submits the verification code received to the server; Kotlin coroutines variant. + * + * @param code The code to submit. + * @return The results of the submit code action. + */ + suspend fun submitCode(code: String): SignInSubmitCodeResult { + LogSession.logMethodCall(TAG, "${TAG}.submitCode(code: String)") + return withContext(Dispatchers.IO) { + val params = CommandParametersAdapter.createSignInSubmitCodeCommandParameters( + config, + config.oAuth2TokenCache, + code, + flowToken, + scopes + ) + + val signInSubmitCodeCommand = SignInSubmitCodeCommand( + parameters = params, + controller = NativeAuthMsalController(), + publicApiId = PublicApiId.NATIVE_AUTH_SIGN_IN_SUBMIT_CODE + ) + + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(signInSubmitCodeCommand).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is SignInCommandResult.IncorrectCode -> { + SignInSubmitCodeResult.CodeIncorrect( + error = IncorrectCodeError( + error = result.error, + errorMessage = result.errorDescription, + correlationId = result.correlationId, + errorCodes = result.errorCodes + ) + ) + } + + is SignInCommandResult.Complete -> { + val authenticationResult = + AuthenticationResultAdapter.adapt(result.authenticationResult) + + SignInResult.Complete( + resultValue = AccountResult.createFromAuthenticationResult( + authenticationResult = authenticationResult, + config = config + ) + ) + } + + is ICommandResult.Redirect -> { + SignInResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + + is ICommandResult.UnknownError -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + errorCodes = result.errorCodes, + exception = result.exception + ) + ) + } + } + } + } + + interface ResendCodeCallback : Callback + + /** + * Resends a new verification code to the user; callback variant. + * + * @param callback [com.microsoft.identity.client.statemachine.states.SignInCodeRequiredState.ResendCodeCallback] to receive the result on. + * @return The results of the resend code action. + */ + fun resendCode(callback: ResendCodeCallback) { + LogSession.logMethodCall(TAG, "${TAG}.resendCode") + NativeAuthPublicClientApplication.pcaScope.launch { + try { + val result = resendCode() + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in resendCode", e) + callback.onError(e) + } + } + } + + /** + * Resends a new verification code to the user; Kotlin coroutines variant. + * + * @return The results of the resend code action. + */ + suspend fun resendCode(): SignInResendCodeResult { + LogSession.logMethodCall(TAG, "${TAG}.resendCode()") + return withContext(Dispatchers.IO) { + val params = CommandParametersAdapter.createSignInResendCodeCommandParameters( + config, + config.oAuth2TokenCache, + flowToken + ) + + val signInResendCodeCommand = SignInResendCodeCommand( + parameters = params, + controller = NativeAuthMsalController(), + publicApiId = PublicApiId.NATIVE_AUTH_SIGN_IN_RESEND_CODE + ) + + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(signInResendCodeCommand).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is SignInCommandResult.CodeRequired -> { + SignInResendCodeResult.Success( + nextState = SignInCodeRequiredState( + flowToken = result.credentialToken, + scopes = scopes, + config = config + ), + codeLength = result.codeLength, + sentTo = result.challengeTargetLabel, + channel = result.challengeChannel + ) + } + + is ICommandResult.Redirect -> { + SignInResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + + is ICommandResult.UnknownError -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + errorCodes = result.errorCodes, + exception = result.exception + ) + ) + } + } + } + } +} + +class SignInPasswordRequiredState( + override val flowToken: String, + private val scopes: List?, + private val config: NativeAuthPublicClientApplicationConfiguration +) : BaseState(flowToken), State { + private val TAG: String = SignInPasswordRequiredState::class.java.simpleName + + interface SubmitPasswordCallback : Callback + + /** + * Submits the password of the account to the server; callback variant. + * + * @param password the password to submit. + * @param callback [com.microsoft.identity.client.statemachine.states.SignInPasswordRequiredState.SubmitPasswordCallback] to receive the result on. + * @return The results of the submit password action. + */ + fun submitPassword(password: String, callback: SubmitPasswordCallback) { + LogSession.logMethodCall(TAG, "${TAG}.submitPassword") + NativeAuthPublicClientApplication.pcaScope.launch { + try { + val result = submitPassword(password) + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in submitPassword", e) + callback.onError(e) + } + } + } + + /** + * Submits the password of the account to the server; Kotlin coroutines variant. + * + * @param password the password to submit. + * @return The results of the submit password action. + */ + suspend fun submitPassword(password: String): SignInSubmitPasswordResult { + LogSession.logMethodCall(TAG, "${TAG}.submitPassword(password: String)") + return withContext(Dispatchers.IO) { + val params = CommandParametersAdapter.createSignInSubmitPasswordCommandParameters( + config, + config.oAuth2TokenCache, + flowToken, + password, + scopes + ) + + val signInSubmitPasswordCommand = SignInSubmitPasswordCommand( + parameters = params, + controller = NativeAuthMsalController(), + publicApiId = PublicApiId.NATIVE_AUTH_SIGN_IN_SUBMIT_PASSWORD + ) + + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(signInSubmitPasswordCommand).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is SignInCommandResult.InvalidCredentials -> { + SignInResult.InvalidCredentials( + error = PasswordIncorrectError( + error = result.error, + errorMessage = result.errorDescription, + correlationId = result.correlationId, + errorCodes = result.errorCodes + ) + ) + } + is SignInCommandResult.Complete -> { + val authenticationResult = + AuthenticationResultAdapter.adapt(result.authenticationResult) + SignInResult.Complete( + resultValue = AccountResult.createFromAuthenticationResult( + authenticationResult = authenticationResult, + config = config + ) + ) + } + is ICommandResult.Redirect -> { + SignInResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + is ICommandResult.UnknownError -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + errorCodes = result.errorCodes, + exception = result.exception + ) + ) + } + } + } + } +} + +abstract class SignInAfterSignUpBaseState( + internal open val signInVerificationCode: String?, + internal open val username: String, + private val config: NativeAuthPublicClientApplicationConfiguration +) : BaseState(signInVerificationCode), State, Serializable { + private val TAG: String = SignInAfterSignUpBaseState::class.java.simpleName + interface SignInAfterSignUpCallback : Callback + + /** + * Submits the sign-in-after-sign-up verification code to the server; callback variant. + * + * @param scopes (Optional) The scopes to request. + * @param callback [com.microsoft.identity.client.statemachine.states.SignInAfterSignUpBaseState.SignInAfterSignUpCallback] to receive the result on. + * @return The results of the sign-in-after-sign-up action. + */ + fun signInAfterSignUp(scopes: List? = null, callback: SignInAfterSignUpCallback) { + LogSession.logMethodCall(TAG, "${TAG}.signInAfterSignUp") + + NativeAuthPublicClientApplication.pcaScope.launch { + try { + val result = signInAfterSignUp(scopes) + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in signInAfterSignUp", e) + callback.onError(e) + } + } + } + + /** + * Submits the sign-in-after-sign-up verification code to the server; Kotlin coroutines variant. + * + * @param scopes (Optional) The scopes to request. + * @return The results of the sign-in-after-sign-up action. + */ + suspend fun signInAfterSignUp(scopes: List? = null): SignInResult { + return withContext(Dispatchers.IO) { + LogSession.logMethodCall(TAG, "${TAG}.signInAfterSignUp(scopes: List)") + + // Check if verification code was passed. If not, return an UnknownError with instructions to call the other + // sign in flows (code or password). + if (signInVerificationCode.isNullOrEmpty()) { + Logger.warn( + TAG, + "Unexpected result: signInSLT was null" + ) + return@withContext SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = "Sign In is not available through this state, please use the standalone sign in methods (signInWithCode or signInWithPassword).", + error = "invalid_state", + correlationId = "UNSET" + ) + ) + } + + val commandParameters = CommandParametersAdapter.createSignInWithSLTCommandParameters( + config, + config.oAuth2TokenCache, + signInVerificationCode, + username, + scopes + ) + + val command = SignInWithSLTCommand( + commandParameters, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_SIGN_IN_WITH_SLT + ) + + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is SignInCommandResult.CodeRequired -> { + SignInResult.CodeRequired( + nextState = SignInCodeRequiredState( + flowToken = result.credentialToken, + scopes = scopes, + config = config + ), + codeLength = result.codeLength, + sentTo = result.challengeTargetLabel, + channel = result.challengeChannel + ) + } + is SignInCommandResult.PasswordRequired -> { + SignInResult.PasswordRequired( + nextState = SignInPasswordRequiredState( + flowToken = result.credentialToken, + scopes = scopes, + config = config + ) + ) + } + is SignInCommandResult.Complete -> { + val authenticationResult = + AuthenticationResultAdapter.adapt(result.authenticationResult) + SignInResult.Complete( + resultValue = AccountResult.createFromAuthenticationResult( + authenticationResult = authenticationResult, + config = config + ) + ) + } + is ICommandResult.Redirect -> { + SignInResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + is ICommandResult.UnknownError -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + errorCodes = result.errorCodes, + exception = result.exception + ) + ) + } + } + } + } +} diff --git a/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthCIAMAuthorityTest.kt b/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthCIAMAuthorityTest.kt new file mode 100644 index 000000000..187f36d38 --- /dev/null +++ b/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthCIAMAuthorityTest.kt @@ -0,0 +1,154 @@ +package com.microsoft.identity.client.nativeauth + +import com.microsoft.identity.common.java.authorities.NativeAuthCIAMAuthority +import com.microsoft.identity.common.java.exception.ClientException +import com.microsoft.identity.common.java.providers.oauth2.OAuth2StrategyParameters +import junit.framework.Assert.assertEquals +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class NativeAuthCIAMAuthorityTest { + + private val B2C_AUTHORITY_URL = "https://fabrikamb2c.b2clogin.com/tfp/fabrikamb2c.onmicrosoft.com/b2c_1_susi/" + private val ADFS_AUTHORITY_URL = "https://somesite.contoso.com/adfs/" + private val AAD_AUTHORITY_URL = "https://login.microsoftonline.com/common" + private val CIAM_AUTHORITY_URL = "https://msidlabciam1.ciamlogin.com/msidlabciam1.onmicrosoft.com" + private val CLIENT_ID = "1234-5678-9123" + + @Test + fun testB2CAuthorityURLShouldThrowError() { + try { + NativeAuthCIAMAuthority.getAuthorityFromAuthorityUrl( + authorityUrl = B2C_AUTHORITY_URL, + clientId = CLIENT_ID + ) + } catch (e: ClientException) { + assertEquals(ClientException.NATIVE_AUTH_INVALID_CIAM_AUTHORITY, e.errorCode) + } + } + + @Test + fun testADFSAuthorityURLShouldThrowError() { + try { + NativeAuthCIAMAuthority.getAuthorityFromAuthorityUrl( + authorityUrl = ADFS_AUTHORITY_URL, + clientId = CLIENT_ID + ) + } catch (e: ClientException) { + assertEquals(ClientException.NATIVE_AUTH_INVALID_CIAM_AUTHORITY, e.errorCode) + } + } + + @Test + fun testInvalidAuthorityWithoutPathURLShouldThrowError() { + try { + NativeAuthCIAMAuthority.getAuthorityFromAuthorityUrl( + authorityUrl = "https://www.microsoft.com", + clientId = CLIENT_ID + ) + } catch (e: ClientException) { + assertEquals(ClientException.NATIVE_AUTH_INVALID_CIAM_AUTHORITY, e.errorCode) + } + } + + @Test + fun testAADAuthorityShouldThrowError() { + try { + NativeAuthCIAMAuthority.getAuthorityFromAuthorityUrl( + authorityUrl = AAD_AUTHORITY_URL, + clientId = CLIENT_ID + ) + } catch (e: ClientException) { + assertEquals(ClientException.NATIVE_AUTH_INVALID_CIAM_AUTHORITY, e.errorCode) + } + } + + @Test + fun testCIAMAuthorityShouldSucceed() { + val authority = NativeAuthCIAMAuthority.getAuthorityFromAuthorityUrl( + authorityUrl = CIAM_AUTHORITY_URL, + clientId = CLIENT_ID + ) + Assert.assertEquals( + CIAM_AUTHORITY_URL, + authority.authorityUri.toString() + ) + Assert.assertEquals( + CLIENT_ID, + authority.clientId + ) + } + + @Test + fun testCIAMAuthorityCreateOAuth2StrategyWithDuplicateChallengeTypes() { + val authority = NativeAuthCIAMAuthority.getAuthorityFromAuthorityUrl( + authorityUrl = CIAM_AUTHORITY_URL, + clientId = CLIENT_ID + ) + val params = OAuth2StrategyParameters.builder() + .challengeTypes(listOf("oob", "oob", "password")) + .build() + + val strategy = authority.createOAuth2Strategy(params) + assertEquals("oob password redirect", strategy.config.challengeType) + } + + @Test + fun testCIAMAuthorityCreateOAuth2StrategyWithSingleExistingDefaultChallengeType() { + val authority = NativeAuthCIAMAuthority.getAuthorityFromAuthorityUrl( + authorityUrl = CIAM_AUTHORITY_URL, + clientId = CLIENT_ID + ) + val params = OAuth2StrategyParameters.builder() + .challengeTypes(listOf("redirect")) + .build() + + val strategy = authority.createOAuth2Strategy(params) + assertEquals("redirect", strategy.config.challengeType) + } + + @Test + fun testCIAMAuthorityCreateOAuth2StrategyWithExistingDefaultChallengeTypes() { + val authority = NativeAuthCIAMAuthority.getAuthorityFromAuthorityUrl( + authorityUrl = CIAM_AUTHORITY_URL, + clientId = CLIENT_ID + ) + val params = OAuth2StrategyParameters.builder() + .challengeTypes(listOf("redirect", "oob", "password")) + .build() + + val strategy = authority.createOAuth2Strategy(params) + assertEquals("redirect oob password", strategy.config.challengeType) + } + + @Test + fun testCIAMAuthorityCreateOAuth2StrategyWithDuplicateDefaultChallengeTypes() { + val authority = NativeAuthCIAMAuthority.getAuthorityFromAuthorityUrl( + authorityUrl = CIAM_AUTHORITY_URL, + clientId = CLIENT_ID + ) + val params = OAuth2StrategyParameters.builder() + .challengeTypes(listOf("redirect", "oob", "password", "password", "oob", "redirect")) + .build() + + val strategy = authority.createOAuth2Strategy(params) + assertEquals("redirect oob password", strategy.config.challengeType) + } + + @Test + fun testCIAMAuthorityCreateOAuth2StrategyWithNoChallengeTypes() { + val authority = NativeAuthCIAMAuthority.getAuthorityFromAuthorityUrl( + authorityUrl = CIAM_AUTHORITY_URL, + clientId = CLIENT_ID + ) + val params = OAuth2StrategyParameters.builder() + .challengeTypes(emptyList()) + .build() + + val strategy = authority.createOAuth2Strategy(params) + assertEquals("redirect", strategy.config.challengeType) + } +} diff --git a/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthOAuth2ConfigurationTest.kt b/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthOAuth2ConfigurationTest.kt new file mode 100644 index 000000000..9cbcd9b04 --- /dev/null +++ b/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthOAuth2ConfigurationTest.kt @@ -0,0 +1,47 @@ +package com.microsoft.identity.client.nativeauth + +import com.microsoft.identity.common.java.providers.nativeauth.NativeAuthOAuth2Configuration +import junit.framework.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.net.URI +import java.net.URL + +@RunWith(RobolectricTestRunner::class) +class NativeAuthOAuth2ConfigurationTest { + + @Test + fun testNoTrailingSlash() { + val authorityUrlString = "https://login.microsoftonline.com/samtoso.onmicrosoft.com" + val authorityUrl = URI(authorityUrlString).toURL() + + val configuration = NativeAuthOAuth2Configuration( + authorityUrl = authorityUrl, + clientId = "1234", + challengeType = "oob password redirect", + useMockApiForNativeAuth = false + ) + + val signUpEndpoint = configuration.getSignUpStartEndpoint() + + assertEquals(URL("https://login.microsoftonline.com/samtoso.onmicrosoft.com/signup/v1.0/start"), signUpEndpoint) + } + + @Test + fun testTrailingSlash() { + val authorityUrlString = "https://login.microsoftonline.com/samtoso.onmicrosoft.com/" + val authorityUrl = URI(authorityUrlString).toURL() + + val configuration = NativeAuthOAuth2Configuration( + authorityUrl = authorityUrl, + clientId = "1234", + challengeType = "oob password redirect", + useMockApiForNativeAuth = false + ) + + val signUpEndpoint = configuration.getSignUpStartEndpoint() + + assertEquals(URL("https://login.microsoftonline.com/samtoso.onmicrosoft.com/signup/v1.0/start"), signUpEndpoint) + } +} diff --git a/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthPublicClientApplicationConfigurationTest.kt b/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthPublicClientApplicationConfigurationTest.kt new file mode 100644 index 000000000..251848cc5 --- /dev/null +++ b/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthPublicClientApplicationConfigurationTest.kt @@ -0,0 +1,272 @@ +package com.microsoft.identity.client.nativeauth + +import com.microsoft.identity.client.NativeAuthPublicClientApplicationConfiguration +import com.microsoft.identity.client.PublicClientApplicationConfiguration.INVALID_REDIRECT_MSG +import com.microsoft.identity.client.configuration.AccountMode +import com.microsoft.identity.client.exception.MsalClientException +import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryB2CAuthority +import com.microsoft.identity.common.java.authorities.NativeAuthCIAMAuthority +import junit.framework.Assert.assertEquals +import junit.framework.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class NativeAuthPublicClientApplicationConfigurationTest { + + private val clientId = "1234" + private val ciamAuthority = "https://msidlabciam1.ciamlogin.com/msidlabciam1.onmicrosoft.com" + + @Test + fun testMissingClientId() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.setClientId(null) + try { + config.validateConfiguration() + } catch (e: MsalClientException) { + assertEquals(MsalClientException.NATIVE_AUTH_USE_WITHOUT_CLIENT_ID_ERROR_CODE, e.errorCode) + assertEquals(MsalClientException.NATIVE_AUTH_USE_WITHOUT_CLIENT_ID_ERROR_MESSAGE, e.message) + return + } + // An exception should be thrown + fail() + } + + @Test + fun testEmptyClientId() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.setClientId("") + try { + config.validateConfiguration() + } catch (e: MsalClientException) { + assertEquals(MsalClientException.NATIVE_AUTH_USE_WITHOUT_CLIENT_ID_ERROR_CODE, e.errorCode) + assertEquals(MsalClientException.NATIVE_AUTH_USE_WITHOUT_CLIENT_ID_ERROR_MESSAGE, e.message) + return + } + // An exception should be thrown + fail() + } + + @Test + fun testEmptyRedirectUri() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.redirectUri = "" + try { + config.validateConfiguration() + } catch (e: IllegalArgumentException) { + assertEquals(INVALID_REDIRECT_MSG, e.message) + return + } + // An exception should be thrown + fail() + } + + @Test + fun testInvalidAccountMode() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.accountMode = AccountMode.MULTIPLE + val spyConfig = spy(config) + whenever(spyConfig.authorities).thenReturn(listOf(NativeAuthCIAMAuthority(ciamAuthority, clientId), NativeAuthCIAMAuthority(ciamAuthority, clientId))) + try { + spyConfig.validateConfiguration() + } catch (e: MsalClientException) { + assertEquals(MsalClientException.NATIVE_AUTH_INVALID_ACCOUNT_MODE_CONFIG_ERROR_CODE, e.errorCode) + assertEquals(MsalClientException.NATIVE_AUTH_INVALID_ACCOUNT_MODE_CONFIG_ERROR_MESSAGE, e.message) + return + } + // An exception should be thrown + fail() + } + + @Test + fun testNoAuthority() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.accountMode = AccountMode.SINGLE + val spyConfig = spy(config) + whenever(spyConfig.authorities).thenReturn(null) + try { + spyConfig.validateConfiguration() + } catch (e: MsalClientException) { + assertEquals(MsalClientException.NATIVE_AUTH_USE_WITH_NO_AUTHORITY_ERROR_CODE, e.errorCode) + assertEquals(MsalClientException.NATIVE_AUTH_USE_WITH_NO_AUTHORITY_ERROR_MESSAGE, e.message) + return + } + // An exception should be thrown + fail() + } + + @Test + fun testEmptyAuthorityList() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.accountMode = AccountMode.SINGLE + val spyConfig = spy(config) + whenever(spyConfig.authorities).thenReturn(emptyList()) + try { + spyConfig.validateConfiguration() + } catch (e: MsalClientException) { + assertEquals(MsalClientException.NATIVE_AUTH_USE_WITH_NO_AUTHORITY_ERROR_CODE, e.errorCode) + assertEquals(MsalClientException.NATIVE_AUTH_USE_WITH_NO_AUTHORITY_ERROR_MESSAGE, e.message) + return + } + // An exception should be thrown + fail() + } + + @Test + fun testTooManyAuthorities() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.accountMode = AccountMode.SINGLE + val spyConfig = spy(config) + whenever(spyConfig.authorities).thenReturn(listOf(NativeAuthCIAMAuthority(ciamAuthority, clientId), NativeAuthCIAMAuthority(ciamAuthority, clientId))) + try { + spyConfig.validateConfiguration() + } catch (e: MsalClientException) { + assertEquals(MsalClientException.NATIVE_AUTH_USE_WITH_MULTI_AUTHORITY_ERROR_CODE, e.errorCode) + assertEquals(MsalClientException.NATIVE_AUTH_USE_WITH_MULTI_AUTHORITY_ERROR_MESSAGE, e.message) + return + } + // An exception should be thrown + fail() + } + + @Test + fun testInvalidAuthority() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.accountMode = AccountMode.SINGLE + val spyConfig = spy(config) + whenever(spyConfig.authorities).thenReturn(listOf(NativeAuthCIAMAuthority(ciamAuthority, clientId))) + whenever(spyConfig.defaultAuthority).thenReturn(AzureActiveDirectoryB2CAuthority(ciamAuthority)) + try { + spyConfig.validateConfiguration() + } catch (e: MsalClientException) { + assertEquals(MsalClientException.NATIVE_AUTH_INVALID_CIAM_AUTHORITY_ERROR_CODE, e.errorCode) + assertEquals(MsalClientException.NATIVE_AUTH_INVALID_CIAM_AUTHORITY_ERROR_MESSAGE, e.message) + return + } + // An exception should be thrown + fail() + } + + @Test + fun testSharedDeviceMode() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.accountMode = AccountMode.SINGLE + val spyConfig = spy(config) + whenever(spyConfig.authorities).thenReturn(listOf(NativeAuthCIAMAuthority(ciamAuthority, clientId))) + whenever(spyConfig.defaultAuthority).thenReturn(NativeAuthCIAMAuthority(ciamAuthority, clientId)) + whenever(spyConfig.isSharedDevice).thenReturn(true) + + try { + spyConfig.validateConfiguration() + } catch (e: MsalClientException) { + assertEquals(MsalClientException.NATIVE_AUTH_SHARED_DEVICE_MODE_ERROR_CODE, e.errorCode) + assertEquals(MsalClientException.NATIVE_AUTH_SHARED_DEVICE_MODE_ERROR_MESSAGE, e.message) + return + } + // An exception should be thrown + fail() + } + + // Challenge types are optional, so no exception should be thrown + @Test + fun testMissingChallengeTypes() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.accountMode = AccountMode.SINGLE + val spyConfig = spy(config) + whenever(spyConfig.authorities).thenReturn(listOf(NativeAuthCIAMAuthority(ciamAuthority, clientId))) + whenever(spyConfig.defaultAuthority).thenReturn(NativeAuthCIAMAuthority(ciamAuthority, clientId)) + whenever(spyConfig.isSharedDevice).thenReturn(false) + whenever(spyConfig.useBroker).thenReturn(false) + whenever(spyConfig.getChallengeTypes()).thenReturn(emptyList()) + spyConfig.validateConfiguration() + } + + @Test + fun testInvalidChallengeTypes() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.accountMode = AccountMode.SINGLE + val spyConfig = spy(config) + whenever(spyConfig.authorities).thenReturn(listOf(NativeAuthCIAMAuthority(ciamAuthority, clientId))) + whenever(spyConfig.defaultAuthority).thenReturn(NativeAuthCIAMAuthority(ciamAuthority, clientId)) + whenever(spyConfig.isSharedDevice).thenReturn(false) + whenever(spyConfig.useBroker).thenReturn(false) + spyConfig.setChallengeTypes(listOf("lorem")) + + try { + spyConfig.validateConfiguration() + } catch (e: MsalClientException) { + assertEquals(MsalClientException.NATIVE_AUTH_INVALID_CHALLENGE_TYPE_ERROR_CODE, e.errorCode) + return + } + // An exception should be thrown + fail() + } + + @Test + fun testCaseInsensitiveChallengeTypes() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.accountMode = AccountMode.SINGLE + val spyConfig = spy(config) + whenever(spyConfig.authorities).thenReturn(listOf(NativeAuthCIAMAuthority(ciamAuthority, clientId))) + whenever(spyConfig.defaultAuthority).thenReturn(NativeAuthCIAMAuthority(ciamAuthority, clientId)) + whenever(spyConfig.isSharedDevice).thenReturn(false) + whenever(spyConfig.useBroker).thenReturn(false) + spyConfig.setChallengeTypes(listOf("PaSsWoRd", "OoB", "ReDiReCt")) + spyConfig.validateConfiguration() + } + + @Test + fun testMultipleCorrectChallengeTypes() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.accountMode = AccountMode.SINGLE + val spyConfig = spy(config) + whenever(spyConfig.authorities).thenReturn(listOf(NativeAuthCIAMAuthority(ciamAuthority, clientId))) + whenever(spyConfig.defaultAuthority).thenReturn(NativeAuthCIAMAuthority(ciamAuthority, clientId)) + whenever(spyConfig.isSharedDevice).thenReturn(false) + whenever(spyConfig.useBroker).thenReturn(false) + spyConfig.setChallengeTypes(listOf("password", "oob", "redirect")) + spyConfig.validateConfiguration() + } + + @Test + fun testSingleCorrectChallengeTypes() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.accountMode = AccountMode.SINGLE + val spyConfig = spy(config) + whenever(spyConfig.authorities).thenReturn(listOf(NativeAuthCIAMAuthority(ciamAuthority, clientId))) + whenever(spyConfig.defaultAuthority).thenReturn(NativeAuthCIAMAuthority(ciamAuthority, clientId)) + whenever(spyConfig.isSharedDevice).thenReturn(false) + whenever(spyConfig.useBroker).thenReturn(false) + spyConfig.setChallengeTypes(listOf("oob")) + spyConfig.validateConfiguration() + } + + @Test + fun testRepeatedCorrectChallengeTypes() { + val config = NativeAuthPublicClientApplicationConfiguration() + config.clientId = clientId + config.accountMode = AccountMode.SINGLE + val spyConfig = spy(config) + whenever(spyConfig.authorities).thenReturn(listOf(NativeAuthCIAMAuthority(ciamAuthority, clientId))) + whenever(spyConfig.defaultAuthority).thenReturn(NativeAuthCIAMAuthority(ciamAuthority, clientId)) + whenever(spyConfig.isSharedDevice).thenReturn(false) + whenever(spyConfig.useBroker).thenReturn(false) + spyConfig.setChallengeTypes(listOf("oob", "oob", "password", "redirect", "redirect", "redirect")) + spyConfig.validateConfiguration() + } +} From 1f107c95a75a2a168871af16db073df5c20bc43d Mon Sep 17 00:00:00 2001 From: Saurabh Gautam Date: Tue, 17 Oct 2023 17:44:46 +0100 Subject: [PATCH 02/12] Add javadoc --- .../INativeAuthPublicClientApplication.kt | 47 +++++++++++++++++++ .../statemachine/results/SignInResult.kt | 5 -- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt index a2697b899..885c7da7e 100644 --- a/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt @@ -22,6 +22,7 @@ // THE SOFTWARE. package com.microsoft.identity.client +import com.microsoft.identity.client.exception.MsalClientException import com.microsoft.identity.client.exception.MsalException import com.microsoft.identity.client.statemachine.results.SignInResult import com.microsoft.identity.client.statemachine.results.SignInUsingPasswordResult @@ -47,15 +48,61 @@ interface INativeAuthPublicClientApplication : IPublicClientApplication { fun onError(exception: MsalException) } + /** + * Retrieve the current signed in account from cache; Kotlin coroutines variant. + * + * @return [com.microsoft.identity.client.statemachine.states.AccountResult] if there is a signed in account, null otherwise. + */ suspend fun getCurrentAccount(): AccountResult? + /** + * Retrieve the current signed in account from cache; Kotlin coroutines variant. + * + * @return [com.microsoft.identity.client.statemachine.states.AccountResult] if there is a signed in account, null otherwise. + */ fun getCurrentAccount(callback: NativeAuthPublicClientApplication.GetCurrentAccountCallback) + /** + * Sign in a user with a given username; Kotlin coroutines variant. + * + * @param username username of the account to sign in. + * @param scopes (Optional) scopes to request during the sign in. + * @return [com.microsoft.identity.client.statemachine.results.SignInResult] see detailed possible return state under the object. + * @throws [MsalException] if an account is already signed in. + */ suspend fun signIn(username: String, scopes: List? = null): SignInResult + /** + * Sign in a user with a given username; callback variant. + * + * @param username username of the account to sign in. + * @param scopes (Optional) scopes to request during the sign in. + * @param callback [com.microsoft.identity.client.NativeAuthPublicClientApplication.SignInCallback] to receive the result. + * @return [com.microsoft.identity.client.statemachine.results.SignInResult] see detailed possible return state under the object. + * @throws [MsalException] if an account is already signed in. + */ fun signIn(username: String, scopes: List? = null, callback: NativeAuthPublicClientApplication.SignInCallback) + /** + * Sign in the account using username and password; Kotlin coroutines variant. + * + * @param username username of the account to sign in. + * @param password password of the account to sign in. + * @param scopes (Optional) list of scopes to request. + * @return [com.microsoft.identity.client.statemachine.results.SignInUsingPasswordResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ suspend fun signInUsingPassword(username: String, password: String, scopes: List? = null): SignInUsingPasswordResult + /** + * Sign in the account using username and password; callback variant. + * + * @param username username of the account to sign in. + * @param password password of the account to sign in. + * @param scopes (Optional) list of scopes to request. + * @param callback [com.microsoft.identity.client.NativeAuthPublicClientApplication.SignInUsingPasswordCallback] to receive the result. + * @return [com.microsoft.identity.client.statemachine.results.SignInUsingPasswordResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ fun signInUsingPassword(username: String, password: String, scopes: List? = null, callback: NativeAuthPublicClientApplication.SignInUsingPasswordCallback) } diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/results/SignInResult.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/results/SignInResult.kt index ee5001d49..eb8b0bf59 100644 --- a/msal/src/main/java/com/microsoft/identity/client/statemachine/results/SignInResult.kt +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/results/SignInResult.kt @@ -32,11 +32,6 @@ import com.microsoft.identity.client.statemachine.states.AccountResult import com.microsoft.identity.client.statemachine.states.SignInCodeRequiredState import com.microsoft.identity.client.statemachine.states.SignInPasswordRequiredState -/** - * Results for native sign in flows. - * TODO add documentation & links to learn.microsoft.com - */ - /** * Sign in result, produced by * [com.microsoft.identity.client.INativeAuthPublicClientApplication.signIn] From 1ba1321751adbe9fad454835b9ae5b833711d594 Mon Sep 17 00:00:00 2001 From: Saurabh Gautam Date: Wed, 18 Oct 2023 15:57:25 +0100 Subject: [PATCH 03/12] Add license and javadoc --- .../NativeAuthPublicClientApplication.kt | 8 +- .../internal/CommandParametersAdapter.java | 78 +++++++++++++++++-- .../client/statemachine/states/BaseStates.kt | 3 + .../client/statemachine/states/Callback.kt | 14 ++++ .../nativeauth/NativeAuthCIAMAuthorityTest.kt | 22 ++++++ .../NativeAuthOAuth2ConfigurationTest.kt | 22 ++++++ ...ublicClientApplicationConfigurationTest.kt | 22 ++++++ 7 files changed, 160 insertions(+), 9 deletions(-) diff --git a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt index 0e333ab40..cb3d4f1cf 100644 --- a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt @@ -142,8 +142,8 @@ class NativeAuthPublicClientApplication( if (cacheRecords.isNullOrEmpty()) { return null } - val account = AccountAdapter.adapt(cacheRecords) - if (account.isNullOrEmpty()) { + val accountList = AccountAdapter.adapt(cacheRecords) + if (accountList.isNullOrEmpty()) { Logger.error( TAG, "Returned cacheRecords were adapted into empty or null IAccount list. " + @@ -153,7 +153,7 @@ class NativeAuthPublicClientApplication( ) return null } - if (account.size != 1) { + if (accountList.size != 1) { Logger.warn( TAG, "Returned cacheRecords were adapted into multiple IAccount. " + @@ -161,7 +161,7 @@ class NativeAuthPublicClientApplication( "Returning the first adapted account." ) } - return account[0] + return accountList[0] } } diff --git a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java index 175d4e1b7..e328aaae6 100644 --- a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java +++ b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java @@ -43,7 +43,7 @@ import com.microsoft.identity.client.claims.RequestedClaimAdditionalInformation; import com.microsoft.identity.common.components.AndroidPlatformComponentsFactory; import com.microsoft.identity.common.internal.commands.parameters.AndroidActivityInteractiveTokenCommandParameters; -import com.microsoft.identity.common.java.constants.FidoConstants; +import com.microsoft.identity.common.internal.util.StringUtil; import com.microsoft.identity.common.java.authorities.Authority; import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryAuthority; import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryB2CAuthority; @@ -51,8 +51,6 @@ import com.microsoft.identity.common.java.authscheme.AbstractAuthenticationScheme; import com.microsoft.identity.common.java.authscheme.AuthenticationSchemeFactory; import com.microsoft.identity.common.java.authscheme.BearerAuthenticationSchemeInternal; -import com.microsoft.identity.common.java.exception.ClientException; -import com.microsoft.identity.common.java.util.SchemaUtil; 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; @@ -66,12 +64,14 @@ import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignInSubmitCodeCommandParameters; import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignInSubmitPasswordCommandParameters; import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignInWithSLTCommandParameters; +import com.microsoft.identity.common.java.constants.FidoConstants; import com.microsoft.identity.common.java.dto.AccountRecord; +import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.providers.oauth2.OAuth2TokenCache; import com.microsoft.identity.common.java.providers.oauth2.OpenIdConnectPromptParameter; import com.microsoft.identity.common.java.request.SdkType; import com.microsoft.identity.common.java.ui.AuthorizationAgent; -import com.microsoft.identity.common.internal.util.StringUtil; +import com.microsoft.identity.common.java.util.SchemaUtil; import com.microsoft.identity.common.logging.Logger; import java.util.AbstractMap; @@ -82,8 +82,10 @@ import java.util.List; import java.util.Map; -import androidx.annotation.Nullable; +/** + * CommandParametersAdapter is a helper class to create various Command parameter objects. + */ public class CommandParametersAdapter { private static final String TAG = CommandParametersAdapter.class.getSimpleName(); @@ -283,6 +285,16 @@ public static DeviceCodeFlowCommandParameters createDeviceCodeFlowWithClaimsComm return commandParameters; } + /** + * Creates command parameter for [AcquireTokenNoFixedScopesCommand] of Native Auth. + * + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param accountRecord accountRecord object containing account information + * @param forceRefresh boolean parameter to denote if refresh should be forced + * @return Command parameter object + * @throws ClientException + */ public static AcquireTokenNoFixedScopesCommandParameters createAcquireTokenNoFixedScopesCommandParameters( @NonNull final PublicClientApplicationConfiguration configuration, @NonNull final OAuth2TokenCache tokenCache, @@ -345,6 +357,14 @@ public static DeviceCodeFlowCommandParameters createDeviceCodeFlowCommandParamet return commandParameters; } + /** + * Creates command parameter for [SignInStartCommand] of Native Auth. + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param username email address of the user + * @return Command parameter object + * @throws ClientException + */ public static SignInStartCommandParameters createSignInStartCommandParameters( @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, @NonNull final OAuth2TokenCache tokenCache, @@ -378,6 +398,16 @@ public static SignInStartCommandParameters createSignInStartCommandParameters( return commandParameters; } + /** + * Creates command parameter for [SignInStartCommand] of Native Auth using username and password + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param username email address of the user + * @param password password of the user + * @param scopes + * @return Command parameter object + * @throws ClientException + */ public static SignInStartUsingPasswordCommandParameters createSignInStartUsingPasswordCommandParameters( @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, @NonNull final OAuth2TokenCache tokenCache, @@ -415,6 +445,16 @@ public static SignInStartUsingPasswordCommandParameters createSignInStartUsingPa return commandParameters; } + /** + * Creates command parameter for [SignInStartCommand] of Native Auth using short lived token + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param signInSLT short lived token + * @param username email address of the user + * @param scopes + * @return Command parameter object + * @throws ClientException + */ public static SignInWithSLTCommandParameters createSignInWithSLTCommandParameters( @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, @NonNull final OAuth2TokenCache tokenCache, @@ -451,6 +491,16 @@ public static SignInWithSLTCommandParameters createSignInWithSLTCommandParameter return commandParameters; } + /** + * Creates command parameter for [SignInSubmitCodeCommand] of Native Auth + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param code Out of band code + * @param credentialToken credential token + * @param scopes + * @return Command parameter object + * @throws ClientException + */ public static SignInSubmitCodeCommandParameters createSignInSubmitCodeCommandParameters( @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, @NonNull final OAuth2TokenCache tokenCache, @@ -488,6 +538,14 @@ public static SignInSubmitCodeCommandParameters createSignInSubmitCodeCommandPar return commandParameters; } + /** + * Creates command parameter for [SignInResendCodeCommand] of Native Auth + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param credentialToken credential token + * @return Command parameter object + * @throws ClientException + */ public static SignInResendCodeCommandParameters createSignInResendCodeCommandParameters( @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, @NonNull final OAuth2TokenCache tokenCache, @@ -516,6 +574,16 @@ public static SignInResendCodeCommandParameters createSignInResendCodeCommandPar return commandParameters; } + /** + * Creates command parameter for [SignInSubmitPasswordCommand] of Native Auth + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param credentialToken credential token + * @param password password of the user + * @param scopes + * @return Command parameter object + * @throws ClientException + */ public static SignInSubmitPasswordCommandParameters createSignInSubmitPasswordCommandParameters( @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, @NonNull final OAuth2TokenCache tokenCache, diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/BaseStates.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/BaseStates.kt index bddc92e27..e1a47e2a8 100644 --- a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/BaseStates.kt +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/BaseStates.kt @@ -57,6 +57,9 @@ import java.io.Serializable sealed interface State +/** + * BaseState is the base class for various states in the Native Auth state machine. + */ abstract class BaseState(internal open val flowToken: String?) /** diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/Callback.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/Callback.kt index ce502db70..76927523f 100644 --- a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/Callback.kt +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/Callback.kt @@ -25,7 +25,21 @@ package com.microsoft.identity.client.statemachine.states import com.microsoft.identity.common.java.exception.BaseException +/** + * Callback class is used when the Native Auth features like SignUp, SignIn and SSPR are + * called from java code using callback variant. + */ interface Callback { + + /** + * onResult callback is made when the Native Auth API call is successful. + * @param result: Result of the successful Native Auth API. + */ fun onResult(result: T) + + /** + * onError callback is made when the Native Auth API call fails. + * @param exception: This object captures the exception encountered during the failure. + */ fun onError(exception: BaseException) } diff --git a/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthCIAMAuthorityTest.kt b/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthCIAMAuthorityTest.kt index 187f36d38..110ec0979 100644 --- a/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthCIAMAuthorityTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthCIAMAuthorityTest.kt @@ -1,3 +1,25 @@ +// 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.client.nativeauth import com.microsoft.identity.common.java.authorities.NativeAuthCIAMAuthority diff --git a/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthOAuth2ConfigurationTest.kt b/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthOAuth2ConfigurationTest.kt index 9cbcd9b04..9c2c93e15 100644 --- a/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthOAuth2ConfigurationTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthOAuth2ConfigurationTest.kt @@ -1,3 +1,25 @@ +// 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.client.nativeauth import com.microsoft.identity.common.java.providers.nativeauth.NativeAuthOAuth2Configuration diff --git a/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthPublicClientApplicationConfigurationTest.kt b/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthPublicClientApplicationConfigurationTest.kt index 251848cc5..df4f16f18 100644 --- a/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthPublicClientApplicationConfigurationTest.kt +++ b/msal/src/test/java/com/microsoft/identity/client/nativeauth/NativeAuthPublicClientApplicationConfigurationTest.kt @@ -1,3 +1,25 @@ +// 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.client.nativeauth import com.microsoft.identity.client.NativeAuthPublicClientApplicationConfiguration From 94aaee3b645b052d9c3789d6291118e8d4f138a6 Mon Sep 17 00:00:00 2001 From: Saurabh Gautam Date: Wed, 18 Oct 2023 16:19:50 +0100 Subject: [PATCH 04/12] Add javadoc --- .../statemachine/results/BaseResults.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/results/BaseResults.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/results/BaseResults.kt index fb37de3b9..7a9a09d47 100644 --- a/msal/src/main/java/com/microsoft/identity/client/statemachine/results/BaseResults.kt +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/results/BaseResults.kt @@ -27,14 +27,44 @@ package com.microsoft.identity.client.statemachine.results import com.microsoft.identity.client.statemachine.Error import com.microsoft.identity.client.statemachine.states.State +/** + * Result is the base class for all Result classes used in Native Auth. + */ interface Result { + /** + * SuccessResult which indicates the API call succeeded. + */ open class SuccessResult(open val nextState: State) : Result + + /** + * ErrorResult, which indicates that the flow failed. + */ open class ErrorResult(open val error: Error) : Result + + /** + * Complete Result, which indicates the flow is complete. + */ open class CompleteResult(open val resultValue: Any? = null) : Result + + /** + * CompleteWithNextStateResult which indicates the flow is complete but the next flow + * should be started e.g. SignIn flow is started after Signup is complete. + */ open class CompleteWithNextStateResult(override val resultValue: Any? = null, open val nextState: State?) : CompleteResult(resultValue = resultValue) + /** + * Returns true if the current API call succeeded + */ fun isSuccess(): Boolean = this is SuccessResult + + /** + * Returns true if the API call failed + */ fun isError(): Boolean = this is ErrorResult + + /** + * Returns true if the flow is complete + */ fun isComplete(): Boolean = this is CompleteResult } From c0cbf8f14ddb82f380d10af61bb33ffbf833dd2e Mon Sep 17 00:00:00 2001 From: Saurabh Gautam Date: Tue, 24 Oct 2023 16:20:24 +0100 Subject: [PATCH 05/12] Refactor class name --- common | 2 +- .../NativeAuthPublicClientApplication.kt | 10 +++++----- .../client/statemachine/states/SignInStates.kt | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/common b/common index 4e10557f2..6d5349376 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 4e10557f205354cb1a29971b136eba4a49341c64 +Subproject commit 6d5349376b05401da6f37668c49092e95d1e8714 diff --git a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt index cb3d4f1cf..00358c69f 100644 --- a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt @@ -52,7 +52,7 @@ import com.microsoft.identity.common.java.authorities.Authority import com.microsoft.identity.common.java.cache.ICacheRecord import com.microsoft.identity.common.java.commands.CommandCallback import com.microsoft.identity.common.java.controllers.CommandDispatcher -import com.microsoft.identity.common.java.controllers.results.ICommandResult +import com.microsoft.identity.common.java.controllers.results.INativeAuthCommandResult import com.microsoft.identity.common.java.controllers.results.SignInCommandResult import com.microsoft.identity.common.java.controllers.results.SignInStartCommandResult import com.microsoft.identity.common.java.eststelemetry.PublicApiId @@ -307,7 +307,7 @@ class NativeAuthPublicClientApplication( channel = result.challengeChannel ) } - is ICommandResult.UnknownError -> { + is INativeAuthCommandResult.UnknownError -> { SignInResult.UnexpectedError( error = GeneralError( errorMessage = result.errorDescription, @@ -365,7 +365,7 @@ class NativeAuthPublicClientApplication( ) ) } - is ICommandResult.Redirect -> { + is INativeAuthCommandResult.Redirect -> { SignInResult.BrowserRequired( error = BrowserRequiredError( correlationId = result.correlationId @@ -506,7 +506,7 @@ class NativeAuthPublicClientApplication( ) } - is ICommandResult.Redirect -> { + is INativeAuthCommandResult.Redirect -> { SignInResult.BrowserRequired( error = BrowserRequiredError( correlationId = result.correlationId @@ -514,7 +514,7 @@ class NativeAuthPublicClientApplication( ) } - is ICommandResult.UnknownError -> { + is INativeAuthCommandResult.UnknownError -> { SignInResult.UnexpectedError( error = GeneralError( errorMessage = result.errorDescription, diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt index 3be070309..1a48db813 100644 --- a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt @@ -42,7 +42,7 @@ import com.microsoft.identity.common.internal.commands.SignInSubmitPasswordComma import com.microsoft.identity.common.internal.commands.SignInWithSLTCommand import com.microsoft.identity.common.internal.controllers.NativeAuthMsalController import com.microsoft.identity.common.java.controllers.CommandDispatcher -import com.microsoft.identity.common.java.controllers.results.ICommandResult +import com.microsoft.identity.common.java.controllers.results.INativeAuthCommandResult import com.microsoft.identity.common.java.controllers.results.SignInCommandResult import com.microsoft.identity.common.java.controllers.results.SignInResendCodeCommandResult import com.microsoft.identity.common.java.controllers.results.SignInSubmitCodeCommandResult @@ -136,7 +136,7 @@ class SignInCodeRequiredState internal constructor( ) } - is ICommandResult.Redirect -> { + is INativeAuthCommandResult.Redirect -> { SignInResult.BrowserRequired( error = BrowserRequiredError( correlationId = result.correlationId @@ -144,7 +144,7 @@ class SignInCodeRequiredState internal constructor( ) } - is ICommandResult.UnknownError -> { + is INativeAuthCommandResult.UnknownError -> { Logger.warn( TAG, "Unexpected result: $result" @@ -221,7 +221,7 @@ class SignInCodeRequiredState internal constructor( ) } - is ICommandResult.Redirect -> { + is INativeAuthCommandResult.Redirect -> { SignInResult.BrowserRequired( error = BrowserRequiredError( correlationId = result.correlationId @@ -229,7 +229,7 @@ class SignInCodeRequiredState internal constructor( ) } - is ICommandResult.UnknownError -> { + is INativeAuthCommandResult.UnknownError -> { Logger.warn( TAG, "Unexpected result: $result" @@ -325,14 +325,14 @@ class SignInPasswordRequiredState( ) ) } - is ICommandResult.Redirect -> { + is INativeAuthCommandResult.Redirect -> { SignInResult.BrowserRequired( error = BrowserRequiredError( correlationId = result.correlationId ) ) } - is ICommandResult.UnknownError -> { + is INativeAuthCommandResult.UnknownError -> { Logger.warn( TAG, "Unexpected result: $result" @@ -456,14 +456,14 @@ abstract class SignInAfterSignUpBaseState( ) ) } - is ICommandResult.Redirect -> { + is INativeAuthCommandResult.Redirect -> { SignInResult.BrowserRequired( error = BrowserRequiredError( correlationId = result.correlationId ) ) } - is ICommandResult.UnknownError -> { + is INativeAuthCommandResult.UnknownError -> { Logger.warn( TAG, "Unexpected result: $result" From 21e7c80f6983f63bd09e7d7cf4a67ee37f516249 Mon Sep 17 00:00:00 2001 From: Saurabh Gautam Date: Fri, 3 Nov 2023 15:24:38 +0000 Subject: [PATCH 06/12] Use char array instead of string --- common | 2 +- .../identity/client/INativeAuthPublicClientApplication.kt | 4 ++-- .../identity/client/NativeAuthPublicClientApplication.kt | 6 ++++-- .../client/internal/CommandParametersAdapter.java | 4 ++-- .../identity/client/statemachine/states/SignInStates.kt | 8 +++++--- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/common b/common index 6d5349376..db3169353 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 6d5349376b05401da6f37668c49092e95d1e8714 +Subproject commit db3169353272413f62308e880b88f20ef733377e diff --git a/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt index 885c7da7e..b03843bac 100644 --- a/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt @@ -92,7 +92,7 @@ interface INativeAuthPublicClientApplication : IPublicClientApplication { * @return [com.microsoft.identity.client.statemachine.results.SignInUsingPasswordResult] see detailed possible return state under the object. * @throws MsalClientException if an account is already signed in. */ - suspend fun signInUsingPassword(username: String, password: String, scopes: List? = null): SignInUsingPasswordResult + suspend fun signInUsingPassword(username: String, password: CharArray, scopes: List? = null): SignInUsingPasswordResult /** * Sign in the account using username and password; callback variant. @@ -104,5 +104,5 @@ interface INativeAuthPublicClientApplication : IPublicClientApplication { * @return [com.microsoft.identity.client.statemachine.results.SignInUsingPasswordResult] see detailed possible return state under the object. * @throws MsalClientException if an account is already signed in. */ - fun signInUsingPassword(username: String, password: String, scopes: List? = null, callback: NativeAuthPublicClientApplication.SignInUsingPasswordCallback) + fun signInUsingPassword(username: String, password: CharArray, scopes: List? = null, callback: NativeAuthPublicClientApplication.SignInUsingPasswordCallback) } diff --git a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt index 00358c69f..3d4161fbb 100644 --- a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt @@ -61,6 +61,7 @@ import com.microsoft.identity.common.java.logging.LogSession import com.microsoft.identity.common.java.logging.Logger import com.microsoft.identity.common.java.providers.microsoft.azureactivedirectory.AzureActiveDirectory import com.microsoft.identity.common.java.util.ResultFuture +import com.microsoft.identity.common.java.util.StringUtil import com.microsoft.identity.common.java.util.checkAndWrapCommandResultType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -390,7 +391,7 @@ class NativeAuthPublicClientApplication( */ override fun signInUsingPassword( username: String, - password: String, + password: CharArray, scopes: List?, callback: SignInUsingPasswordCallback ) { @@ -417,7 +418,7 @@ class NativeAuthPublicClientApplication( */ override suspend fun signInUsingPassword( username: String, - password: String, + password: CharArray, scopes: List? ): SignInUsingPasswordResult { LogSession.logMethodCall(TAG, "${TAG}.signInUsingPassword") @@ -453,6 +454,7 @@ class NativeAuthPublicClientApplication( ) val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + StringUtil.overwriteWithZero(params.password) return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { is SignInCommandResult.Complete -> { diff --git a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java index e328aaae6..a705f4743 100644 --- a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java +++ b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java @@ -412,7 +412,7 @@ public static SignInStartUsingPasswordCommandParameters createSignInStartUsingPa @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, @NonNull final OAuth2TokenCache tokenCache, @NonNull final String username, - @NonNull final String password, + @NonNull final char[] password, final List scopes) throws ClientException { final AbstractAuthenticationScheme authenticationScheme = AuthenticationSchemeFactory.createScheme( AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext()), @@ -588,7 +588,7 @@ public static SignInSubmitPasswordCommandParameters createSignInSubmitPasswordCo @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, @NonNull final OAuth2TokenCache tokenCache, @NonNull final String credentialToken, - @NonNull final String password, + @NonNull final char[] password, final List scopes) throws ClientException { final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt index 1a48db813..43f710400 100644 --- a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt @@ -51,7 +51,7 @@ import com.microsoft.identity.common.java.controllers.results.SignInWithSLTComma import com.microsoft.identity.common.java.eststelemetry.PublicApiId import com.microsoft.identity.common.java.logging.LogSession import com.microsoft.identity.common.java.logging.Logger -import com.microsoft.identity.common.java.logging.Logger.LogLevel +import com.microsoft.identity.common.java.util.StringUtil import com.microsoft.identity.common.java.util.checkAndWrapCommandResultType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -266,7 +266,7 @@ class SignInPasswordRequiredState( * @param callback [com.microsoft.identity.client.statemachine.states.SignInPasswordRequiredState.SubmitPasswordCallback] to receive the result on. * @return The results of the submit password action. */ - fun submitPassword(password: String, callback: SubmitPasswordCallback) { + fun submitPassword(password: CharArray, callback: SubmitPasswordCallback) { LogSession.logMethodCall(TAG, "${TAG}.submitPassword") NativeAuthPublicClientApplication.pcaScope.launch { try { @@ -285,7 +285,7 @@ class SignInPasswordRequiredState( * @param password the password to submit. * @return The results of the submit password action. */ - suspend fun submitPassword(password: String): SignInSubmitPasswordResult { + suspend fun submitPassword(password: CharArray): SignInSubmitPasswordResult { LogSession.logMethodCall(TAG, "${TAG}.submitPassword(password: String)") return withContext(Dispatchers.IO) { val params = CommandParametersAdapter.createSignInSubmitPasswordCommandParameters( @@ -304,6 +304,8 @@ class SignInPasswordRequiredState( val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(signInSubmitPasswordCommand).get() + StringUtil.overwriteWithZero(params.password) + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { is SignInCommandResult.InvalidCredentials -> { SignInResult.InvalidCredentials( From e5246003b50f1050a7e91243249b3f9cae9de8a2 Mon Sep 17 00:00:00 2001 From: Saurabh Gautam Date: Mon, 6 Nov 2023 13:49:44 +0000 Subject: [PATCH 07/12] Use try finally --- common | 2 +- .../NativeAuthPublicClientApplication.kt | 171 +++++++++--------- .../internal/CommandParametersAdapter.java | 5 +- .../statemachine/states/SignInStates.kt | 101 ++++++----- 4 files changed, 143 insertions(+), 136 deletions(-) diff --git a/common b/common index db3169353..14e4b4ac4 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit db3169353272413f62308e880b88f20ef733377e +Subproject commit 14e4b4ac491f171c1a35d4f238e224c2b0e48c20 diff --git a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt index 3d4161fbb..2b57cac0f 100644 --- a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt @@ -29,11 +29,7 @@ import com.microsoft.identity.client.exception.MsalException import com.microsoft.identity.client.internal.CommandParametersAdapter import com.microsoft.identity.client.statemachine.BrowserRequiredError import com.microsoft.identity.client.statemachine.GeneralError -import com.microsoft.identity.client.statemachine.InvalidAttributesError -import com.microsoft.identity.client.statemachine.InvalidEmailError -import com.microsoft.identity.client.statemachine.InvalidPasswordError import com.microsoft.identity.client.statemachine.PasswordIncorrectError -import com.microsoft.identity.client.statemachine.UserAlreadyExistsError import com.microsoft.identity.client.statemachine.UserNotFoundError import com.microsoft.identity.client.statemachine.results.SignInResult import com.microsoft.identity.client.statemachine.results.SignInUsingPasswordResult @@ -447,100 +443,105 @@ class NativeAuthPublicClientApplication( scopes ) - val command = SignInStartCommand( - params, - NativeAuthMsalController(), - PublicApiId.NATIVE_AUTH_SIGN_IN_WITH_EMAIL_PASSWORD - ) + try + { + val command = SignInStartCommand( + params, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_SIGN_IN_WITH_EMAIL_PASSWORD + ) - val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() - StringUtil.overwriteWithZero(params.password) + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() - return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { - is SignInCommandResult.Complete -> { - val authenticationResult = - AuthenticationResultAdapter.adapt(result.authenticationResult) + return@withContext when (val result = + rawCommandResult.checkAndWrapCommandResultType()) { + is SignInCommandResult.Complete -> { + val authenticationResult = + AuthenticationResultAdapter.adapt(result.authenticationResult) - SignInResult.Complete( - resultValue = AccountResult.createFromAuthenticationResult( - authenticationResult, - nativeAuthConfig + SignInResult.Complete( + resultValue = AccountResult.createFromAuthenticationResult( + authenticationResult, + nativeAuthConfig + ) ) - ) - } - is SignInCommandResult.CodeRequired -> { - Logger.warn( - TAG, - "Sign in with password flow was started, but server requires" + - "a code. Password was not sent to the API; switching to code " + - "authentication." - ) - SignInResult.CodeRequired( - nextState = SignInCodeRequiredState( - flowToken = result.credentialToken, - scopes = scopes, - config = nativeAuthConfig - ), - codeLength = result.codeLength, - sentTo = result.challengeTargetLabel, - channel = result.challengeChannel - ) - } - is SignInCommandResult.UserNotFound -> { - SignInResult.UserNotFound( - error = UserNotFoundError( - errorMessage = result.errorDescription, - error = result.error, - correlationId = result.correlationId, - errorCodes = result.errorCodes + } + is SignInCommandResult.CodeRequired -> { + Logger.warn( + TAG, + "Sign in with password flow was started, but server requires" + + "a code. Password was not sent to the API; switching to code " + + "authentication." ) - ) - } + SignInResult.CodeRequired( + nextState = SignInCodeRequiredState( + flowToken = result.credentialToken, + scopes = scopes, + config = nativeAuthConfig + ), + codeLength = result.codeLength, + sentTo = result.challengeTargetLabel, + channel = result.challengeChannel + ) + } + is SignInCommandResult.UserNotFound -> { + SignInResult.UserNotFound( + error = UserNotFoundError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + errorCodes = result.errorCodes + ) + ) + } - is SignInCommandResult.InvalidCredentials -> { - SignInResult.InvalidCredentials( - error = PasswordIncorrectError( - errorMessage = result.errorDescription, - error = result.error, - correlationId = result.correlationId, - errorCodes = result.errorCodes + is SignInCommandResult.InvalidCredentials -> { + SignInResult.InvalidCredentials( + error = PasswordIncorrectError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + errorCodes = result.errorCodes + ) ) - ) - } + } - is INativeAuthCommandResult.Redirect -> { - SignInResult.BrowserRequired( - error = BrowserRequiredError( - correlationId = result.correlationId + is INativeAuthCommandResult.Redirect -> { + SignInResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) ) - ) - } + } - is INativeAuthCommandResult.UnknownError -> { - SignInResult.UnexpectedError( - error = GeneralError( - errorMessage = result.errorDescription, - error = result.error, - correlationId = result.correlationId, - details = result.details, - errorCodes = result.errorCodes, - exception = result.exception + is INativeAuthCommandResult.UnknownError -> { + SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + errorCodes = result.errorCodes, + exception = result.exception + ) ) - ) - } - is SignInCommandResult.PasswordRequired -> { - Logger.warn( - TAG, - "Unexpected result $result" - ) - SignInResult.UnexpectedError( - error = GeneralError( - errorMessage = "Unexpected state", - error = "unexpected_state", - correlationId = "UNSET" + } + is SignInCommandResult.PasswordRequired -> { + Logger.warn( + TAG, + "Unexpected result $result" ) - ) + SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = "Unexpected state", + error = "unexpected_state", + correlationId = "UNSET" + ) + ) + } } + } finally { + StringUtil.overwriteWithNull(params.password) } } } diff --git a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java index a705f4743..334cb03db 100644 --- a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java +++ b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java @@ -421,8 +421,9 @@ public static SignInStartUsingPasswordCommandParameters createSignInStartUsingPa final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); - final SignInStartUsingPasswordCommandParameters commandParameters = SignInStartUsingPasswordCommandParameters.builder() - .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) + SignInStartUsingPasswordCommandParametersBuilder builder = SignInStartUsingPasswordCommandParameters.builder(); + + final SignInStartUsingPasswordCommandParameters commandParameters = builder.platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) .applicationName(configuration.getAppContext().getPackageName()) .applicationVersion(getPackageVersion(configuration.getAppContext())) .clientId(configuration.getClientId()) diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt index 43f710400..15d0769cb 100644 --- a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt @@ -296,60 +296,65 @@ class SignInPasswordRequiredState( scopes ) - val signInSubmitPasswordCommand = SignInSubmitPasswordCommand( - parameters = params, - controller = NativeAuthMsalController(), - publicApiId = PublicApiId.NATIVE_AUTH_SIGN_IN_SUBMIT_PASSWORD - ) - - val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(signInSubmitPasswordCommand).get() - - StringUtil.overwriteWithZero(params.password) + try + { + val signInSubmitPasswordCommand = SignInSubmitPasswordCommand( + parameters = params, + controller = NativeAuthMsalController(), + publicApiId = PublicApiId.NATIVE_AUTH_SIGN_IN_SUBMIT_PASSWORD + ) - return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { - is SignInCommandResult.InvalidCredentials -> { - SignInResult.InvalidCredentials( - error = PasswordIncorrectError( - error = result.error, - errorMessage = result.errorDescription, - correlationId = result.correlationId, - errorCodes = result.errorCodes + val rawCommandResult = + CommandDispatcher.submitSilentReturningFuture(signInSubmitPasswordCommand).get() + + return@withContext when (val result = + rawCommandResult.checkAndWrapCommandResultType()) { + is SignInCommandResult.InvalidCredentials -> { + SignInResult.InvalidCredentials( + error = PasswordIncorrectError( + error = result.error, + errorMessage = result.errorDescription, + correlationId = result.correlationId, + errorCodes = result.errorCodes + ) ) - ) - } - is SignInCommandResult.Complete -> { - val authenticationResult = - AuthenticationResultAdapter.adapt(result.authenticationResult) - SignInResult.Complete( - resultValue = AccountResult.createFromAuthenticationResult( - authenticationResult = authenticationResult, - config = config + } + is SignInCommandResult.Complete -> { + val authenticationResult = + AuthenticationResultAdapter.adapt(result.authenticationResult) + SignInResult.Complete( + resultValue = AccountResult.createFromAuthenticationResult( + authenticationResult = authenticationResult, + config = config + ) ) - ) - } - is INativeAuthCommandResult.Redirect -> { - SignInResult.BrowserRequired( - error = BrowserRequiredError( - correlationId = result.correlationId + } + is INativeAuthCommandResult.Redirect -> { + SignInResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) ) - ) - } - is INativeAuthCommandResult.UnknownError -> { - Logger.warn( - TAG, - "Unexpected result: $result" - ) - SignInResult.UnexpectedError( - error = GeneralError( - errorMessage = result.errorDescription, - error = result.error, - correlationId = result.correlationId, - details = result.details, - errorCodes = result.errorCodes, - exception = result.exception + } + is INativeAuthCommandResult.UnknownError -> { + Logger.warn( + TAG, + "Unexpected result: $result" ) - ) + SignInResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + errorCodes = result.errorCodes, + exception = result.exception + ) + ) + } } + } finally { + StringUtil.overwriteWithNull(params.password) } } } From e79735ad6fe7d06745b282a126f1f4220bb04212 Mon Sep 17 00:00:00 2001 From: Saurabh Gautam Date: Mon, 6 Nov 2023 23:13:35 +0000 Subject: [PATCH 08/12] Added a comment for PBI --- .../com/microsoft/identity/client/statemachine/Error.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/Error.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/Error.kt index 1fc3d5251..8dd2b92fd 100644 --- a/msal/src/main/java/com/microsoft/identity/client/statemachine/Error.kt +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/Error.kt @@ -23,6 +23,10 @@ package com.microsoft.identity.client.statemachine +/** + * Error is a base class for all errors present in the Native Auth. + * This file will be refactored as part of https://identitydivision.visualstudio.com/Engineering/_workitems/edit/2730354 + */ sealed class Error( open val error: String? = null, open val errorMessage: String?, @@ -32,7 +36,7 @@ sealed class Error( ) /** - * GeneralError is a base class for all errors present in the Native Auth. + * GeneralError represents general errors in NativeAuth */ class GeneralError( override var error: String? = null, From ead4af7c8fc8688e0379fcaa0a53e09b6be1b631 Mon Sep 17 00:00:00 2001 From: Saurabh Gautam Date: Wed, 8 Nov 2023 15:27:30 +0000 Subject: [PATCH 09/12] Fix build --- .../identity/client/internal/CommandParametersAdapter.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java index c43b0de17..1da0f82ac 100644 --- a/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java +++ b/msal/src/main/java/com/microsoft/identity/client/internal/CommandParametersAdapter.java @@ -421,9 +421,8 @@ public static SignInStartUsingPasswordCommandParameters createSignInStartUsingPa final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); - SignInStartUsingPasswordCommandParametersBuilder builder = SignInStartUsingPasswordCommandParameters.builder(); - - final SignInStartUsingPasswordCommandParameters commandParameters = builder.platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) + final SignInStartUsingPasswordCommandParameters commandParameters = SignInStartUsingPasswordCommandParameters.builder() + .platformComponents(AndroidPlatformComponentsFactory.createFromContext(configuration.getAppContext())) .applicationName(configuration.getAppContext().getPackageName()) .applicationVersion(getPackageVersion(configuration.getAppContext())) .clientId(configuration.getClientId()) From 0c8a4f003aed57d33636f01cfde3cf7c4795546b Mon Sep 17 00:00:00 2001 From: Saurabh Gautam Date: Wed, 8 Nov 2023 16:54:07 +0000 Subject: [PATCH 10/12] More comments --- common | 2 +- .../NativeAuthPublicClientApplication.kt | 41 ++++++++----------- .../exception/MsalUserNotFoundException.java | 32 --------------- .../statemachine/states/SignInStates.kt | 38 +++++++++++++++++ 4 files changed, 56 insertions(+), 57 deletions(-) delete mode 100644 msal/src/main/java/com/microsoft/identity/client/exception/MsalUserNotFoundException.java diff --git a/common b/common index f72b8fa9e..76bf90d71 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit f72b8fa9ed6f4174da9ce36802918bd0d75f044c +Subproject commit 76bf90d71fab2c5fb2f2f007f269647bd245605b diff --git a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt index 2b57cac0f..3fc1d3e0c 100644 --- a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt @@ -264,18 +264,7 @@ class NativeAuthPublicClientApplication( return withContext(Dispatchers.IO) { LogSession.logMethodCall(TAG, "${TAG}.signIn") - val doesAccountExist = checkForPersistedAccount().get() - if (doesAccountExist) { - Logger.error( - TAG, - "An account is already signed in.", - null - ) - throw MsalClientException( - MsalClientException.INVALID_PARAMETER, - "An account is already signed in." - ) - } + verifyUserIsNotSignedIn() val params = CommandParametersAdapter.createSignInStartCommandParameters( nativeAuthConfig, @@ -421,18 +410,7 @@ class NativeAuthPublicClientApplication( return withContext(Dispatchers.IO) { LogSession.logMethodCall(TAG, "${TAG}.signInUsingPassword.withContext") - val doesAccountExist = checkForPersistedAccount().get() - if (doesAccountExist) { - Logger.error( - TAG, - "An account is already signed in.", - null - ) - throw MsalClientException( - MsalClientException.INVALID_PARAMETER, - "An account is already signed in." - ) - } + verifyUserIsNotSignedIn() val params = CommandParametersAdapter.createSignInStartUsingPasswordCommandParameters( @@ -546,6 +524,21 @@ class NativeAuthPublicClientApplication( } } + private fun verifyUserIsNotSignedIn() { + val doesAccountExist = checkForPersistedAccount().get() + if (doesAccountExist) { + Logger.error( + TAG, + "An account is already signed in.", + null + ) + throw MsalClientException( + MsalClientException.INVALID_PARAMETER, + "An account is already signed in." + ) + } + } + private fun checkForPersistedAccount(): ResultFuture { LogSession.logMethodCall(TAG, "${TAG}.checkForPersistedAccount") val future = ResultFuture() diff --git a/msal/src/main/java/com/microsoft/identity/client/exception/MsalUserNotFoundException.java b/msal/src/main/java/com/microsoft/identity/client/exception/MsalUserNotFoundException.java deleted file mode 100644 index e162f72c0..000000000 --- a/msal/src/main/java/com/microsoft/identity/client/exception/MsalUserNotFoundException.java +++ /dev/null @@ -1,32 +0,0 @@ -// 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.client.exception; - -public class MsalUserNotFoundException extends MsalException { - private final static String USER_NOT_FOUND_ERROR_CODE = "user_not_found"; - - public MsalUserNotFoundException() { - super(USER_NOT_FOUND_ERROR_CODE, "No user found in cache"); - } -} diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt index 15d0769cb..0faaa4eb1 100644 --- a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignInStates.kt @@ -58,6 +58,14 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.Serializable +/** + * Native Auth uses a state machine to denote state and transitions for a user. + * SignInCodeRequiredState class represents a state where the user has to provide a code to progress + * in the signin flow. + * @property flowToken: Flow token to be passed in the next request + * @property scopes: List of scopes + * @property config Configuration used by Native Auth + */ class SignInCodeRequiredState internal constructor( override val flowToken: String, private val scopes: List?, @@ -65,6 +73,9 @@ class SignInCodeRequiredState internal constructor( ) : BaseState(flowToken), State, Serializable { private val TAG: String = SignInCodeRequiredState::class.java.simpleName + /** + * SubmitCodeCallback receives the result for submit code for SignIn for Native Auth + */ interface SubmitCodeCallback : Callback /** @@ -164,6 +175,9 @@ class SignInCodeRequiredState internal constructor( } } + /** + * SubmitCodeCallback receives the result for resend code for SignIn for Native Auth + */ interface ResendCodeCallback : Callback /** @@ -250,6 +264,14 @@ class SignInCodeRequiredState internal constructor( } } +/** + * Native Auth uses a state machine to denote state and transitions for a user. + * SignInPasswordRequiredState class represents a state where the user has to provide a password to progress + * in the signin flow. + * @property flowToken: Flow token to be passed in the next request + * @property scopes: List of scopes + * @property config Configuration used by Native Auth + */ class SignInPasswordRequiredState( override val flowToken: String, private val scopes: List?, @@ -257,6 +279,9 @@ class SignInPasswordRequiredState( ) : BaseState(flowToken), State { private val TAG: String = SignInPasswordRequiredState::class.java.simpleName + /** + * SubmitCodeCallback receives the result for submit password for SignIn for Native Auth + */ interface SubmitPasswordCallback : Callback /** @@ -360,12 +385,25 @@ class SignInPasswordRequiredState( } } +/** + * Native Auth uses a state machine to denote state and transitions for a user. + * SignInAfterSignUpBaseState class is an abstract class to represent signin state after + * successfull signup + * in the signin flow. + * @property signInVerificationCode: Short lived token from signup APIS + * @property username: Username of the user + * @property config Configuration used by Native Auth + */ abstract class SignInAfterSignUpBaseState( internal open val signInVerificationCode: String?, internal open val username: String, private val config: NativeAuthPublicClientApplicationConfiguration ) : BaseState(signInVerificationCode), State, Serializable { private val TAG: String = SignInAfterSignUpBaseState::class.java.simpleName + + /** + * SubmitCodeCallback receives the result for sign in after signup for Native Auth + */ interface SignInAfterSignUpCallback : Callback /** From 8685f9ec84c34a06c39d8b4064eee617dccffd67 Mon Sep 17 00:00:00 2001 From: Saurabh Gautam Date: Wed, 8 Nov 2023 17:37:43 +0000 Subject: [PATCH 11/12] Fix version file --- msal/versioning/version.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/msal/versioning/version.properties b/msal/versioning/version.properties index 4415edb5c..81eda03de 100644 --- a/msal/versioning/version.properties +++ b/msal/versioning/version.properties @@ -1,4 +1,3 @@ #Wed Aug 01 15:24:11 PDT 2018 -<<<<<<< HEAD versionName=4.9.0 versionCode=0 From bc3ef8cef77f3a00c087e9c4286ad7ae11e9690c Mon Sep 17 00:00:00 2001 From: Saurabh Gautam Date: Thu, 9 Nov 2023 10:48:12 +0000 Subject: [PATCH 12/12] Move AccountResult to its own file --- .../statemachine/states/AccountResult.kt | 281 ++++++++++++++++++ .../client/statemachine/states/BaseStates.kt | 258 ---------------- 2 files changed, 281 insertions(+), 258 deletions(-) create mode 100644 msal/src/main/java/com/microsoft/identity/client/statemachine/states/AccountResult.kt diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/AccountResult.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/AccountResult.kt new file mode 100644 index 000000000..029836414 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/AccountResult.kt @@ -0,0 +1,281 @@ +// 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.client.statemachine.states + +import com.microsoft.identity.client.Account +import com.microsoft.identity.client.AcquireTokenSilentParameters +import com.microsoft.identity.client.AuthenticationResultAdapter +import com.microsoft.identity.client.IAccount +import com.microsoft.identity.client.IAuthenticationResult +import com.microsoft.identity.client.NativeAuthPublicClientApplication +import com.microsoft.identity.client.NativeAuthPublicClientApplicationConfiguration +import com.microsoft.identity.client.PublicClientApplication +import com.microsoft.identity.client.exception.MsalClientException +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.internal.CommandParametersAdapter +import com.microsoft.identity.client.statemachine.results.SignOutResult +import com.microsoft.identity.common.internal.commands.AcquireTokenNoFixedScopesCommand +import com.microsoft.identity.common.internal.commands.RemoveCurrentAccountCommand +import com.microsoft.identity.common.internal.controllers.LocalMSALController +import com.microsoft.identity.common.internal.controllers.NativeAuthMsalController +import com.microsoft.identity.common.java.commands.CommandCallback +import com.microsoft.identity.common.java.controllers.CommandDispatcher +import com.microsoft.identity.common.java.controllers.ExceptionAdapter +import com.microsoft.identity.common.java.dto.AccountRecord +import com.microsoft.identity.common.java.eststelemetry.PublicApiId +import com.microsoft.identity.common.java.exception.BaseException +import com.microsoft.identity.common.java.exception.ServiceException +import com.microsoft.identity.common.java.logging.LogSession +import com.microsoft.identity.common.java.logging.Logger +import com.microsoft.identity.common.java.result.ILocalAuthenticationResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.Serializable + +/** + * AcccountResult returned as part of a successful completion of sign in flow [com.microsoft.identity.client.statemachine.results.SignInResult.Complete]. + */ +class AccountResult private constructor( + private val account: IAccount, + private val config: NativeAuthPublicClientApplicationConfiguration +) : Serializable { + + companion object { + + private val TAG = NativeAuthPublicClientApplication::class.java.simpleName + + fun createFromAuthenticationResult( + authenticationResult: IAuthenticationResult, + config: NativeAuthPublicClientApplicationConfiguration + ): AccountResult { + return AccountResult( + account = authenticationResult.account, + config = config + ) + } + + fun createFromAccountResult( + account: IAccount, + config: NativeAuthPublicClientApplicationConfiguration + ): AccountResult { + return AccountResult( + account = account, + config = config + ) + } + } + + interface SignOutCallback : Callback + + /** + * Remove the current account from the cache; callback variant. + * + * @param callback [com.microsoft.identity.client.statemachine.states.AccountResult.SignOutCallback] to receive the result on. + */ + fun signOut(callback: SignOutCallback) { + LogSession.logMethodCall(TAG, "${TAG}.signOut") + NativeAuthPublicClientApplication.pcaScope.launch { + try { + val result = signOut() + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in signOut", e) + callback.onError(e) + } + } + } + + /** + * Remove the current account from the cache; Kotlin coroutines variant. + */ + suspend fun signOut(): SignOutResult { + return withContext(Dispatchers.IO) { + LogSession.logMethodCall(TAG, "${TAG}.signOut.withContext") + + val account: IAccount = + NativeAuthPublicClientApplication.getCurrentAccountInternal(config) + ?: throw MsalClientException( + MsalClientException.NO_CURRENT_ACCOUNT, + MsalClientException.NO_CURRENT_ACCOUNT_ERROR_MESSAGE + ) + + val requestAccountRecord = AccountRecord() + requestAccountRecord.environment = (account as Account).environment + requestAccountRecord.homeAccountId = account.homeAccountId + + val params = CommandParametersAdapter.createRemoveAccountCommandParameters( + config, + config.oAuth2TokenCache, + requestAccountRecord + ) + + val removeCurrentAccountCommandParameters = RemoveCurrentAccountCommand( + params, + LocalMSALController(), + object : CommandCallback { + override fun onError(error: BaseException?) { + // Do nothing, handled by CommandDispatcher.submitSilentReturningFuture() + } + + override fun onTaskCompleted(result: Boolean?) { + // Do nothing, handled by CommandDispatcher.submitSilentReturningFuture() + } + + override fun onCancel() { + // Do nothing + } + }, + PublicApiId.NATIVE_AUTH_ACCOUNT_SIGN_OUT + ) + + val result = CommandDispatcher.submitSilentReturningFuture(removeCurrentAccountCommandParameters) + .get().result as Boolean + + return@withContext if (result) { + SignOutResult.Complete + } else { + Logger.error( + TAG, + "Unexpected error during signOut.", + null + ) + throw MsalClientException( + MsalClientException.UNKNOWN_ERROR, + "Unexpected error during signOut." + ) + } + } + } + + /** + * Gets the current account. + * + * @return account [com.microsoft.identity.client.IAccount]. + */ + fun getAccount(): IAccount { + return account + } + + /** + * Gets the current account's ID token (if present). + * + * @return idToken [String]. + */ + fun getIdToken(): String? { + return account.idToken + } + + /** + * Gets the claims associated with the current account. + * + * @return A Map of claims. + */ + fun getClaims(): Map? { + return account.claims + } + + interface GetAccessTokenCallback : Callback + + /** + * Retrieves the access token for the currently signed in account from the cache. + * If the access token is expired, it will be attempted to be refreshed using the refresh token that's stored in the cache; + * callback variant. + * + * @return [com.microsoft.identity.client.IAuthenticationResult] If successful. + * @throws [MsalClientException] If the the account doesn't exist in the cache. + * @throws [ServiceException] If the refresh token doesn't exist in the cache/is expired, or the refreshing fails. + */ + fun getAccessToken(forceRefresh: Boolean = false, callback: GetAccessTokenCallback) { + LogSession.logMethodCall(TAG, "${TAG}.getAccessToken") + NativeAuthPublicClientApplication.pcaScope.launch { + try { + val result = getAccessToken(forceRefresh) + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in getAccessToken", e) + callback.onError(e) + } + } + } + + /** + * Retrieves the access token for the currently signed in account from the cache. + * If the access token is expired, it will be attempted to be refreshed using the refresh token that's stored in the cache; + * Kotlin coroutines variant. + * + * @return [com.microsoft.identity.client.IAuthenticationResult] If successful. + * @throws [MsalClientException] If the the account doesn't exist in the cache. + * @throws [ServiceException] If the refresh token doesn't exist in the cache/is expired, or the refreshing fails. + */ + suspend fun getAccessToken(forceRefresh: Boolean = false): IAuthenticationResult? { + LogSession.logMethodCall(TAG, "${TAG}.getAccessToken(forceRefresh: Boolean)") + return withContext(Dispatchers.IO) { + val account = + NativeAuthPublicClientApplication.getCurrentAccountInternal(config) as? Account + ?: throw MsalClientException( + MsalClientException.NO_CURRENT_ACCOUNT, + MsalClientException.NO_CURRENT_ACCOUNT_ERROR_MESSAGE + ) + + val acquireTokenSilentParameters = AcquireTokenSilentParameters.Builder() + .forAccount(account) + .fromAuthority(account.authority) + .build() + + val accountToBeUsed = PublicClientApplication.selectAccountRecordForTokenRequest( + config, + acquireTokenSilentParameters + ) + + val params = CommandParametersAdapter.createAcquireTokenNoFixedScopesCommandParameters( + config, + config.oAuth2TokenCache, + accountToBeUsed, + forceRefresh + ) + + val command = AcquireTokenNoFixedScopesCommand( + params, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_ACCOUNT_GET_ACCESS_TOKEN + ) + + val commandResult = CommandDispatcher.submitSilentReturningFuture(command) + .get().result + + when (commandResult) { + is ServiceException -> { + throw ExceptionAdapter.convertToNativeAuthException(commandResult) + } + is Exception -> { + throw commandResult + } + else -> { + return@withContext AuthenticationResultAdapter.adapt(commandResult as ILocalAuthenticationResult) + } + } + } + } +} diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/BaseStates.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/BaseStates.kt index e1a47e2a8..ac4a1d41b 100644 --- a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/BaseStates.kt +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/BaseStates.kt @@ -23,267 +23,9 @@ package com.microsoft.identity.client.statemachine.states -import com.microsoft.identity.client.Account -import com.microsoft.identity.client.AcquireTokenSilentParameters -import com.microsoft.identity.client.AuthenticationResultAdapter -import com.microsoft.identity.client.IAccount -import com.microsoft.identity.client.IAuthenticationResult -import com.microsoft.identity.client.NativeAuthPublicClientApplication -import com.microsoft.identity.client.NativeAuthPublicClientApplicationConfiguration -import com.microsoft.identity.client.PublicClientApplication -import com.microsoft.identity.client.exception.MsalClientException -import com.microsoft.identity.client.exception.MsalException -import com.microsoft.identity.client.internal.CommandParametersAdapter -import com.microsoft.identity.client.statemachine.results.SignOutResult -import com.microsoft.identity.common.internal.commands.AcquireTokenNoFixedScopesCommand -import com.microsoft.identity.common.internal.commands.RemoveCurrentAccountCommand -import com.microsoft.identity.common.internal.controllers.LocalMSALController -import com.microsoft.identity.common.internal.controllers.NativeAuthMsalController -import com.microsoft.identity.common.java.commands.CommandCallback -import com.microsoft.identity.common.java.controllers.CommandDispatcher -import com.microsoft.identity.common.java.controllers.ExceptionAdapter -import com.microsoft.identity.common.java.dto.AccountRecord -import com.microsoft.identity.common.java.eststelemetry.PublicApiId -import com.microsoft.identity.common.java.exception.BaseException -import com.microsoft.identity.common.java.exception.ServiceException -import com.microsoft.identity.common.java.logging.LogSession -import com.microsoft.identity.common.java.logging.Logger -import com.microsoft.identity.common.java.logging.Logger.LogLevel -import com.microsoft.identity.common.java.result.ILocalAuthenticationResult -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.Serializable - sealed interface State /** * BaseState is the base class for various states in the Native Auth state machine. */ abstract class BaseState(internal open val flowToken: String?) - -/** - * AcccountResult returned as part of a successful completion of sign in flow [com.microsoft.identity.client.statemachine.results.SignInResult.Complete]. - */ -class AccountResult private constructor( - private val account: IAccount, - private val config: NativeAuthPublicClientApplicationConfiguration -) : Serializable { - - companion object { - - private val TAG = NativeAuthPublicClientApplication::class.java.simpleName - - fun createFromAuthenticationResult( - authenticationResult: IAuthenticationResult, - config: NativeAuthPublicClientApplicationConfiguration - ): AccountResult { - return AccountResult( - account = authenticationResult.account, - config = config - ) - } - - fun createFromAccountResult( - account: IAccount, - config: NativeAuthPublicClientApplicationConfiguration - ): AccountResult { - return AccountResult( - account = account, - config = config - ) - } - } - - interface SignOutCallback : Callback - - /** - * Remove the current account from the cache; callback variant. - * - * @param callback [com.microsoft.identity.client.statemachine.states.AccountResult.SignOutCallback] to receive the result on. - */ - fun signOut(callback: SignOutCallback) { - LogSession.logMethodCall(TAG, "${TAG}.signOut") - NativeAuthPublicClientApplication.pcaScope.launch { - try { - val result = signOut() - callback.onResult(result) - } catch (e: MsalException) { - Logger.error(TAG, "Exception thrown in signOut", e) - callback.onError(e) - } - } - } - - /** - * Remove the current account from the cache; Kotlin coroutines variant. - */ - suspend fun signOut(): SignOutResult { - return withContext(Dispatchers.IO) { - LogSession.logMethodCall(TAG, "${TAG}.signOut.withContext") - - val account: IAccount = - NativeAuthPublicClientApplication.getCurrentAccountInternal(config) - ?: throw MsalClientException( - MsalClientException.NO_CURRENT_ACCOUNT, - MsalClientException.NO_CURRENT_ACCOUNT_ERROR_MESSAGE - ) - - val requestAccountRecord = AccountRecord() - requestAccountRecord.environment = (account as Account).environment - requestAccountRecord.homeAccountId = account.homeAccountId - - val params = CommandParametersAdapter.createRemoveAccountCommandParameters( - config, - config.oAuth2TokenCache, - requestAccountRecord - ) - - val removeCurrentAccountCommandParameters = RemoveCurrentAccountCommand( - params, - LocalMSALController(), - object : CommandCallback { - override fun onError(error: BaseException?) { - // Do nothing, handled by CommandDispatcher.submitSilentReturningFuture() - } - - override fun onTaskCompleted(result: Boolean?) { - // Do nothing, handled by CommandDispatcher.submitSilentReturningFuture() - } - - override fun onCancel() { - // Do nothing - } - }, - PublicApiId.NATIVE_AUTH_ACCOUNT_SIGN_OUT - ) - - val result = CommandDispatcher.submitSilentReturningFuture(removeCurrentAccountCommandParameters) - .get().result as Boolean - - return@withContext if (result) { - SignOutResult.Complete - } else { - Logger.error( - TAG, - "Unexpected error during signOut.", - null - ) - throw MsalClientException( - MsalClientException.UNKNOWN_ERROR, - "Unexpected error during signOut." - ) - } - } - } - - /** - * Gets the current account. - * - * @return account [com.microsoft.identity.client.IAccount]. - */ - fun getAccount(): IAccount { - return account - } - - /** - * Gets the current account's ID token (if present). - * - * @return idToken [String]. - */ - fun getIdToken(): String? { - return account.idToken - } - - /** - * Gets the claims associated with the current account. - * - * @return A Map of claims. - */ - fun getClaims(): Map? { - return account.claims - } - - interface GetAccessTokenCallback : Callback - - /** - * Retrieves the access token for the currently signed in account from the cache. - * If the access token is expired, it will be attempted to be refreshed using the refresh token that's stored in the cache; - * callback variant. - * - * @return [com.microsoft.identity.client.IAuthenticationResult] If successful. - * @throws [MsalClientException] If the the account doesn't exist in the cache. - * @throws [ServiceException] If the refresh token doesn't exist in the cache/is expired, or the refreshing fails. - */ - fun getAccessToken(forceRefresh: Boolean = false, callback: GetAccessTokenCallback) { - LogSession.logMethodCall(TAG, "${TAG}.getAccessToken") - NativeAuthPublicClientApplication.pcaScope.launch { - try { - val result = getAccessToken(forceRefresh) - callback.onResult(result) - } catch (e: MsalException) { - Logger.error(TAG, "Exception thrown in getAccessToken", e) - callback.onError(e) - } - } - } - - /** - * Retrieves the access token for the currently signed in account from the cache. - * If the access token is expired, it will be attempted to be refreshed using the refresh token that's stored in the cache; - * Kotlin coroutines variant. - * - * @return [com.microsoft.identity.client.IAuthenticationResult] If successful. - * @throws [MsalClientException] If the the account doesn't exist in the cache. - * @throws [ServiceException] If the refresh token doesn't exist in the cache/is expired, or the refreshing fails. - */ - suspend fun getAccessToken(forceRefresh: Boolean = false): IAuthenticationResult? { - LogSession.logMethodCall(TAG, "${TAG}.getAccessToken(forceRefresh: Boolean)") - return withContext(Dispatchers.IO) { - val account = - NativeAuthPublicClientApplication.getCurrentAccountInternal(config) as? Account - ?: throw MsalClientException( - MsalClientException.NO_CURRENT_ACCOUNT, - MsalClientException.NO_CURRENT_ACCOUNT_ERROR_MESSAGE - ) - - val acquireTokenSilentParameters = AcquireTokenSilentParameters.Builder() - .forAccount(account) - .fromAuthority(account.authority) - .build() - - val accountToBeUsed = PublicClientApplication.selectAccountRecordForTokenRequest( - config, - acquireTokenSilentParameters - ) - - val params = CommandParametersAdapter.createAcquireTokenNoFixedScopesCommandParameters( - config, - config.oAuth2TokenCache, - accountToBeUsed, - forceRefresh - ) - - val command = AcquireTokenNoFixedScopesCommand( - params, - NativeAuthMsalController(), - PublicApiId.NATIVE_AUTH_ACCOUNT_GET_ACCESS_TOKEN - ) - - val commandResult = CommandDispatcher.submitSilentReturningFuture(command) - .get().result - - when (commandResult) { - is ServiceException -> { - throw ExceptionAdapter.convertToNativeAuthException(commandResult) - } - is Exception -> { - throw commandResult - } - else -> { - return@withContext AuthenticationResultAdapter.adapt(commandResult as ILocalAuthenticationResult) - } - } - } - } -}