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 016b708a7..4e538edfd 100644 --- a/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt @@ -24,6 +24,7 @@ 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.ResetPasswordStartResult import com.microsoft.identity.client.statemachine.results.SignInResult import com.microsoft.identity.client.statemachine.results.SignInUsingPasswordResult import com.microsoft.identity.client.statemachine.results.SignUpResult @@ -151,4 +152,23 @@ interface INativeAuthPublicClientApplication : IPublicClientApplication { * @throws MsalClientException if an account is already signed in. */ fun signUpUsingPassword(username: String, password: CharArray, attributes: UserAttributes? = null, callback: NativeAuthPublicClientApplication.SignUpUsingPasswordCallback) + + /** + * Reset password for the account starting from a username; Kotlin coroutines variant. + * + * @param username username of the account to reset password. + * @return [com.microsoft.identity.client.statemachine.results.ResetPasswordStartResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + suspend fun resetPassword(username: String): ResetPasswordStartResult + + /** + * Reset password for the account starting from a username; callback variant. + * + * @param username username of the account to reset password. + * @param callback [com.microsoft.identity.client.NativeAuthPublicClientApplication.ResetPasswordCallback] to receive the result. + * @return [com.microsoft.identity.client.statemachine.results.ResetPasswordStartResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + fun resetPassword(username: String, callback: NativeAuthPublicClientApplication.ResetPasswordCallback) } 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 bd618ee10..3ae43a3cd 100644 --- a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt @@ -35,12 +35,15 @@ 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.ResetPasswordResult +import com.microsoft.identity.client.statemachine.results.ResetPasswordStartResult import com.microsoft.identity.client.statemachine.results.SignInResult import com.microsoft.identity.client.statemachine.results.SignInUsingPasswordResult import com.microsoft.identity.client.statemachine.results.SignUpResult import com.microsoft.identity.client.statemachine.results.SignUpUsingPasswordResult import com.microsoft.identity.client.statemachine.states.AccountResult import com.microsoft.identity.client.statemachine.states.Callback +import com.microsoft.identity.client.statemachine.states.ResetPasswordCodeRequiredState import com.microsoft.identity.client.statemachine.states.SignInAfterSignUpState import com.microsoft.identity.client.statemachine.states.SignInCodeRequiredState import com.microsoft.identity.client.statemachine.states.SignInPasswordRequiredState @@ -50,6 +53,7 @@ import com.microsoft.identity.client.statemachine.states.SignUpPasswordRequiredS 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.ResetPasswordStartCommand import com.microsoft.identity.common.internal.commands.SignInStartCommand import com.microsoft.identity.common.internal.commands.SignUpStartCommand import com.microsoft.identity.common.internal.controllers.LocalMSALController @@ -60,6 +64,8 @@ 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.INativeAuthCommandResult +import com.microsoft.identity.common.java.controllers.results.ResetPasswordCommandResult +import com.microsoft.identity.common.java.controllers.results.ResetPasswordStartCommandResult import com.microsoft.identity.common.java.controllers.results.SignInCommandResult import com.microsoft.identity.common.java.controllers.results.SignInStartCommandResult import com.microsoft.identity.common.java.controllers.results.SignUpCommandResult @@ -930,8 +936,136 @@ class NativeAuthPublicClientApplication( } } + interface ResetPasswordCallback : Callback + /** + * Reset password for the account starting from a username; callback variant. + * + * @param username username of the account to reset password. + * @param callback [com.microsoft.identity.client.NativeAuthPublicClientApplication.ResetPasswordCallback] to receive the result. + * @return [com.microsoft.identity.client.statemachine.results.ResetPasswordStartResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + override fun resetPassword(username: String, callback: ResetPasswordCallback) { + LogSession.logMethodCall(TAG, "${TAG}.resetPassword") + pcaScope.launch { + try { + val result = resetPassword(username = username) + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in resetPassword", e) + callback.onError(e) + } + } + } + + /** + * Reset password for the account starting from a username; Kotlin coroutines variant. + * + * @param username username of the account to reset password. + * @return [com.microsoft.identity.client.statemachine.results.ResetPasswordStartResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + override suspend fun resetPassword(username: String): ResetPasswordStartResult { + LogSession.logMethodCall(TAG, "${TAG}.resetPassword(username: String)") + + return withContext(Dispatchers.IO) { + val doesAccountExist = checkForPersistedAccount().get() + if (doesAccountExist) { + throw MsalClientException( + MsalClientException.INVALID_PARAMETER, + "An account is already signed in." + ) + } + + val parameters = CommandParametersAdapter.createResetPasswordStartCommandParameters( + nativeAuthConfig, + nativeAuthConfig.oAuth2TokenCache, + username + ) + + val command = ResetPasswordStartCommand( + parameters, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_RESET_PASSWORD_START + ) + + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is ResetPasswordCommandResult.CodeRequired -> { + ResetPasswordStartResult.CodeRequired( + nextState = ResetPasswordCodeRequiredState( + flowToken = result.passwordResetToken, + config = nativeAuthConfig + ), + codeLength = result.codeLength, + sentTo = result.challengeTargetLabel, + channel = result.challengeChannel + ) + } + + is ResetPasswordCommandResult.UserNotFound -> { + ResetPasswordStartResult.UserNotFound( + error = UserNotFoundError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ) + ) + } + + is INativeAuthCommandResult.UnknownError -> { + ResetPasswordResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + exception = result.exception + ) + ) + } + + is INativeAuthCommandResult.Redirect -> { + ResetPasswordResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + + is ResetPasswordCommandResult.PasswordNotSet -> { + Logger.warn( + TAG, + "Unexpected result $result", + ) + ResetPasswordResult.UnexpectedError( + error = GeneralError( + errorMessage = "Unexpected state", + error = "unexpected_state", + correlationId = result.correlationId + ) + ) + } + is ResetPasswordCommandResult.EmailNotVerified -> { + Logger.warn( + TAG, + "Unexpected result $result" + ) + ResetPasswordResult.UnexpectedError( + error = GeneralError( + errorMessage = "Unexpected state", + error = "unexpected_state", + correlationId = result.correlationId + ) + ) + } + } + } + } + private fun verifyUserIsNotSignedIn() { val doesAccountExist = checkForPersistedAccount().get() if (doesAccountExist) { 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 3bffb010e..48e229783 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 @@ -60,6 +60,10 @@ 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.ResetPasswordResendCodeCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.ResetPasswordStartCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.ResetPasswordSubmitCodeCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.ResetPasswordSubmitNewPasswordCommandParameters; 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; @@ -845,6 +849,155 @@ public static SignInSubmitPasswordCommandParameters createSignInSubmitPasswordCo } + /** + * Creates command parameter for [ResetPasswordStartCommand] of Native Auth. + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param username username associated with password change + * @return Command parameter object + */ + public static ResetPasswordStartCommandParameters createResetPasswordStartCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String username) { + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + final ResetPasswordStartCommandParameters commandParameters = + ResetPasswordStartCommandParameters.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) + .challengeType(configuration.getChallengeTypes()) + .clientId(configuration.getClientId()) + .build(); + + return commandParameters; + } + + /** + * Creates command parameter for [ResetPasswordSubmitCodeCommand] of Native Auth. + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param code out of band code + * @param passwordResetToken password reset token + * @return Command parameter object + */ + public static ResetPasswordSubmitCodeCommandParameters createResetPasswordSubmitCodeCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String code, + @NonNull final String passwordResetToken) { + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + final ResetPasswordSubmitCodeCommandParameters commandParameters = + ResetPasswordSubmitCodeCommandParameters.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) + .code(code) + .challengeType(configuration.getChallengeTypes()) + .passwordResetToken(passwordResetToken) + .clientId(configuration.getClientId()) + .build(); + + return commandParameters; + } + + /** + * Creates command parameter for [ResetPasswordResendCodeCommand] of Native Auth. + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param passwordResetToken password reset token + * @return Command parameter object + */ + public static ResetPasswordResendCodeCommandParameters createResetPasswordResendCodeCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String passwordResetToken) { + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + final ResetPasswordResendCodeCommandParameters commandParameters = + ResetPasswordResendCodeCommandParameters.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()) + .passwordResetToken(passwordResetToken) + .clientId(configuration.getClientId()) + .build(); + + return commandParameters; + } + + /** + * Creates command parameter for [ResetPasswordSubmitNewPasswordCommandParameters] of Native Auth. + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param passwordSubmitToken password submit token + * @return Command parameter object + */ + public static ResetPasswordSubmitNewPasswordCommandParameters createResetPasswordSubmitNewPasswordCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String passwordSubmitToken, + @NonNull final char[] password) { + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + final ResetPasswordSubmitNewPasswordCommandParameters commandParameters = + ResetPasswordSubmitNewPasswordCommandParameters.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) + .passwordSubmitToken(passwordSubmitToken) + .challengeType(configuration.getChallengeTypes()) + .newPassword(password) + .clientId(configuration.getClientId()) + .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/results/ResetPasswordResult.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/results/ResetPasswordResult.kt new file mode 100644 index 000000000..d2aaaa934 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/results/ResetPasswordResult.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.GeneralError +import com.microsoft.identity.client.statemachine.IncorrectCodeError +import com.microsoft.identity.client.statemachine.InvalidPasswordError +import com.microsoft.identity.client.statemachine.UserNotFoundError +import com.microsoft.identity.client.statemachine.states.ResetPasswordCodeRequiredState +import com.microsoft.identity.client.statemachine.states.ResetPasswordPasswordRequiredState + +/** + * Self-service password reset. + */ +sealed interface ResetPasswordResult : Result { + /** + * Complete Result, which indicates the reset password flow completed successfully. + * i.e. the password is successfully reset. + * + * @param resultValue null + */ + object Complete : + Result.CompleteResult(resultValue = null), + ResetPasswordResult, + ResetPasswordSubmitPasswordResult + + /** + * BrowserRequired ErrorResult, which indicates that the server requires more/different authentication mechanisms than the client is configured or able 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), + ResetPasswordResult, + ResetPasswordStartResult, + ResetPasswordSubmitCodeResult, + ResetPasswordSubmitPasswordResult, + ResetPasswordResendCodeResult + + /** + * 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: com.microsoft.identity.client.statemachine.Error) : + Result.ErrorResult(error = error), + ResetPasswordResult, + ResetPasswordStartResult, + ResetPasswordSubmitCodeResult, + ResetPasswordSubmitPasswordResult, + ResetPasswordResendCodeResult +} + +/** + * SSPR start result, produced by + * [com.microsoft.identity.client.INativeAuthPublicClientApplication.resetPassword] + */ +sealed interface ResetPasswordStartResult : Result { + /** + * CodeRequired Result, which indicates a verification code is required from the user to continue. + * + * @param nextState [com.microsoft.identity.client.statemachine.states.ResetPasswordCodeRequiredState] 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: ResetPasswordCodeRequiredState, + val codeLength: Int, + val sentTo: String, + val channel: String, + ) : ResetPasswordStartResult, Result.SuccessResult(nextState = nextState) + + /** + * 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] the current state of the flow with follow-on methods. + */ + class UserNotFound( + override val error: UserNotFoundError + ) : ResetPasswordStartResult, Result.ErrorResult(error = error) +} + +/** + * SSPR submit code result, produced by + * [com.microsoft.identity.client.statemachine.states.ResetPasswordCodeRequiredState.submitCode] + */ +sealed interface ResetPasswordSubmitCodeResult : Result { + /** + * PasswordRequired Result, which indicates that a valid new password is required from the user to continue. + * + * @param nextState [com.microsoft.identity.client.statemachine.states.ResetPasswordPasswordRequiredState] the current state of the flow with follow-on methods. + */ + class PasswordRequired( + override val nextState: ResetPasswordPasswordRequiredState + ) : ResetPasswordSubmitCodeResult, Result.SuccessResult(nextState = nextState) + + /** + * 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 + ) : ResetPasswordSubmitCodeResult, Result.ErrorResult(error = error) +} + +/** + * Sign in resend code result, produced by + * [com.microsoft.identity.client.statemachine.states.ResetPasswordCodeRequiredState.resendCode] + */ +sealed interface ResetPasswordResendCodeResult : Result { + /** + * Success Result, which indicates a new verification code was successfully resent. + * + * @param nextState [com.microsoft.identity.client.statemachine.states.ResetPasswordCodeRequiredState] 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 channel(email/phone) the code was sent through. + */ + class Success( + override val nextState: ResetPasswordCodeRequiredState, + val codeLength: Int, + val sentTo: String, + val channel: String, + ) : ResetPasswordResendCodeResult, Result.SuccessResult(nextState = nextState) +} + +/** + * SSPR submit password result, produced by + * [com.microsoft.identity.client.statemachine.states.ResetPasswordPasswordRequiredState.submitPassword] + */ +sealed interface ResetPasswordSubmitPasswordResult : Result { + /** + * InvalidPassword ErrorResult, which indicates the new password provided by the user is not acceptable to the server. + * The password should be re-submitted. + * + * @param error [com.microsoft.identity.client.statemachine.InvalidPasswordError] + */ + class InvalidPassword( + override val error: InvalidPasswordError + ) : ResetPasswordSubmitPasswordResult, Result.ErrorResult(error = error) + /** + * PasswordResetFailed ErrorResult, which indicates the password reset flow failed. + * + * @param error [com.microsoft.identity.client.statemachine.GeneralError] + */ + class PasswordResetFailed( + override val error: GeneralError + ) : ResetPasswordSubmitPasswordResult, Result.ErrorResult(error = error) +} diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/ResetPasswordStates.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/ResetPasswordStates.kt new file mode 100644 index 000000000..248632556 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/ResetPasswordStates.kt @@ -0,0 +1,374 @@ +// 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.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.InvalidPasswordError +import com.microsoft.identity.client.statemachine.results.ResetPasswordResendCodeResult +import com.microsoft.identity.client.statemachine.results.ResetPasswordResult +import com.microsoft.identity.client.statemachine.results.ResetPasswordSubmitCodeResult +import com.microsoft.identity.client.statemachine.results.ResetPasswordSubmitPasswordResult +import com.microsoft.identity.common.internal.commands.ResetPasswordResendCodeCommand +import com.microsoft.identity.common.internal.commands.ResetPasswordSubmitCodeCommand +import com.microsoft.identity.common.internal.commands.ResetPasswordSubmitNewPasswordCommand +import com.microsoft.identity.common.internal.controllers.NativeAuthMsalController +import com.microsoft.identity.common.java.controllers.CommandDispatcher +import com.microsoft.identity.common.java.controllers.results.INativeAuthCommandResult +import com.microsoft.identity.common.java.controllers.results.ResetPasswordCommandResult +import com.microsoft.identity.common.java.controllers.results.ResetPasswordResendCodeCommandResult +import com.microsoft.identity.common.java.controllers.results.ResetPasswordSubmitCodeCommandResult +import com.microsoft.identity.common.java.controllers.results.ResetPasswordSubmitNewPasswordCommandResult +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.util.StringUtil +import com.microsoft.identity.common.java.util.checkAndWrapCommandResultType +import kotlinx.coroutines.Dispatchers +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. + * ResetPasswordCodeRequiredState class represents a state where the user has to provide a code to progress + * in the reset password flow. + * @property flowToken: Flow token to be passed in the next request + * @property config Configuration used by Native Auth + */ +class ResetPasswordCodeRequiredState internal constructor( + override val flowToken: String, + private val config: NativeAuthPublicClientApplicationConfiguration +) : BaseState(flowToken), State, Serializable { + private val TAG: String = ResetPasswordCodeRequiredState::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.ResetPasswordPasswordRequiredState.SubmitPasswordCallback] 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 = 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): ResetPasswordSubmitCodeResult { + LogSession.logMethodCall(TAG, "${TAG}.submitCode(code: String)") + return withContext(Dispatchers.IO) { + val parameters = + CommandParametersAdapter.createResetPasswordSubmitCodeCommandParameters( + config, + config.oAuth2TokenCache, + code, + flowToken + ) + + val command = ResetPasswordSubmitCodeCommand( + parameters = parameters, + controller = NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_RESET_PASSWORD_SUBMIT_CODE + ) + + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is ResetPasswordCommandResult.PasswordRequired -> { + ResetPasswordSubmitCodeResult.PasswordRequired( + nextState = ResetPasswordPasswordRequiredState( + flowToken = result.passwordSubmitToken, + config = config + ) + ) + } + + is ResetPasswordCommandResult.IncorrectCode -> { + ResetPasswordSubmitCodeResult.CodeIncorrect( + error = IncorrectCodeError( + error = result.error, + errorMessage = result.errorDescription, + correlationId = result.correlationId + ) + ) + } + + is INativeAuthCommandResult.Redirect -> { + ResetPasswordResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + + is INativeAuthCommandResult.UnknownError -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + ResetPasswordResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + exception = result.exception + ) + ) + } + } + } + } + + /** + * ResendCodeCallback receives the result for submit code for Reset Password for Native Auth + */ + interface ResendCodeCallback : Callback + + /** + * Resends a new verification code to the user; callback variant. + * + * @param callback [com.microsoft.identity.client.statemachine.states.ResetPasswordCodeRequiredState.ResendCodeCallback] to receive the result on. + * @return The results of the resend code action. + */ + fun resendCode(callback: ResendCodeCallback) { + LogSession.logMethodCall(TAG, "${TAG}.resendCode(callback: ResendCodeCallback)") + 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(): ResetPasswordResendCodeResult { + LogSession.logMethodCall(TAG, "${TAG}.resendCode") + return withContext(Dispatchers.IO) { + val parameters = + CommandParametersAdapter.createResetPasswordResendCodeCommandParameters( + config, + config.oAuth2TokenCache, + flowToken + ) + + val command = ResetPasswordResendCodeCommand( + parameters = parameters, + controller = NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_RESET_PASSWORD_RESEND_CODE + ) + + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is ResetPasswordCommandResult.CodeRequired -> { + ResetPasswordResendCodeResult.Success( + nextState = ResetPasswordCodeRequiredState( + flowToken = result.passwordResetToken, + config = config + ), + codeLength = result.codeLength, + sentTo = result.challengeTargetLabel, + channel = result.challengeChannel + ) + } + + is INativeAuthCommandResult.Redirect -> { + ResetPasswordResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + + is INativeAuthCommandResult.UnknownError -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + ResetPasswordResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + exception = result.exception + ) + ) + } + } + } + } +} + +/** + * Native Auth uses a state machine to denote state and transitions for a user. + * ResetPasswordPasswordRequiredState class represents a state where the user has to provide a password to progress + * in the reset password flow. + * @property flowToken: Flow token to be passed in the next request + * @property config Configuration used by Native Auth + */ +class ResetPasswordPasswordRequiredState internal constructor( + override val flowToken: String, + private val config: NativeAuthPublicClientApplicationConfiguration +) : BaseState(flowToken), State, Serializable { + private val TAG: String = ResetPasswordPasswordRequiredState::class.java.simpleName + + interface SubmitPasswordCallback : Callback + + /** + * Submits a new password to the server; callback variant. + * + * @param password The password to submit. + * @param callback [com.microsoft.identity.client.statemachine.states.ResetPasswordPasswordRequiredState.SubmitPasswordCallback] to receive the result on. + * @return The results of the submit password action. + */ + fun submitPassword(password: CharArray, callback: SubmitPasswordCallback) { + LogSession.logMethodCall(TAG, "${TAG}.submitPassword") + NativeAuthPublicClientApplication.pcaScope.launch { + try { + val result = submitPassword(password = password) + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in submitPassword", e) + callback.onError(e) + } + } + } + + /** + * Submits a new password to the server; Kotlin coroutines variant. + * + * @param password The password to submit. + * @return The results of the submit password action. + */ + suspend fun submitPassword(password: CharArray): ResetPasswordSubmitPasswordResult { + LogSession.logMethodCall(TAG, "${TAG}.submitPassword(password: String)") + return withContext(Dispatchers.IO) { + val parameters = + CommandParametersAdapter.createResetPasswordSubmitNewPasswordCommandParameters( + config, + config.oAuth2TokenCache, + flowToken, + password + ) + + val command = ResetPasswordSubmitNewPasswordCommand( + parameters = parameters, + controller = NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_RESET_PASSWORD_SUBMIT_NEW_PASSWORD + ) + + try { + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = + rawCommandResult.checkAndWrapCommandResultType()) { + is ResetPasswordCommandResult.Complete -> { + ResetPasswordResult.Complete + } + + is ResetPasswordCommandResult.PasswordNotAccepted -> { + ResetPasswordSubmitPasswordResult.InvalidPassword( + error = InvalidPasswordError( + error = result.error, + errorMessage = result.errorDescription, + correlationId = result.correlationId + ) + ) + } + + is ResetPasswordCommandResult.PasswordResetFailed -> { + ResetPasswordSubmitPasswordResult.PasswordResetFailed( + error = GeneralError( + error = result.error, + errorMessage = result.errorDescription, + correlationId = result.correlationId + ) + ) + } + + is ResetPasswordCommandResult.UserNotFound -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + ResetPasswordResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ) + ) + } + + is INativeAuthCommandResult.UnknownError -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + ResetPasswordResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + exception = result.exception + ) + ) + } + } + } finally { + StringUtil.overwriteWithNull(parameters.newPassword) + } + } + } +}