Skip to content

Commit

Permalink
chore(auth): Backport Gen2 config to Gen1 JSON for older authenticato…
Browse files Browse the repository at this point in the history
…r versions (#2768)
  • Loading branch information
mattcreaser authored Apr 17, 2024
1 parent fecd580 commit faba208
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,13 @@ class AWSCognitoAuthPlugin : AuthPlugin<AWSCognitoAuthService>() {
}
}

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
Expand All @@ -127,7 +128,6 @@ class AWSCognitoAuthPlugin : AuthPlugin<AWSCognitoAuthService>() {

@Throws(AmplifyException::class)
override fun configure(pluginConfiguration: JSONObject, context: Context) {
pluginConfigurationJSON = pluginConfiguration
try {
configure(AuthConfiguration.fromJson(pluginConfiguration), context)
} catch (exception: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <T> JSONArray.map(func: JSONArray.(Int) -> T) = List(length()) {
func(it)
}
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -82,6 +87,6 @@ data class IdentityPoolConfiguration internal constructor(
/**
* Contains identity pool identifier.
*/
POOL_ID("PoolId"),
POOL_ID("PoolId")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.amplifyframework.statemachine.codegen.data

import com.amplifyframework.annotations.InternalAmplifyApi
import org.json.JSONArray
import org.json.JSONObject

@InternalAmplifyApi
Expand All @@ -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<String>()
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
Loading

0 comments on commit faba208

Please sign in to comment.