From 9ea76cdf488c15fc18edb4dfaaf1fd3e5edae5cb Mon Sep 17 00:00:00 2001 From: Matt Creaser Date: Fri, 12 Apr 2024 14:48:48 -0300 Subject: [PATCH] Backport Gen2 config to Gen1 JSON for older authenticator versions --- .../auth/cognito/AWSCognitoAuthPlugin.kt | 8 +- .../auth/cognito/AuthConfiguration.kt | 51 ++++++ .../codegen/data/IdentityPoolConfiguration.kt | 7 +- .../codegen/data/OauthConfiguration.kt | 31 +++- .../codegen/data/UserPoolConfiguration.kt | 12 +- .../auth/cognito/AuthConfigurationTest.kt | 146 +++++++++++++++++ .../codegen/data/AuthConfigurationTest.kt | 151 ------------------ 7 files changed, 242 insertions(+), 164 deletions(-) delete mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/statemachine/codegen/data/AuthConfigurationTest.kt diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt index 9836f30486..140e3801d6 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt @@ -97,12 +97,13 @@ class AWSCognitoAuthPlugin : AuthPlugin() { } } - private lateinit var pluginConfigurationJSON: JSONObject - + // This function is used for versions of the Authenticator component <= 1.1.0 to get the configuration values needed + // to configure the Authenticator UI. Starting in 1.2.0 it uses getAuthConfiguration() instead. In order to support + // older Authenticator versions we translate the config - whether it comes from Gen1 or Gen2 - back into Gen1 JSON @InternalAmplifyApi @Deprecated("Use getAuthConfiguration instead", replaceWith = ReplaceWith("getAuthConfiguration()")) fun getPluginConfiguration(): JSONObject { - return pluginConfigurationJSON + return getAuthConfiguration().toGen1Json() } @InternalAmplifyApi @@ -127,7 +128,6 @@ class AWSCognitoAuthPlugin : AuthPlugin() { @Throws(AmplifyException::class) override fun configure(pluginConfiguration: JSONObject, context: Context) { - pluginConfigurationJSON = pluginConfiguration try { configure(AuthConfiguration.fromJson(pluginConfiguration), context) } catch (exception: Exception) { diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthConfiguration.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthConfiguration.kt index f82b8a4eb4..e5faf738bb 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthConfiguration.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthConfiguration.kt @@ -64,6 +64,35 @@ data class AuthConfiguration internal constructor( val passwordProtectionSettings: PasswordProtectionSettings? ) { + internal fun toGen1Json(configName: String = "Default"): JSONObject { + val authConfig = JSONObject().apply { + val signupAttributes = signUpAttributes.map { it.keyString.uppercase() } + put("signupAttributes", JSONArray(signupAttributes)) + + val usernameAttributes = usernameAttributes.map { it.toGen1Json() } + put("usernameAttributes", JSONArray(usernameAttributes)) + + val verifyMechanisms = verificationMechanisms.map { it.toGen1Json() } + put("verificationMechanisms", JSONArray(verifyMechanisms)) + + put("authenticationFlowType", authFlowType.name) + + oauth?.let { put("OAuth", it.toGen1Json()) } + passwordProtectionSettings?.let { put("passwordProtectionSettings", it.toGen1Json()) } + } + + return JSONObject().apply { + put("Auth", JSONObject().put(configName, authConfig)) + userPool?.let { put("CognitoUserPool", JSONObject().put(configName, it.toGen1Json())) } + identityPool?.let { + put( + "CredentialsProvider", + JSONObject().put("CognitoIdentity", JSONObject().put(configName, it.toGen1Json())) + ) + } + } + } + internal companion object { /** * Returns an AuthConfiguration instance from JSON @@ -179,6 +208,17 @@ data class AuthConfiguration internal constructor( ) } + private fun PasswordProtectionSettings.toGen1Json() = JSONObject().apply { + put("passwordPolicyMinLength", length) + val characters = JSONArray().apply { + if (requiresLower) put("REQUIRES_LOWER") + if (requiresUpper) put("REQUIRES_UPPER") + if (requiresNumber) put("REQUIRES_NUMBERS") + if (requiresSpecial) put("REQUIRES_SYMBOLS") + } + put("passwordPolicyCharacters", characters) + } + private inline fun JSONArray.map(func: JSONArray.(Int) -> T) = List(length()) { func(it) } @@ -194,11 +234,22 @@ data class AuthConfiguration internal constructor( AmplifyOutputsData.Auth.UsernameAttributes.Username -> UsernameAttribute.Username } + private fun UsernameAttribute.toGen1Json() = when (this) { + UsernameAttribute.Username -> "USERNAME" + UsernameAttribute.Email -> "EMAIL" + UsernameAttribute.PhoneNumber -> "PHONE_NUMBER" + } + private fun AmplifyOutputsData.Auth.UserVerificationTypes.toConfigType() = when (this) { AmplifyOutputsData.Auth.UserVerificationTypes.Email -> VerificationMechanism.Email AmplifyOutputsData.Auth.UserVerificationTypes.PhoneNumber -> VerificationMechanism.PhoneNumber } + private fun VerificationMechanism.toGen1Json() = when (this) { + VerificationMechanism.Email -> "EMAIL" + VerificationMechanism.PhoneNumber -> "PHONE_NUMBER" + } + private fun AmplifyOutputsData.Auth.PasswordPolicy.toConfigType() = PasswordProtectionSettings( length = minLength ?: 6, requiresNumber = requireNumbers ?: false, diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/IdentityPoolConfiguration.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/IdentityPoolConfiguration.kt index 0a48f6dee6..ebeca95f27 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/IdentityPoolConfiguration.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/IdentityPoolConfiguration.kt @@ -26,6 +26,11 @@ data class IdentityPoolConfiguration internal constructor( val region: String?, val poolId: String? ) { + internal fun toGen1Json() = JSONObject().apply { + region?.let { put(Config.REGION.key, it) } + poolId?.let { put(Config.POOL_ID.key, it) } + } + internal companion object { private const val DEFAULT_REGION = "us-east-1" @@ -82,6 +87,6 @@ data class IdentityPoolConfiguration internal constructor( /** * Contains identity pool identifier. */ - POOL_ID("PoolId"), + POOL_ID("PoolId") } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/OauthConfiguration.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/OauthConfiguration.kt index 0372be1e04..8359dc56eb 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/OauthConfiguration.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/OauthConfiguration.kt @@ -16,6 +16,7 @@ package com.amplifyframework.statemachine.codegen.data import com.amplifyframework.annotations.InternalAmplifyApi +import org.json.JSONArray import org.json.JSONObject @InternalAmplifyApi @@ -27,22 +28,38 @@ data class OauthConfiguration internal constructor( val signInRedirectURI: String, val signOutRedirectURI: String ) { + internal fun toGen1Json() = JSONObject().apply { + put(AppClientId, appClient) + appSecret?.let { put(AppClientSecret, it) } + put(WebDomain, domain) + put(Scopes, JSONArray(scopes)) + put(SignInRedirectURI, signInRedirectURI) + put(SignOutRedirectURI, signOutRedirectURI) + } + internal companion object { + private const val AppClientId = "AppClientId" + private const val AppClientSecret = "AppClientSecret" + private const val WebDomain = "WebDomain" + private const val Scopes = "Scopes" + private const val SignInRedirectURI = "SignInRedirectURI" + private const val SignOutRedirectURI = "SignOutRedirectURI" + fun fromJson(jsonObject: JSONObject?): OauthConfiguration? { return jsonObject?.run { - val appClient = optString("AppClientId").takeUnless { it.isNullOrEmpty() } - val appSecret = optString("AppClientSecret", null).takeUnless { it.isNullOrEmpty() } - val domain = optString("WebDomain").takeUnless { it.isNullOrEmpty() } - val scopes = optJSONArray("Scopes")?.let { scopesArray -> + val appClient = optString(AppClientId).takeUnless { it.isNullOrEmpty() } + val appSecret = optString(AppClientSecret, null).takeUnless { it.isNullOrEmpty() } + val domain = optString(WebDomain).takeUnless { it.isNullOrEmpty() } + val scopes = optJSONArray(Scopes)?.let { scopesArray -> val scopesSet = mutableSetOf() - for (i in 0..scopesArray.length()) { + for (i in 0 until scopesArray.length()) { scopesArray.optString(i)?.let { scopesSet.add(it) } } scopesSet } - val signInRedirectURI = optString("SignInRedirectURI").takeUnless { it.isNullOrEmpty() } - val signOutRedirectURI = optString("SignOutRedirectURI").takeUnless { it.isNullOrEmpty() } + val signInRedirectURI = optString(SignInRedirectURI).takeUnless { it.isNullOrEmpty() } + val signOutRedirectURI = optString(SignOutRedirectURI).takeUnless { it.isNullOrEmpty() } return if (appClient != null && domain != null && scopes != null && signInRedirectURI != null && signOutRedirectURI != null diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/UserPoolConfiguration.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/UserPoolConfiguration.kt index 6d2fa8a131..3bffc00ffd 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/UserPoolConfiguration.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/statemachine/codegen/data/UserPoolConfiguration.kt @@ -32,6 +32,15 @@ data class UserPoolConfiguration internal constructor( val pinpointAppId: String? ) { + internal fun toGen1Json() = JSONObject().apply { + region?.let { put(Config.REGION.key, it) } + endpoint?.let { put(Config.ENDPOINT.key, it) } + poolId?.let { put(Config.POOL_ID.key, it) } + appClient?.let { put(Config.APP_CLIENT_ID.key, it) } + appClientSecret?.let { put(Config.APP_CLIENT_SECRET.key, it) } + pinpointAppId?.let { put(Config.PINPOINT_APP_ID.key, it) } + } + internal companion object { private const val DEFAULT_REGION = "us-east-1" @@ -103,8 +112,9 @@ data class UserPoolConfiguration internal constructor( "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9]" + "[A-Za-z0-9\\-]*[A-Za-z0-9])\$" ) - if (!regex.matches(it)) + if (!regex.matches(it)) { throw Exception("Invalid endpoint") + } } return endpoint?.let { "https://$endpoint" diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthConfigurationTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthConfigurationTest.kt index b07fbc3c64..970141d3ad 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthConfigurationTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthConfigurationTest.kt @@ -23,13 +23,155 @@ import com.amplifyframework.testutils.configuration.amplifyOutputsData import io.kotest.assertions.throwables.shouldThrow 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 org.json.JSONObject import org.junit.Test class AuthConfigurationTest { + val jsonObject = JSONObject( + """ + { + "UserAgent": "aws-amplify-cli/0.1.0", + "Version": "0.1.0", + "IdentityManager": { + "Default": {} + }, + "CredentialsProvider": { + "CognitoIdentity": { + "Default": { + "PoolId": "identity-pool-id", + "Region": "us-east-1" + } + } + }, + "CognitoUserPool": { + "Default": { + "PoolId": "user-pool-id", + "AppClientId": "user-app-id", + "Region": "us-east-1" + } + }, + "Auth": { + "Default": { + "authenticationFlowType": "USER_SRP_AUTH", + "OAuth": { + "AppClientId": "testAppClientId", + "AppClientSecret": "testAppClientSecret", + "WebDomain": "webDomain", + "Scopes": ["oauth"], + "SignInRedirectURI": "http://example.com/signin", + "SignOutRedirectURI": "http://example.com/signout" + }, + "socialProviders": [], + "usernameAttributes": ["USERNAME", "PHONE_NUMBER"], + "signupAttributes": [ + "EMAIL", "NAME", "BIRTHDATE" + ], + "passwordProtectionSettings": { + "passwordPolicyMinLength": 10, + "passwordPolicyCharacters": ["REQUIRES_NUMBERS", "REQUIRES_LOWER"] + }, + "mfaConfiguration": "OFF", + "mfaTypes": [ + "SMS" + ], + "verificationMechanisms": [ + "PHONE_NUMBER", "EMAIL" + ] + } + } + } + """.trimIndent() + ) + + @Test + fun `parses auth configuration`() { + val configuration = AuthConfiguration.fromJson(jsonObject) + + configuration.authFlowType shouldBe AuthFlowType.USER_SRP_AUTH + configuration.usernameAttributes shouldContainExactly listOf( + UsernameAttribute.Username, + UsernameAttribute.PhoneNumber + ) + configuration.signUpAttributes shouldContainExactly listOf( + AuthUserAttributeKey.email(), + AuthUserAttributeKey.name(), + AuthUserAttributeKey.birthdate() + ) + configuration.passwordProtectionSettings?.shouldNotBeNull() + configuration.passwordProtectionSettings?.run { + length shouldBe 10 + requiresUpper.shouldBeFalse() + requiresLower.shouldBeTrue() + requiresSpecial.shouldBeFalse() + requiresNumber.shouldBeTrue() + } + configuration.verificationMechanisms shouldContainExactly listOf( + VerificationMechanism.PhoneNumber, + VerificationMechanism.Email + ) + } + + @Test + fun `signupAttributes is empty if json field is missing`() { + getAuthConfig().remove("signupAttributes") + val configuration = AuthConfiguration.fromJson(jsonObject) + configuration.signUpAttributes.shouldBeEmpty() + } + + @Test + fun `usernameAttributes is empty if json field is missing`() { + getAuthConfig().remove("usernameAttributes") + val configuration = AuthConfiguration.fromJson(jsonObject) + configuration.usernameAttributes.shouldBeEmpty() + } + + @Test + fun `verificationMechanisms is empty if json field is missing`() { + getAuthConfig().remove("verificationMechanisms") + val configuration = AuthConfiguration.fromJson(jsonObject) + configuration.verificationMechanisms.shouldBeEmpty() + } + + @Test + fun `passwordProtectionSettings is null if json field is missing`() { + getAuthConfig().remove("passwordProtectionSettings") + val configuration = AuthConfiguration.fromJson(jsonObject) + configuration.passwordProtectionSettings?.shouldBeNull() + } + + @Test + fun `password min length defaults to zero if json field is missing`() { + getPasswordSettings().remove("passwordPolicyMinLength") + val configuration = AuthConfiguration.fromJson(jsonObject) + configuration.passwordProtectionSettings?.length shouldBe 0 + } + + @Test + fun `password character requirements are false if json field is missing`() { + getPasswordSettings().remove("passwordPolicyCharacters") + val configuration = AuthConfiguration.fromJson(jsonObject) + configuration.passwordProtectionSettings?.requiresLower?.shouldBeFalse() + configuration.passwordProtectionSettings?.requiresUpper?.shouldBeFalse() + configuration.passwordProtectionSettings?.requiresNumber?.shouldBeFalse() + configuration.passwordProtectionSettings?.requiresSpecial?.shouldBeFalse() + } + + @Test + fun `rebuilds valid Gen1 JSON`() { + // Go JSON -> Config -> JSON -> Config + val configuration1 = AuthConfiguration.fromJson(jsonObject) + val newJson = configuration1.toGen1Json() + val configuration2 = AuthConfiguration.fromJson(newJson) + + // Verify that the two configuration objects are identical + configuration2 shouldBe configuration1 + } + @Test fun `configures with amplify outputs`() { val data = amplifyOutputsData { @@ -145,4 +287,8 @@ class AuthConfigurationTest { AuthConfiguration.from(data) } } + + private fun getAuthConfig() = jsonObject.getJSONObject("Auth").getJSONObject("Default") + private fun getPasswordSettings() = jsonObject.getJSONObject("Auth").getJSONObject("Default") + .getJSONObject("passwordProtectionSettings") } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/statemachine/codegen/data/AuthConfigurationTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/statemachine/codegen/data/AuthConfigurationTest.kt deleted file mode 100644 index 0826c5e098..0000000000 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/statemachine/codegen/data/AuthConfigurationTest.kt +++ /dev/null @@ -1,151 +0,0 @@ -package com.amplifyframework.statemachine.codegen.data - -import com.amplifyframework.auth.AuthUserAttributeKey -import com.amplifyframework.auth.cognito.AuthConfiguration -import com.amplifyframework.auth.cognito.UsernameAttribute -import com.amplifyframework.auth.cognito.VerificationMechanism -import com.amplifyframework.auth.cognito.options.AuthFlowType -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 org.json.JSONObject -import org.junit.Test - -class AuthConfigurationTest { - val jsonObject = JSONObject( - """ - { - "UserAgent": "aws-amplify-cli/0.1.0", - "Version": "0.1.0", - "IdentityManager": { - "Default": {} - }, - "CredentialsProvider": { - "CognitoIdentity": { - "Default": { - "PoolId": "identity-pool-id", - "Region": "us-east-1" - } - } - }, - "CognitoUserPool": { - "Default": { - "PoolId": "user-pool-id", - "AppClientId": "user-app-id", - "Region": "us-east-1" - } - }, - "Auth": { - "Default": { - "authenticationFlowType": "USER_SRP_AUTH", - "OAuth": { - "AppClientId": "testAppClientId", - "AppClientSecret": "testAppClientSecret", - "WebDomain": "webDomain", - "Scopes": ["oauth"], - "SignInRedirectURI": "http://example.com/signin", - "SignOutRedirectURI": "http://example.com/signout" - }, - "socialProviders": [], - "usernameAttributes": ["USERNAME", "PHONE_NUMBER"], - "signupAttributes": [ - "EMAIL", "NAME", "BIRTHDATE" - ], - "passwordProtectionSettings": { - "passwordPolicyMinLength": 10, - "passwordPolicyCharacters": ["REQUIRES_NUMBERS", "REQUIRES_LOWER"] - }, - "mfaConfiguration": "OFF", - "mfaTypes": [ - "SMS" - ], - "verificationMechanisms": [ - "PHONE_NUMBER", "EMAIL" - ] - } - } - } - """.trimIndent() - ) - - @Test - fun `parses auth configuration`() { - val configuration = AuthConfiguration.fromJson(jsonObject) - - configuration.authFlowType shouldBe AuthFlowType.USER_SRP_AUTH - configuration.usernameAttributes shouldContainExactly listOf( - UsernameAttribute.Username, - UsernameAttribute.PhoneNumber - ) - configuration.signUpAttributes shouldContainExactly listOf( - AuthUserAttributeKey.email(), - AuthUserAttributeKey.name(), - AuthUserAttributeKey.birthdate() - ) - configuration.passwordProtectionSettings?.shouldNotBeNull() - configuration.passwordProtectionSettings?.run { - length shouldBe 10 - requiresUpper.shouldBeFalse() - requiresLower.shouldBeTrue() - requiresSpecial.shouldBeFalse() - requiresNumber.shouldBeTrue() - } - configuration.verificationMechanisms shouldContainExactly listOf( - VerificationMechanism.PhoneNumber, - VerificationMechanism.Email - ) - } - - @Test - fun `signupAttributes is empty if json field is missing`() { - getAuthConfig().remove("signupAttributes") - val configuration = AuthConfiguration.fromJson(jsonObject) - configuration.signUpAttributes.shouldBeEmpty() - } - - @Test - fun `usernameAttributes is empty if json field is missing`() { - getAuthConfig().remove("usernameAttributes") - val configuration = AuthConfiguration.fromJson(jsonObject) - configuration.usernameAttributes.shouldBeEmpty() - } - - @Test - fun `verificationMechanisms is empty if json field is missing`() { - getAuthConfig().remove("verificationMechanisms") - val configuration = AuthConfiguration.fromJson(jsonObject) - configuration.verificationMechanisms.shouldBeEmpty() - } - - @Test - fun `passwordProtectionSettings is null if json field is missing`() { - getAuthConfig().remove("passwordProtectionSettings") - val configuration = AuthConfiguration.fromJson(jsonObject) - configuration.passwordProtectionSettings?.shouldBeNull() - } - - @Test - fun `password min length defaults to zero if json field is missing`() { - getPasswordSettings().remove("passwordPolicyMinLength") - val configuration = AuthConfiguration.fromJson(jsonObject) - configuration.passwordProtectionSettings?.length shouldBe 0 - } - - @Test - fun `password character requirements are false if json field is missing`() { - getPasswordSettings().remove("passwordPolicyCharacters") - val configuration = AuthConfiguration.fromJson(jsonObject) - configuration.passwordProtectionSettings?.requiresLower?.shouldBeFalse() - configuration.passwordProtectionSettings?.requiresUpper?.shouldBeFalse() - configuration.passwordProtectionSettings?.requiresNumber?.shouldBeFalse() - configuration.passwordProtectionSettings?.requiresSpecial?.shouldBeFalse() - } - - private fun getAuthConfig() = jsonObject.getJSONObject("Auth").getJSONObject("Default") - private fun getPasswordSettings() = jsonObject.getJSONObject("Auth").getJSONObject("Default") - .getJSONObject("passwordProtectionSettings") -}