Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ class AWSCognitoAuthPlugin : AuthPlugin<AWSCognitoAuthService>() {
) { queueFacade.signOut(options) }

override fun deleteUser(onSuccess: Action, onError: Consumer<AuthException>) = enqueue(onSuccess, onError) {
queueFacade.deleteUser()
useCaseFactory.deleteUser().execute()
}

override fun setUpTOTP(onSuccess: Consumer<TOTPSetupDetails>, onError: Consumer<AuthException>) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,6 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth
delegate.signOut(options) { continuation.resume(it) }
}

suspend fun deleteUser() = suspendCoroutine { continuation ->
delegate.deleteUser(
{ continuation.resume(Unit) },
{ continuation.resumeWithException(it) }
)
}

suspend fun federateToIdentityPool(
providerToken: String,
authProvider: AuthProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ import com.amplifyframework.statemachine.codegen.errors.SessionError
import com.amplifyframework.statemachine.codegen.events.AuthEvent
import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent
import com.amplifyframework.statemachine.codegen.events.AuthorizationEvent
import com.amplifyframework.statemachine.codegen.events.DeleteUserEvent
import com.amplifyframework.statemachine.codegen.events.HostedUIEvent
import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent
import com.amplifyframework.statemachine.codegen.events.SignInChallengeEvent
Expand All @@ -102,7 +101,6 @@ import com.amplifyframework.statemachine.codegen.events.SignOutEvent
import com.amplifyframework.statemachine.codegen.states.AuthState
import com.amplifyframework.statemachine.codegen.states.AuthenticationState
import com.amplifyframework.statemachine.codegen.states.AuthorizationState
import com.amplifyframework.statemachine.codegen.states.DeleteUserState
import com.amplifyframework.statemachine.codegen.states.HostedUISignInState
import com.amplifyframework.statemachine.codegen.states.SRPSignInState
import com.amplifyframework.statemachine.codegen.states.SetupTOTPState
Expand All @@ -117,10 +115,8 @@ import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch

internal class RealAWSCognitoAuthPlugin(
val configuration: AuthConfiguration,
Expand Down Expand Up @@ -1238,68 +1234,6 @@ internal class RealAWSCognitoAuthPlugin(
)
}

fun deleteUser(onSuccess: Action, onError: Consumer<AuthException>) {
authStateMachine.getCurrentState { authState ->
when (authState.authNState) {
is AuthenticationState.SignedIn -> {
GlobalScope.launch {
try {
val accessToken = getSession().userPoolTokensResult.value?.accessToken
accessToken?.let {
_deleteUser(accessToken, onSuccess, onError)
} ?: onError.accept(SignedOutException())
} catch (error: Exception) {
onError.accept(SignedOutException())
}
}
}
is AuthenticationState.SignedOut -> onError.accept(SignedOutException())
else -> onError.accept(InvalidStateException())
}
}
}

private fun _deleteUser(token: String, onSuccess: Action, onError: Consumer<AuthException>) {
val listenerToken = StateChangeListenerToken()
var deleteUserException: Exception? = null
authStateMachine.listen(
listenerToken,
{ authState ->
if (authState is AuthState.Configured) {
val (authNState, authZState) = authState
val exception = deleteUserException
when {
authZState is AuthorizationState.DeletingUser &&
authZState.deleteUserState is DeleteUserState.Error -> {
deleteUserException = authZState.deleteUserState.exception
}
authNState is AuthenticationState.SignedOut && authZState is AuthorizationState.Configured -> {
sendHubEvent(AuthChannelEventName.USER_DELETED.toString())
authStateMachine.cancel(listenerToken)
onSuccess.call()
}
authZState is AuthorizationState.SessionEstablished && exception != null -> {
authStateMachine.cancel(listenerToken)
onError.accept(
CognitoAuthExceptionConverter.lookup(
exception,
"Request to delete user may have failed. Please check exception stack"
)
)
}
else -> {
// No - op
}
}
}
},
{
val event = DeleteUserEvent(DeleteUserEvent.EventType.DeleteUser(accessToken = token))
authStateMachine.send(event)
}
)
}

private fun addAuthStateChangeListener() {
authStateMachine.listen(
StateChangeListenerToken(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,9 @@ internal class AuthUseCaseFactory(
fetchMfaPreference = fetchMfaPreference(),
stateMachine = stateMachine
)

fun deleteUser() = DeleteUserUseCase(
fetchAuthSession = fetchAuthSession(),
stateMachine = stateMachine
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package com.amplifyframework.auth.cognito.usecases

import com.amplifyframework.auth.AuthChannelEventName
import com.amplifyframework.auth.cognito.AuthStateMachine
import com.amplifyframework.auth.cognito.CognitoAuthExceptionConverter
import com.amplifyframework.auth.cognito.helpers.collectWhile
import com.amplifyframework.auth.cognito.requireAccessToken
import com.amplifyframework.auth.cognito.requireSignedInState
import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter
import com.amplifyframework.statemachine.codegen.events.DeleteUserEvent
import com.amplifyframework.statemachine.codegen.states.AuthenticationState
import com.amplifyframework.statemachine.codegen.states.AuthorizationState
import com.amplifyframework.statemachine.codegen.states.DeleteUserState
import kotlinx.coroutines.flow.onStart

internal class DeleteUserUseCase(
private val fetchAuthSession: FetchAuthSessionUseCase,
private val stateMachine: AuthStateMachine,
private val emitter: AuthHubEventEmitter = AuthHubEventEmitter()
) {

suspend fun execute() {
stateMachine.requireSignedInState()

val token = fetchAuthSession.execute().requireAccessToken()

var deleteUserException: Exception? = null
stateMachine.state
.onStart {
val event = DeleteUserEvent(DeleteUserEvent.EventType.DeleteUser(accessToken = token))
stateMachine.send(event)
}.collectWhile { authState ->
val authNState = authState.authNState
val authZState = authState.authZState

when {
authZState is AuthorizationState.DeletingUser &&
authZState.deleteUserState is DeleteUserState.Error -> {
// Hold on to the exception until the state machine settles
deleteUserException = authZState.deleteUserState.exception
true
}
authNState is AuthenticationState.SignedOut && authZState is AuthorizationState.Configured -> {
emitter.sendHubEvent(AuthChannelEventName.USER_DELETED.toString())
false // done
}
authZState is AuthorizationState.SessionEstablished && deleteUserException != null -> {
throw CognitoAuthExceptionConverter.lookup(
deleteUserException!!,
"Request to delete user may have failed. Please check exception stack"
)
}
else -> true // no-op
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -640,9 +640,10 @@ class AWSCognitoAuthPluginTest {
val expectedOnSuccess = Action { }
val expectedOnError = Consumer<AuthException> { }

val useCase = authPlugin.useCaseFactory.deleteUser()
authPlugin.deleteUser(expectedOnSuccess, expectedOnError)

verify(timeout = CHANNEL_TIMEOUT) { realPlugin.deleteUser(any(), any()) }
coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute() }
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@ import com.amplifyframework.auth.AuthUserAttributeKey
import com.amplifyframework.auth.cognito.options.AuthFlowType
import com.amplifyframework.auth.exceptions.ConfigurationException
import com.amplifyframework.core.configuration.AmplifyOutputsData
import com.amplifyframework.statemachine.codegen.data.UserPoolConfiguration
import com.amplifyframework.testutils.configuration.amplifyOutputsData
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.throwables.shouldThrowWithMessage
import io.kotest.matchers.booleans.shouldBeFalse
import io.kotest.matchers.booleans.shouldBeTrue
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import kotlin.test.assertEquals
import org.json.JSONObject
import org.junit.Test

Expand Down Expand Up @@ -270,6 +273,102 @@ class AuthConfigurationTest {
}
}

@Test
fun `custom endpoint with query fails`() {
val configJsonObject = JSONObject()
configJsonObject.put("PoolId", "TestUserPool")
configJsonObject.put("AppClientId", "0000000000")
configJsonObject.put("Region", "test-region")
val invalidEndpoint = "fsjjdh.com?q=id"
configJsonObject.put("Endpoint", invalidEndpoint)
val expectedErrorMessage = "Invalid endpoint value $invalidEndpoint. Expected fully qualified hostname with " +
"no scheme, no path and no query"

shouldThrowWithMessage<Exception>(expectedErrorMessage) {
UserPoolConfiguration.fromJson(configJsonObject).build()
}
}

@Test
fun `custom endpoint with path fails`() {
val configJsonObject = JSONObject()
configJsonObject.put("PoolId", "TestUserPool")
configJsonObject.put("AppClientId", "0000000000")
configJsonObject.put("Region", "test-region")
val invalidEndpoint = "fsjjdh.com/id"
configJsonObject.put("Endpoint", invalidEndpoint)
val expectedErrorMessage = "Invalid endpoint value $invalidEndpoint. Expected fully qualified hostname with " +
"no scheme, no path and no query"

shouldThrowWithMessage<Exception>(expectedErrorMessage) {
UserPoolConfiguration.fromJson(configJsonObject).build()
}
}

@Test
fun `custom endpoint with scheme fails`() {
val configJsonObject = JSONObject()
configJsonObject.put("PoolId", "TestUserPool")
configJsonObject.put("AppClientId", "0000000000")
configJsonObject.put("Region", "test-region")

val invalidEndpoint = "https://fsjjdh.com"
configJsonObject.put("Endpoint", invalidEndpoint)
val expectedErrorMessage = "Invalid endpoint value $invalidEndpoint. Expected fully qualified hostname with " +
"no scheme, no path and no query"

shouldThrowWithMessage<Exception>(expectedErrorMessage) {
UserPoolConfiguration.fromJson(configJsonObject).build()
}
}

@Test
fun `custom endpoint with no query,path, scheme success`() {
val configJsonObject = JSONObject()
val poolId = "TestUserPool"
val region = "test-region"
val appClientId = "0000000000"
val endpoint = "fsjjdh.com"
configJsonObject.put("PoolId", poolId)
configJsonObject.put("AppClientId", appClientId)
configJsonObject.put("Region", region)
configJsonObject.put("Endpoint", endpoint)

val userPool = UserPoolConfiguration.fromJson(configJsonObject).build()
assertEquals(userPool.region, region, "Regions do not match expected")
assertEquals(userPool.poolId, poolId, "Pool id do not match expected")
assertEquals(userPool.appClient, appClientId, "AppClientId do not match expected")
assertEquals(userPool.endpoint, "https://$endpoint", "Endpoint do not match expected")
}

@Test
fun `validate auth flow type defaults to user_srp_auth for invalid types`() {
val configJsonObject = JSONObject()
val configAuthJsonObject = JSONObject()
val configAuthDefaultJsonObject = JSONObject()
configAuthDefaultJsonObject.put("authenticationFlowType", "INVALID_FLOW_TYPE")
configAuthJsonObject.put("Default", configAuthDefaultJsonObject)
configJsonObject.put("Auth", configAuthJsonObject)
val configuration = AuthConfiguration.fromJson(configJsonObject)
assertEquals(configuration.authFlowType, AuthFlowType.USER_SRP_AUTH, "Auth flow types do not match expected")
}

@Test
fun `validate auth flow type success`() {
val configJsonObject = JSONObject()
val configAuthJsonObject = JSONObject()
val configAuthDefaultJsonObject = JSONObject()
configAuthDefaultJsonObject.put("authenticationFlowType", "USER_PASSWORD_AUTH")
configAuthJsonObject.put("Default", configAuthDefaultJsonObject)
configJsonObject.put("Auth", configAuthJsonObject)
val configuration = AuthConfiguration.fromJson(configJsonObject)
assertEquals(
configuration.authFlowType,
AuthFlowType.USER_PASSWORD_AUTH,
"Auth flow types do not match expected"
)
}

private fun getAuthConfig() = jsonObject.getJSONObject("Auth").getJSONObject("Default")
private fun getPasswordSettings() = jsonObject.getJSONObject("Auth").getJSONObject("Default")
.getJSONObject("passwordProtectionSettings")
Expand Down
Loading