diff --git a/common b/common index 76bf90d71..fd6a266a9 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 76bf90d71fab2c5fb2f2f007f269647bd245605b +Subproject commit fd6a266a92cdf22cbf222927c3f2afc60e3164ed 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 b03843bac..016b708a7 100644 --- a/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/INativeAuthPublicClientApplication.kt @@ -26,6 +26,8 @@ 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 +import com.microsoft.identity.client.statemachine.results.SignUpResult +import com.microsoft.identity.client.statemachine.results.SignUpUsingPasswordResult import com.microsoft.identity.client.statemachine.states.AccountResult /** @@ -105,4 +107,48 @@ interface INativeAuthPublicClientApplication : IPublicClientApplication { * @throws MsalClientException if an account is already signed in. */ fun signInUsingPassword(username: String, password: CharArray, scopes: List? = null, callback: NativeAuthPublicClientApplication.SignInUsingPasswordCallback) + + /** + * Sign up the account starting from a username; Kotlin coroutines variant. + * + * @param username username of the account to sign up. + * @param attributes (Optional) user attributes to be used during account creation. + * @return [com.microsoft.identity.client.statemachine.results.SignUpResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + suspend fun signUp(username: String, attributes: UserAttributes? = null): SignUpResult + + /** + * Sign up the account starting from a username; callback variant. + * + * @param username username of the account to sign up. + * @param attributes (Optional) user attributes to be used during account creation. + * @param callback [com.microsoft.identity.client.NativeAuthPublicClientApplication.SignUpCallback] to receive the result. + * @return [com.microsoft.identity.client.statemachine.results.SignUpResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + fun signUp(username: String, attributes: UserAttributes? = null, callback: NativeAuthPublicClientApplication.SignUpCallback) + + /** + * Sign up the account using username and password. Kotlin coroutines variant. + * + * @param username username of the account to sign up. + * @param password password of the account to sign up. + * @param attributes (Optional) user attributes to be used during account creation + * @return [com.microsoft.identity.client.statemachine.results.SignUpUsingPasswordResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + suspend fun signUpUsingPassword(username: String, password: CharArray, attributes: UserAttributes? = null): SignUpUsingPasswordResult + + /** + * Sign up the account using username and password; callback variant. + * + * @param username username of the account to sign up. + * @param password password of the account to sign up. + * @param attributes (Optional) user attributes to be used during account creation + * @param callback [com.microsoft.identity.client.NativeAuthPublicClientApplication.SignUpUsingPasswordCallback] to receive the result. + * @return [com.microsoft.identity.client.statemachine.results.SignUpUsingPasswordResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + fun signUpUsingPassword(username: String, password: CharArray, attributes: UserAttributes? = null, callback: NativeAuthPublicClientApplication.SignUpUsingPasswordCallback) } 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 3fc1d3e0c..bd618ee10 100644 --- a/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt +++ b/msal/src/main/java/com/microsoft/identity/client/NativeAuthPublicClientApplication.kt @@ -29,18 +29,29 @@ 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 +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.SignInAfterSignUpState import com.microsoft.identity.client.statemachine.states.SignInCodeRequiredState import com.microsoft.identity.client.statemachine.states.SignInPasswordRequiredState +import com.microsoft.identity.client.statemachine.states.SignUpAttributesRequiredState +import com.microsoft.identity.client.statemachine.states.SignUpCodeRequiredState +import com.microsoft.identity.client.statemachine.states.SignUpPasswordRequiredState 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.commands.SignUpStartCommand 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 @@ -51,6 +62,8 @@ 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.SignInCommandResult import com.microsoft.identity.common.java.controllers.results.SignInStartCommandResult +import com.microsoft.identity.common.java.controllers.results.SignUpCommandResult +import com.microsoft.identity.common.java.controllers.results.SignUpStartCommandResult import com.microsoft.identity.common.java.eststelemetry.PublicApiId import com.microsoft.identity.common.java.exception.BaseException import com.microsoft.identity.common.java.logging.LogSession @@ -421,8 +434,7 @@ class NativeAuthPublicClientApplication( scopes ) - try - { + try { val command = SignInStartCommand( params, NativeAuthMsalController(), @@ -524,6 +536,402 @@ class NativeAuthPublicClientApplication( } } + interface SignUpUsingPasswordCallback : Callback + + /** + * Sign up the account using username and password; callback variant. + * + * @param username username of the account to sign up. + * @param password password of the account to sign up. + * @param attributes (Optional) user attributes to be used during account creation + * @param callback [com.microsoft.identity.client.NativeAuthPublicClientApplication.SignUpUsingPasswordCallback] to receive the result. + * @return [com.microsoft.identity.client.statemachine.results.SignUpUsingPasswordResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + override fun signUpUsingPassword( + username: String, + password: CharArray, + attributes: UserAttributes?, + callback: SignUpUsingPasswordCallback + ) { + LogSession.logMethodCall(TAG, "${TAG}.signUpUsingPassword") + pcaScope.launch { + try { + val result = signUpUsingPassword(username, password, attributes) + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in signUpUsingPassword", e) + callback.onError(e) + } + } + } + + /** + * Sign up the account using username and password. Kotlin coroutines variant. + * + * @param username username of the account to sign up. + * @param password password of the account to sign up. + * @param attributes (Optional) user attributes to be used during account creation + * @return [com.microsoft.identity.client.statemachine.results.SignUpUsingPasswordResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + override suspend fun signUpUsingPassword( + username: String, + password: CharArray, + attributes: UserAttributes? + ): SignUpUsingPasswordResult { + LogSession.logMethodCall(TAG, "${TAG}.signUpUsingPassword(username: String, password: String, attributes: UserAttributes?)") + + return withContext(Dispatchers.IO) { + val doesAccountExist = checkForPersistedAccount().get() + if (doesAccountExist) { + throw MsalClientException( + MsalClientException.INVALID_PARAMETER, + "An account is already signed in." + ) + } + + val parameters = + CommandParametersAdapter.createSignUpStartUsingPasswordCommandParameters( + nativeAuthConfig, + nativeAuthConfig.oAuth2TokenCache, + username, + password, + attributes?.toMap() + ) + + val command = SignUpStartCommand( + parameters, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_SIGN_UP_START_WITH_PASSWORD + ) + + try { + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = + rawCommandResult.checkAndWrapCommandResultType()) { + is SignUpCommandResult.AuthNotSupported -> { + SignUpUsingPasswordResult.AuthNotSupported( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ) + ) + } + + is SignUpCommandResult.InvalidPassword -> { + SignUpResult.InvalidPassword( + InvalidPasswordError( + error = result.error, + errorMessage = result.errorDescription, + correlationId = result.correlationId + ) + ) + } + + is SignUpCommandResult.AttributesRequired -> { + SignUpResult.AttributesRequired( + nextState = SignUpAttributesRequiredState( + flowToken = result.signupToken, + username = username, + config = nativeAuthConfig + ), + requiredAttributes = result.requiredAttributes.toListOfRequiredUserAttribute() + ) + } + + is SignUpCommandResult.CodeRequired -> { + SignUpResult.CodeRequired( + nextState = SignUpCodeRequiredState( + flowToken = result.signupToken, + username = username, + config = nativeAuthConfig + ), + codeLength = result.codeLength, + sentTo = result.challengeTargetLabel, + channel = result.challengeChannel, + ) + } + + is SignUpCommandResult.UsernameAlreadyExists -> { + SignUpResult.UserAlreadyExists( + error = UserAlreadyExistsError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ) + ) + } + + is SignUpCommandResult.InvalidEmail -> { + SignUpResult.InvalidEmail( + error = InvalidEmailError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ) + ) + } + + is SignUpCommandResult.InvalidAttributes -> { + SignUpResult.InvalidAttributes( + error = InvalidAttributesError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ), + invalidAttributes = result.invalidAttributes + ) + } + + is SignUpCommandResult.Complete -> { + SignUpResult.Complete( + nextState = SignInAfterSignUpState( + signInVerificationCode = result.signInSLT, + username = username, + config = nativeAuthConfig + ) + ) + } + + is INativeAuthCommandResult.Redirect -> { + SignUpResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + + is INativeAuthCommandResult.UnknownError -> { + SignUpResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + exception = result.exception + ) + ) + } + + is SignUpCommandResult.PasswordRequired -> { + Logger.warn( + TAG, + "Unexpected result $result" + ) + SignUpResult.UnexpectedError( + error = GeneralError( + errorMessage = "Unexpected state", + error = "unexpected_state", + correlationId = "UNSET" + ) + ) + } + } + } finally { + StringUtil.overwriteWithNull(parameters.password) + } + } + } + + interface SignUpCallback : Callback + + /** + * Sign up the account starting from a username; callback variant. + * + * @param username username of the account to sign up. + * @param attributes (Optional) user attributes to be used during account creation. + * @param callback [com.microsoft.identity.client.NativeAuthPublicClientApplication.SignUpCallback] to receive the result. + * @return [com.microsoft.identity.client.statemachine.results.SignUpResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + override fun signUp( + username: String, + attributes: UserAttributes?, + callback: SignUpCallback + ) { + LogSession.logMethodCall(TAG, "${TAG}.signUp") + + pcaScope.launch { + try { + val result = signUp(username, attributes) + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in signUp", e) + callback.onError(e) + } + } + } + + /** + * Sign up the account starting from a username; Kotlin coroutines variant. + * + * @param username username of the account to sign up. + * @param attributes (Optional) user attributes to be used during account creation. + * @return [com.microsoft.identity.client.statemachine.results.SignUpResult] see detailed possible return state under the object. + * @throws MsalClientException if an account is already signed in. + */ + override suspend fun signUp( + username: String, + attributes: UserAttributes? + ): SignUpResult { + LogSession.logMethodCall(TAG, "${TAG}.signUp(username: String, attributes: UserAttributes?)") + + return withContext(Dispatchers.IO) { + val doesAccountExist = checkForPersistedAccount().get() + if (doesAccountExist) { + throw MsalClientException( + MsalClientException.INVALID_PARAMETER, + "An account is already signed in." + ) + } + + val parameters = + CommandParametersAdapter.createSignUpStartCommandParameters( + nativeAuthConfig, + nativeAuthConfig.oAuth2TokenCache, + username, + attributes?.userAttributes + ) + + val command = SignUpStartCommand( + parameters, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_SIGN_UP_START + ) + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is SignUpCommandResult.AttributesRequired -> { + SignUpResult.AttributesRequired( + nextState = SignUpAttributesRequiredState( + flowToken = result.signupToken, + username = username, + config = nativeAuthConfig + ), + requiredAttributes = result.requiredAttributes.toListOfRequiredUserAttribute() + ) + } + + is SignUpCommandResult.CodeRequired -> { + SignUpResult.CodeRequired( + nextState = SignUpCodeRequiredState( + flowToken = result.signupToken, + username = username, + config = nativeAuthConfig + ), + codeLength = result.codeLength, + sentTo = result.challengeTargetLabel, + channel = result.challengeChannel, + ) + } + + is SignUpCommandResult.UsernameAlreadyExists -> { + SignUpResult.UserAlreadyExists( + error = UserAlreadyExistsError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ) + ) + } + + is SignUpCommandResult.InvalidEmail -> { + SignUpResult.InvalidEmail( + error = InvalidEmailError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ) + ) + } + + is SignUpCommandResult.InvalidAttributes -> { + SignUpResult.InvalidAttributes( + error = InvalidAttributesError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ), + invalidAttributes = result.invalidAttributes + ) + } + + is SignUpCommandResult.PasswordRequired -> { + SignUpResult.PasswordRequired( + nextState = SignUpPasswordRequiredState( + flowToken = result.signupToken, + username = username, + config = nativeAuthConfig + ) + ) + } + + is SignUpCommandResult.Complete -> { + SignUpResult.Complete( + nextState = SignInAfterSignUpState( + signInVerificationCode = result.signInSLT, + username = username, + config = nativeAuthConfig + ) + ) + } + + is INativeAuthCommandResult.Redirect -> { + SignUpResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + + is INativeAuthCommandResult.UnknownError -> { + SignUpResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + exception = result.exception + ) + ) + } + + is SignUpCommandResult.InvalidPassword -> { + Logger.warn( + TAG, + "Unexpected result $result" + ) + SignUpResult.UnexpectedError( + error = GeneralError( + errorMessage = "Unexpected state", + error = "unexpected_state", + correlationId = result.correlationId + ) + ) + } + + is SignUpCommandResult.AuthNotSupported -> { + Logger.warn( + TAG, + "Unexpected result $result" + ) + SignUpResult.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/RequiredUserAttribute.kt b/msal/src/main/java/com/microsoft/identity/client/RequiredUserAttribute.kt new file mode 100644 index 000000000..becb07378 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/RequiredUserAttribute.kt @@ -0,0 +1,63 @@ +// 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 + +import com.microsoft.identity.common.java.providers.nativeauth.responses.UserAttributeApiResult + +/** + * RequiredUserAttribute represents details about the account attributes required by the server. + */ +data class RequiredUserAttribute( + //Name of the attribute + val attributeName: String?, + + //Data type for the attribute + val type: String?, + + //If the attribute is required + val required: Boolean?, + + //Attribute value should match the constraints + val options: RequiredUserAttributeOptions? +) + +/** + * Converts a list of required user attribute API received as part of signup API to + * a list of [RequiredUserAttribute] object + */ +internal fun List.toListOfRequiredUserAttribute(): List { + return this.map { it.toRequiredUserAttribute() } +} + +/** + * Converts the required user attribute API received as part of signup API to + * [RequiredUserAttribute] object + */ +internal fun UserAttributeApiResult.toRequiredUserAttribute(): RequiredUserAttribute { + return RequiredUserAttribute( + attributeName = this.name, + type = this.type, + required = this.required, + options = this.options?.toListOfRequiredUserAttributeOptions() + ) +} diff --git a/msal/src/main/java/com/microsoft/identity/client/RequiredUserAttributeOptions.kt b/msal/src/main/java/com/microsoft/identity/client/RequiredUserAttributeOptions.kt new file mode 100644 index 000000000..41e7fb6bf --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/RequiredUserAttributeOptions.kt @@ -0,0 +1,42 @@ +// 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 + +import com.microsoft.identity.common.java.providers.nativeauth.responses.UserAttributeOptionsApiResult + +/** + * RequiredUserAttributeOptions contains the regular expression that the attribute value must match. + */ +data class RequiredUserAttributeOptions( + val regex: String? +) + +internal fun List.toListOfRequiredUserAttributeOptions(): List { + return this.map { it.toListOfRequiredUserAttributeOptions() } +} + +fun UserAttributeOptionsApiResult.toListOfRequiredUserAttributeOptions(): RequiredUserAttributeOptions { + return RequiredUserAttributeOptions( + regex = regex + ) +} diff --git a/msal/src/main/java/com/microsoft/identity/client/UserAttributes.kt b/msal/src/main/java/com/microsoft/identity/client/UserAttributes.kt new file mode 100644 index 000000000..cbb241043 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/UserAttributes.kt @@ -0,0 +1,154 @@ +// 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 + +import com.microsoft.identity.common.java.util.ObjectMapper + +/** + * UserAttributes is a helper class for a client to provide user attributes required for signup + * operation in Native Auth + */ +class UserAttributes(internal val userAttributes: Map) { + companion object Builder { + private const val CITY = "city" + private const val COUNTRY = "country" + private const val DISPLAY_NAME = "displayName" + private const val EMAIL_ADDRESS = "email" + private const val GIVEN_NAME = "givenName" + private const val JOB_TITLE = "jobTitle" + private const val POSTAL_CODE = "postalCode" + private const val STATE = "state" + private const val STREET_ADDRESS = "streetAddress" + private const val SURNAME = "surname" + + private val userAttributes = mutableMapOf() + + /** + * Sets the city for user + * @param city: City for user + */ + fun city(city: String): Builder { + userAttributes[CITY] = city + return this + } + + /** + * Sets the country for the user + * @param country: Country for the user + */ + fun country(country: String): Builder { + userAttributes[COUNTRY] = country + return this + } + + /** + * Sets the name for display purposes for the user + * @param displayName: Display name for the user + */ + fun displayName(displayName: String): Builder { + userAttributes[DISPLAY_NAME] = displayName + return this + } + + /** + * Sets the email address for the user + * @param emailAddress: Email address for the user + */ + fun emailAddress(emailAddress: String): Builder { + userAttributes[EMAIL_ADDRESS] = emailAddress + return this + } + + /** + * Sets the given name for the user + * @param givenName: Given name for the user + */ + fun givenName(givenName: String): Builder { + userAttributes[GIVEN_NAME] = givenName + return this + } + + /** + * Sets the job title for the user + * @param givenName: Given name for the user + */ + fun jobTitle(jobTitle: String): Builder { + userAttributes[JOB_TITLE] = jobTitle + return this + } + + /** + * Sets the given name for the user + * @param givenName: Given name for the user + */ + fun postalCode(postalCode: String): Builder { + userAttributes[POSTAL_CODE] = postalCode + return this + } + + /** + * Sets the state/province for the user + * @param state: State/province for the user + */ + fun state(state: String): Builder { + userAttributes[STATE] = state + return this + } + + /** + * Sets the street address for the user + * @param streetAddress: Street address for the user + */ + fun streetAddress(streetAddress: String): Builder { + userAttributes[STREET_ADDRESS] = streetAddress + return this + } + + /** + * Sets the surname for the user + * @param surname: Surname for the user + */ + fun surname(surname: String): Builder { + userAttributes[SURNAME] = surname + return this + } + + /** + * Sets any custom attribute for the use + * @param key: Name of the attribute + * @param value: Attribute value + */ + fun customAttribute(key: String, value: String): Builder { + userAttributes[key] = value + return this + } + + fun build(): UserAttributes { + return UserAttributes(userAttributes) + } + } +} + +internal fun UserAttributes.toMap(): Map { + return ObjectMapper.constructMapFromObject(userAttributes) +} 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 1da0f82ac..3d9475748 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 @@ -51,6 +51,7 @@ 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.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; @@ -65,13 +66,19 @@ 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.commands.parameters.nativeauth.SignUpResendCodeCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignUpStartCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignUpStartUsingPasswordCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignUpSubmitCodeCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignUpSubmitPasswordCommandParameters; +import com.microsoft.identity.common.java.commands.parameters.nativeauth.SignUpSubmitUserAttributesCommandParameters; 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.java.util.SchemaUtil; +import com.microsoft.identity.common.internal.util.StringUtil; import com.microsoft.identity.common.logging.Logger; import java.util.AbstractMap; @@ -358,7 +365,221 @@ public static DeviceCodeFlowCommandParameters createDeviceCodeFlowCommandParamet } /** - * Creates command parameter for [SignInStartCommand] of Native Auth. + * Creates command parameter for [SignUpStartCommand] 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 SignUpStartCommandParameters createSignUpStartCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String username, + final Map userAttributes) { + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + return SignUpStartCommandParameters.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()) + .userAttributes(userAttributes) + .build(); + } + + /** + * Creates command parameter for [SignUpStartCommand] of Native Auth when password is provided + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param username email address of the user + * @param password password of the user + * @return Command parameter object + * @throws ClientException + */ + public static SignUpStartUsingPasswordCommandParameters createSignUpStartUsingPasswordCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String username, + @NonNull final char[] password, + final Map userAttributes) { + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + return SignUpStartUsingPasswordCommandParameters.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) + .challengeType(configuration.getChallengeTypes()) + .userAttributes(userAttributes) + .build(); + } + + /** + * Creates command parameter for [{@link com.microsoft.identity.common.internal.commands.SignUpSubmitCodeCommand}] of Native Auth. + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param code Out of band code + * @param signupToken Signup token received from the start command + * @return Command parameter object + * @throws ClientException + */ + public static SignUpSubmitCodeCommandParameters createSignUpSubmitCodeCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String code, + @NonNull final String signupToken) { + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + return SignUpSubmitCodeCommandParameters.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()) + .signupToken(signupToken) + .code(code) + .build(); + } + + /** + * Creates command parameter for [{@link com.microsoft.identity.common.internal.commands.SignUpResendCodeCommand}] of Native Auth. + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param signupToken Signup token received from the start command + * @return Command parameter object + * @throws ClientException + */ + public static SignUpResendCodeCommandParameters createSignUpResendCodeCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String signupToken) { + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + return SignUpResendCodeCommandParameters.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()) + .challengeType(configuration.getChallengeTypes()) + .authority(authority) + .signupToken(signupToken) + .build(); + } + + /** + * Creates command parameter for [{@link com.microsoft.identity.common.internal.commands.SignUpSubmitUserAttributesCommand}] of Native Auth. + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param signupToken Signup token received from the start command + * @return Command parameter object + * @throws ClientException + */ + public static SignUpSubmitUserAttributesCommandParameters createSignUpStarSubmitUserAttributesCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String signupToken, + final Map userAttributes) { + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + return SignUpSubmitUserAttributesCommandParameters.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) + .clientId(configuration.getClientId()) + .challengeType(configuration.getChallengeTypes()) + .signupToken(signupToken) + .userAttributes(userAttributes) + .build(); + } + + /** + * Creates command parameter for [{@link com.microsoft.identity.common.internal.commands.SignUpSubmitPasswordCommand}] of Native Auth. + * @param configuration PCA configuration + * @param tokenCache token cache for storing results + * @param signupToken email address of the user + * @param password password for the user + * @return Command parameter object + * @throws ClientException + */ + public static SignUpSubmitPasswordCommandParameters createSignUpSubmitPasswordCommandParameters( + @NonNull final NativeAuthPublicClientApplicationConfiguration configuration, + @NonNull final OAuth2TokenCache tokenCache, + @NonNull final String signupToken, + @NonNull final char[] password) { + + final NativeAuthCIAMAuthority authority = ((NativeAuthCIAMAuthority) configuration.getDefaultAuthority()); + + return SignUpSubmitPasswordCommandParameters.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()) + .signupToken(signupToken) + .password(password) + .build(); + } + + /** + * Creates command parameter for [{@link com.microsoft.identity.common.internal.commands.SignInStartCommand}] of Native Auth. * @param configuration PCA configuration * @param tokenCache token cache for storing results * @param username email address of the user @@ -399,7 +620,7 @@ public static SignInStartCommandParameters createSignInStartCommandParameters( } /** - * Creates command parameter for [SignInStartCommand] of Native Auth using username and password + * Creates command parameter for [{@link com.microsoft.identity.common.internal.commands.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 @@ -446,7 +667,7 @@ public static SignInStartUsingPasswordCommandParameters createSignInStartUsingPa } /** - * Creates command parameter for [SignInStartCommand] of Native Auth using short lived token + * Creates command parameter for [{@link com.microsoft.identity.common.internal.commands.SignInStartCommand}] of Native Auth using short lived token * @param configuration PCA configuration * @param tokenCache token cache for storing results * @param signInSLT short lived token @@ -492,7 +713,7 @@ public static SignInWithSLTCommandParameters createSignInWithSLTCommandParameter } /** - * Creates command parameter for [SignInSubmitCodeCommand] of Native Auth + * Creates command parameter for [{@link com.microsoft.identity.common.internal.commands.SignInSubmitCodeCommand}] of Native Auth * @param configuration PCA configuration * @param tokenCache token cache for storing results * @param code Out of band code @@ -539,7 +760,7 @@ public static SignInSubmitCodeCommandParameters createSignInSubmitCodeCommandPar } /** - * Creates command parameter for [SignInResendCodeCommand] of Native Auth + * Creates command parameter for [{@link com.microsoft.identity.common.internal.commands.SignInResendCodeCommand}] of Native Auth * @param configuration PCA configuration * @param tokenCache token cache for storing results * @param credentialToken credential token @@ -575,7 +796,7 @@ public static SignInResendCodeCommandParameters createSignInResendCodeCommandPar } /** - * Creates command parameter for [SignInSubmitPasswordCommand] of Native Auth + * Creates command parameter for [{@link com.microsoft.identity.common.internal.commands.SignInSubmitPasswordCommand}] of Native Auth * @param configuration PCA configuration * @param tokenCache token cache for storing results * @param credentialToken credential token diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/results/SignUpResult.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/results/SignUpResult.kt new file mode 100644 index 000000000..485442d78 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/results/SignUpResult.kt @@ -0,0 +1,233 @@ +// 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.RequiredUserAttribute +import com.microsoft.identity.client.statemachine.BrowserRequiredError +import com.microsoft.identity.client.statemachine.Error +import com.microsoft.identity.client.statemachine.GeneralError +import com.microsoft.identity.client.statemachine.IncorrectCodeError +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.UserAlreadyExistsError +import com.microsoft.identity.client.statemachine.states.SignInAfterSignUpState +import com.microsoft.identity.client.statemachine.states.SignUpAttributesRequiredState +import com.microsoft.identity.client.statemachine.states.SignUpCodeRequiredState +import com.microsoft.identity.client.statemachine.states.SignUpPasswordRequiredState + +/** + * Sign up result, produced by + * [com.microsoft.identity.client.INativeAuthPublicClientApplication.signUp] + */ +sealed interface SignUpResult : Result { + /** + * CompleteResult which indicates the sign up flow is complete, + * i.e. the user account is created and can now be used to sign in with. + * + * @param nextState [com.microsoft.identity.client.statemachine.states.SignInAfterSignUpState] the current state of the flow with follow-on methods. + */ + class Complete( + override val nextState: SignInAfterSignUpState + ) : Result.CompleteWithNextStateResult(resultValue = null, nextState = nextState), + SignUpResult, + SignUpSubmitCodeResult, + SignUpSubmitPasswordResult, + SignUpSubmitAttributesResult, + SignUpUsingPasswordResult + + /** + * 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 error [com.microsoft.identity.client.statemachine.BrowserRequiredError]. + */ + class BrowserRequired(override val error: BrowserRequiredError) : + Result.ErrorResult(error = error), + SignUpResult, + SignUpSubmitCodeResult, + SignUpSubmitPasswordResult, + SignUpSubmitAttributesResult, + SignUpResendCodeResult, + SignUpUsingPasswordResult + + /** + * 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.GeneralError]. + */ + class UnexpectedError(override val error: Error) : + Result.ErrorResult(error = error), + SignUpResult, + SignUpSubmitCodeResult, + SignUpSubmitPasswordResult, + SignUpSubmitAttributesResult, + SignUpResendCodeResult, + SignUpUsingPasswordResult + + /** + * CodeRequired Result, which indicates a verification code is required from the user to continue. + * + * @param nextState [com.microsoft.identity.client.statemachine.states.SignUpCodeRequiredState] 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: SignUpCodeRequiredState, + val codeLength: Int, + val sentTo: String, + val channel: String + ) : SignUpResult, Result.SuccessResult(nextState = nextState), SignUpUsingPasswordResult + + /** + * AttributesRequired Result, which indicates the server requires one or more attributes to be sent, before the account can be created. + * + * @param nextState [com.microsoft.identity.client.statemachine.states.SignUpAttributesRequiredState] the current state of the flow with follow-on methods. + * @param requiredAttributes a list of [com.microsoft.identity.common.java.providers.nativeauth.responses.RequiredUserAttributeApiResult.RequiredUserAttribute] objects with details about the account attributes required by the server. + */ + class AttributesRequired( + override val nextState: SignUpAttributesRequiredState, + val requiredAttributes: List, + ) : SignUpResult, + Result.SuccessResult(nextState = nextState), + SignUpUsingPasswordResult, + SignUpSubmitCodeResult, + SignUpSubmitAttributesResult, + SignUpSubmitPasswordResult + + /** + * UserAlreadyExists ErrorResult, which indicates there is already an account with the same email. + * If this occurs, the flow should be restarted. + * + * @param error [com.microsoft.identity.client.statemachine.UserAlreadyExistsError] + */ + class UserAlreadyExists( + override val error: UserAlreadyExistsError + ) : SignUpResult, Result.ErrorResult(error = error), SignUpUsingPasswordResult + + /** + * InvalidEmail ErrorResult, which indicates the email provided by the user is not acceptable to the server. + * If this occurs, the flow should be restarted. + */ + class InvalidEmail( + override val error: InvalidEmailError + ) : SignUpResult, Result.ErrorResult(error = error), SignUpUsingPasswordResult + + /** + * InvalidAttributes ErrorResult, which indicates one or more attributes that were sent failed input validation. + * The attributes should be resubmitted. + * + * @param invalidAttributes a list of attributes that failed input validation + */ + class InvalidAttributes( + override val error: InvalidAttributesError, + val invalidAttributes: List, + ) : SignUpResult, Result.ErrorResult(error = error), SignUpUsingPasswordResult, SignUpSubmitAttributesResult + + /** + * PasswordRequired Result, which indicates that the valid password is required from the user to continue. + * + * @param nextState [com.microsoft.identity.client.statemachine.states.SignUpPasswordRequiredState] the current state of the flow with follow-on methods. + */ + class PasswordRequired( + override val nextState: SignUpPasswordRequiredState, + ) : SignUpResult, Result.SuccessResult(nextState = nextState), SignUpSubmitCodeResult + + /** + * InvalidPassword ErrorResult, which indicates the new password provided by the user was not accepted by the server. + * The password should be re-submitted. + * + * @param error [com.microsoft.identity.client.statemachine.InvalidPasswordError]. + */ + class InvalidPassword( + override val error: InvalidPasswordError + ) : SignUpSubmitPasswordResult, SignUpUsingPasswordResult, Result.ErrorResult(error = error) +} + +/** + * Sign up with password result, produced by [com.microsoft.identity.client.INativeAuthPublicClientApplication.signUpUsingPassword] + */ +sealed interface SignUpUsingPasswordResult : Result { + /** + * AuthNotSupported ErrorResult, which indicates the server does not support password authentication. + * Check your challenge type configuration in the client with the user flow configuration in the tenant. + * The flow should be restarted. + * + * @param error [com.microsoft.identity.client.statemachine.GeneralError]. + */ + class AuthNotSupported( + override val error: GeneralError + ) : SignUpUsingPasswordResult, Result.ErrorResult(error = error) +} + +/** + * Sign in submit code result, produced by + * [com.microsoft.identity.client.statemachine.states.SignUpCodeRequiredState.submitCode] + */ +sealed interface SignUpSubmitCodeResult : 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 + ) : SignUpSubmitCodeResult, Result.ErrorResult(error = error) +} + +/** + * Sign in resend code result, produced by + * [com.microsoft.identity.client.statemachine.states.SignUpCodeRequiredState.resendCode] + */ +sealed interface SignUpResendCodeResult : 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: SignUpCodeRequiredState, + val codeLength: Int, + val sentTo: String, + val channel: String + ) : SignUpResendCodeResult, Result.SuccessResult(nextState = nextState) +} + +/** + * Sign in submit code result, produced by + * [com.microsoft.identity.client.statemachine.states.SignUpPasswordRequiredState.submitPassword] + */ +sealed interface SignUpSubmitPasswordResult : + SignUpResult + +/** + * Sign in submit code result, produced by + * [com.microsoft.identity.client.statemachine.states.SignUpAttributesRequiredState.submitAttributes] + */ +sealed interface SignUpSubmitAttributesResult : + SignUpResult diff --git a/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignUpStates.kt b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignUpStates.kt new file mode 100644 index 000000000..81107d5c2 --- /dev/null +++ b/msal/src/main/java/com/microsoft/identity/client/statemachine/states/SignUpStates.kt @@ -0,0 +1,632 @@ +// 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.UserAttributes +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.InvalidAttributesError +import com.microsoft.identity.client.statemachine.InvalidPasswordError +import com.microsoft.identity.client.statemachine.results.SignInResult +import com.microsoft.identity.client.statemachine.results.SignUpResendCodeResult +import com.microsoft.identity.client.statemachine.results.SignUpResult +import com.microsoft.identity.client.statemachine.results.SignUpSubmitAttributesResult +import com.microsoft.identity.client.statemachine.results.SignUpSubmitCodeResult +import com.microsoft.identity.client.statemachine.results.SignUpSubmitPasswordResult +import com.microsoft.identity.client.toListOfRequiredUserAttribute +import com.microsoft.identity.client.toMap +import com.microsoft.identity.common.internal.commands.SignUpResendCodeCommand +import com.microsoft.identity.common.internal.commands.SignUpSubmitCodeCommand +import com.microsoft.identity.common.internal.commands.SignUpSubmitPasswordCommand +import com.microsoft.identity.common.internal.commands.SignUpSubmitUserAttributesCommand +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.SignUpCommandResult +import com.microsoft.identity.common.java.controllers.results.SignUpResendCodeCommandResult +import com.microsoft.identity.common.java.controllers.results.SignUpSubmitCodeCommandResult +import com.microsoft.identity.common.java.controllers.results.SignUpSubmitPasswordCommandResult +import com.microsoft.identity.common.java.controllers.results.SignUpSubmitUserAttributesCommandResult +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. + * SignUpCodeRequiredState class represents a state where the user has to provide a code to progress + * in the signup flow. + * @property flowToken: Flow token to be passed in the next request + * @property username: Email address of the user + * @property config Configuration used by Native Auth + */ +class SignUpCodeRequiredState internal constructor( + override val flowToken: String, + private val username: String, + private val config: NativeAuthPublicClientApplicationConfiguration +) : BaseState(flowToken), State, Serializable { + private val TAG: String = SignUpCodeRequiredState::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.SignUpCodeRequiredState.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, + ): SignUpSubmitCodeResult { + LogSession.logMethodCall(TAG, "${TAG}.submitCode(code: String)") + return withContext(Dispatchers.IO) { + val commandParameters = + CommandParametersAdapter.createSignUpSubmitCodeCommandParameters( + config, + config.oAuth2TokenCache, + code, + flowToken + ) + + val command = SignUpSubmitCodeCommand( + commandParameters, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_SIGN_UP_SUBMIT_CODE + ) + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is SignUpCommandResult.InvalidCode -> { + SignUpSubmitCodeResult.CodeIncorrect( + IncorrectCodeError( + error = result.error, + errorMessage = result.errorDescription, + correlationId = result.correlationId + ) + ) + } + + is SignUpCommandResult.PasswordRequired -> { + SignUpResult.PasswordRequired( + nextState = SignUpPasswordRequiredState( + flowToken = result.signupToken, + username = username, + config = config + ) + ) + } + + is SignUpCommandResult.AttributesRequired -> { + SignUpResult.AttributesRequired( + nextState = SignUpAttributesRequiredState( + flowToken = result.signupToken, + username = username, + config = config + ), + requiredAttributes = result.requiredAttributes.toListOfRequiredUserAttribute() + ) + } + + is SignUpCommandResult.Complete -> { + SignUpResult.Complete( + nextState = SignInAfterSignUpState( + signInVerificationCode = result.signInSLT, + username = username, + config = config + ) + ) + } + + is INativeAuthCommandResult.Redirect -> { + SignUpResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + + // This should be caught earlier in the flow, so throwing UnexpectedError + is SignUpCommandResult.UsernameAlreadyExists -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + SignUpResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ) + ) + } + + is INativeAuthCommandResult.UnknownError -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + SignUpResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + exception = result.exception + ) + ) + } + } + } + } + + interface SignUpWithResendCodeCallback : Callback + + /** + * Resends a new verification code to the user; callback variant. + * + * @param callback [com.microsoft.identity.client.statemachine.states.SignUpCodeRequiredState.SignUpWithResendCodeCallback] to receive the result on. + * @return The results of the resend code action. + */ + fun resendCode( + callback: SignUpWithResendCodeCallback + ) { + 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(): SignUpResendCodeResult { + LogSession.logMethodCall(TAG, "${TAG}.resendCode()") + return withContext(Dispatchers.IO) { + val commandParameters = + CommandParametersAdapter.createSignUpResendCodeCommandParameters( + config, + config.oAuth2TokenCache, + flowToken + ) + val command = SignUpResendCodeCommand( + commandParameters, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_SIGN_UP_RESEND_CODE + ) + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is SignUpCommandResult.CodeRequired -> { + SignUpResendCodeResult.Success( + nextState = SignUpCodeRequiredState( + flowToken = result.signupToken, + username = username, + config = config + ), + codeLength = result.codeLength, + sentTo = result.challengeTargetLabel, + channel = result.challengeChannel + ) + } + + is INativeAuthCommandResult.Redirect -> { + SignUpResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + + is INativeAuthCommandResult.UnknownError -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + SignUpResult.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. + * SignUpPasswordRequiredState class represents a state where the user has to provide a password + * to progress in the signup flow. + * @property flowToken: Flow token to be passed in the next request + * @property username: Email address of the user + * @property config Configuration used by Native Auth + */ +class SignUpPasswordRequiredState internal constructor( + override val flowToken: String, + private val username: String, + private val config: NativeAuthPublicClientApplicationConfiguration +) : BaseState(flowToken), State, Serializable { + private val TAG: String = SignUpPasswordRequiredState::class.java.simpleName + + interface SignUpSubmitPasswordCallback : Callback + + /** + * Submits a password for the account to the server; callback variant. + * + * @param password the password to submit. + * @param callback [com.microsoft.identity.client.statemachine.states.SignUpPasswordRequiredState.SignUpSubmitPasswordCallback] to receive the result on. + * @return The results of the submit password action. + */ + fun submitPassword( + password: CharArray, + callback: SignUpSubmitPasswordCallback + ) { + 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 a password for 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: CharArray): SignUpSubmitPasswordResult { + LogSession.logMethodCall(TAG, "${TAG}.submitPassword(password: String)") + return withContext(Dispatchers.IO) { + val commandParameters = + CommandParametersAdapter.createSignUpSubmitPasswordCommandParameters( + config, + config.oAuth2TokenCache, + flowToken, + password + ) + val command = SignUpSubmitPasswordCommand( + commandParameters, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_SIGN_UP_SUBMIT_PASSWORD + ) + + try { + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = + rawCommandResult.checkAndWrapCommandResultType()) { + is SignUpCommandResult.InvalidPassword -> { + SignUpResult.InvalidPassword( + InvalidPasswordError( + error = result.error, + errorMessage = result.errorDescription, + correlationId = result.correlationId + ) + ) + } + + is SignUpCommandResult.AttributesRequired -> { + SignUpResult.AttributesRequired( + nextState = SignUpAttributesRequiredState( + flowToken = result.signupToken, + username = username, + config = config + ), + requiredAttributes = result.requiredAttributes.toListOfRequiredUserAttribute() + ) + } + + is SignUpCommandResult.Complete -> { + SignUpResult.Complete( + nextState = SignInAfterSignUpState( + signInVerificationCode = result.signInSLT, + username = username, + config = config + ) + ) + } + + is INativeAuthCommandResult.Redirect -> { + SignUpResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + + // This should be caught earlier in the flow, so throwing UnexpectedError + is SignUpCommandResult.UsernameAlreadyExists -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + SignUpResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ) + ) + } + + // This should be caught earlier in the flow, so throwing UnexpectedError + is SignUpCommandResult.InvalidEmail -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + SignUpResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ) + ) + } + + is INativeAuthCommandResult.UnknownError -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + SignUpResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId, + details = result.details, + exception = result.exception + ) + ) + } + } + } finally { + StringUtil.overwriteWithNull(commandParameters.password) + } + } + } +} + +/** + * Native Auth uses a state machine to denote state and transitions for a user. + * SignUpAttributesRequiredState class represents a state where the user has to provide signup + * attributes to progress in the signup flow. + * @property flowToken: Flow token to be passed in the next request + * @property username: Email address of the user + * @property config Configuration used by Native Auth + */ +class SignUpAttributesRequiredState internal constructor( + override val flowToken: String, + private val username: String, + private val config: NativeAuthPublicClientApplicationConfiguration +) : BaseState(flowToken), State, Serializable { + private val TAG: String = SignUpAttributesRequiredState::class.java.simpleName + + interface SignUpSubmitUserAttributesCallback : Callback + + /** + * Submits the user attributes required to the server; callback variant. + * + * @param attributes mandatory attributes set in the tenant configuration. Should use [com.microsoft.identity.client.UserAttributes] to convert to a map. + * @param callback [com.microsoft.identity.client.statemachine.states.SignUpAttributesRequiredState.SignUpSubmitUserAttributesCallback] to receive the result on. + * @return The results of the submit user attributes action. + */ + fun submitAttributes( + attributes: UserAttributes, + callback: SignUpSubmitUserAttributesCallback + ) { + LogSession.logMethodCall(TAG, "${TAG}.submitAttributes") + NativeAuthPublicClientApplication.pcaScope.launch { + try { + val result = submitAttributes(attributes) + callback.onResult(result) + } catch (e: MsalException) { + Logger.error(TAG, "Exception thrown in submitAttributes", e) + callback.onError(e) + } + } + } + + /** + * Submits the user attributes required to the server; Kotlin coroutines variant. + * + * @param attributes mandatory attributes set in the tenant configuration. Should use [com.microsoft.identity.client.UserAttributes] to convert to a map. + * @return The results of the submit user attributes action. + */ + suspend fun submitAttributes(attributes: UserAttributes): SignUpSubmitAttributesResult { + LogSession.logMethodCall(TAG, "${TAG}.submitAttributes(attributes: UserAttributes)") + return withContext(Dispatchers.IO) { + + val commandParameters = + CommandParametersAdapter.createSignUpStarSubmitUserAttributesCommandParameters( + config, + config.oAuth2TokenCache, + flowToken, + attributes.toMap() + ) + + val command = SignUpSubmitUserAttributesCommand( + commandParameters, + NativeAuthMsalController(), + PublicApiId.NATIVE_AUTH_SIGN_UP_SUBMIT_ATTRIBUTES + ) + + val rawCommandResult = CommandDispatcher.submitSilentReturningFuture(command).get() + + return@withContext when (val result = rawCommandResult.checkAndWrapCommandResultType()) { + is SignUpCommandResult.InvalidAttributes -> { + SignUpResult.InvalidAttributes( + error = InvalidAttributesError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ), + invalidAttributes = result.invalidAttributes + ) + } + is SignUpCommandResult.AttributesRequired -> { + SignUpResult.AttributesRequired( + nextState = SignUpAttributesRequiredState( + flowToken = result.signupToken, + username = username, + config = config + ), + requiredAttributes = result.requiredAttributes.toListOfRequiredUserAttribute() + ) + } + is SignUpCommandResult.Complete -> { + SignUpResult.Complete( + nextState = SignInAfterSignUpState( + signInVerificationCode = result.signInSLT, + username = username, + config = config + ) + ) + } + is INativeAuthCommandResult.Redirect -> { + SignUpResult.BrowserRequired( + error = BrowserRequiredError( + correlationId = result.correlationId + ) + ) + } + // This should be caught earlier in the flow, so throwing UnexpectedError + is SignUpCommandResult.UsernameAlreadyExists -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + SignUpResult.UnexpectedError( + error = GeneralError( + errorMessage = result.errorDescription, + error = result.error, + correlationId = result.correlationId + ) + ) + } + is INativeAuthCommandResult.UnknownError -> { + Logger.warn( + TAG, + "Unexpected result: $result" + ) + SignUpResult.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. + * SignInAfterSignUpState class represents a state where the user must signin after successful + * signup flow. + * @property signInVerificationCode: Token to be passed in the next request + * @property username: Email address of the user + * @property config Configuration used by Native Auth + */ +class SignInAfterSignUpState internal constructor( + override val signInVerificationCode: String?, + override val username: String, + private val config: NativeAuthPublicClientApplicationConfiguration +) : SignInAfterSignUpBaseState(signInVerificationCode, username, config) { + private val TAG: String = SignInAfterSignUpState::class.java.simpleName + interface SignInAfterSignUpCallback : SignInAfterSignUpBaseState.SignInAfterSignUpCallback + + /** + * Signs in with the sign-in-after-sign-up verification code; callback variant. + * + * @param scopes (Optional) the scopes to request. + * @param callback [com.microsoft.identity.client.statemachine.states.SignInAfterSignUpState.SignInAfterSignUpCallback] to receive the result on. + * @return The results of the sign-in-after-sign-up action. + */ + fun signIn(scopes: List? = null, callback: SignInAfterSignUpCallback) { + LogSession.logMethodCall(TAG, "${TAG}.signIn") + return signInAfterSignUp(scopes = scopes, callback = callback) + } + + /** + * Signs in with the sign-in-after-sign-up verification code; Kotlin coroutines variant. + * + * @param scopes (Optional) the scopes to request. + * @return The results of the sign-in-after-sign-up action. + */ + suspend fun signIn(scopes: List? = null): SignInResult { + LogSession.logMethodCall(TAG, "${TAG}.signIn(scopes: List)") + return signInAfterSignUp(scopes = scopes) + } +}