Skip to content

Commit

Permalink
Configure Auth Plugin when using Amplify Outputs
Browse files Browse the repository at this point in the history
  • Loading branch information
mattcreaser committed Apr 15, 2024
1 parent 0a59fbc commit 222986a
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import com.amplifyframework.core.Action
import com.amplifyframework.core.Amplify
import com.amplifyframework.core.Consumer
import com.amplifyframework.core.category.CategoryType
import com.amplifyframework.core.configuration.AmplifyOutputsData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -128,36 +129,52 @@ class AWSCognitoAuthPlugin : AuthPlugin<AWSCognitoAuthService>() {
override fun configure(pluginConfiguration: JSONObject, context: Context) {
pluginConfigurationJSON = pluginConfiguration
try {
val configuration = AuthConfiguration.fromJson(pluginConfiguration)
val credentialStoreClient = CredentialStoreClient(configuration, context, logger)
val authEnvironment = AuthEnvironment(
context,
configuration,
AWSCognitoAuthService.fromConfiguration(configuration),
credentialStoreClient,
configuration.userPool?.let { UserContextDataProvider(context, it.poolId!!, it.appClient!!) },
HostedUIClient.create(context, configuration.oauth, logger),
logger
)

val authStateMachine = AuthStateMachine(authEnvironment)
realPlugin = RealAWSCognitoAuthPlugin(
configuration,
authEnvironment,
authStateMachine,
logger
configure(AuthConfiguration.fromJson(pluginConfiguration), context)
} catch (exception: Exception) {
throw ConfigurationException(
"Failed to configure AWSCognitoAuthPlugin.",
"Make sure your amplifyconfiguration.json is valid.",
exception
)
}
}

blockQueueChannelWhileConfiguring()
@InternalAmplifyApi
override fun configure(amplifyOutputs: AmplifyOutputsData, context: Context) {
try {
configure(AuthConfiguration.from(amplifyOutputs), context)
} catch (exception: Exception) {
throw ConfigurationException(
"Failed to configure AWSCognitoAuthPlugin.",
"Make sure your amplifyconfiguration.json is valid.",
"Make sure your amplify-outputs.json is valid.",
exception
)
}
}

private fun configure(configuration: AuthConfiguration, context: Context) {
val credentialStoreClient = CredentialStoreClient(configuration, context, logger)
val authEnvironment = AuthEnvironment(
context,
configuration,
AWSCognitoAuthService.fromConfiguration(configuration),
credentialStoreClient,
configuration.userPool?.let { UserContextDataProvider(context, it.poolId!!, it.appClient!!) },
HostedUIClient.create(context, configuration.oauth, logger),
logger
)

val authStateMachine = AuthStateMachine(authEnvironment)
realPlugin = RealAWSCognitoAuthPlugin(
configuration,
authEnvironment,
authStateMachine,
logger
)

blockQueueChannelWhileConfiguring()
}

// Auth configuration is an async process. Wait until the state machine is in a settled state before attempting
// to process any customer calls
private fun blockQueueChannelWhileConfiguring() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import androidx.annotation.IntRange
import com.amplifyframework.annotations.InternalAmplifyApi
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.IdentityPoolConfiguration
import com.amplifyframework.statemachine.codegen.data.OauthConfiguration
import com.amplifyframework.statemachine.codegen.data.UserPoolConfiguration
Expand Down Expand Up @@ -109,6 +111,50 @@ data class AuthConfiguration internal constructor(
passwordProtectionSettings = getPasswordProtectionSettings(authConfig)
)
}

fun from(amplifyOutputs: AmplifyOutputsData): AuthConfiguration {
val auth = amplifyOutputs.auth ?: throw ConfigurationException(
"Missing Auth configuration",
"Ensure the auth category is properly configured"
)

val oauth = auth.oauth?.let {
OauthConfiguration(
appClient = auth.userPoolClientId,
appSecret = null, // Not supported in Gen2
domain = it.domain,
scopes = it.scopes.toSet(),
// Note: Gen2 config gives an array for these values, while Gen1 is just a String. In Gen1
// if you specify multiple URIs the CLI will join them to a comma-delimited string in the json.
// We are matching that behaviour here for Gen2.
signInRedirectURI = it.redirectSignInUri.joinToString(","),
signOutRedirectURI = it.redirectSignOutUri.joinToString(",")
)
}

val identityPool = auth.identityPoolId?.let {
IdentityPoolConfiguration(region = auth.awsRegion, poolId = it)
}

return AuthConfiguration(
userPool = UserPoolConfiguration(
region = auth.awsRegion,
endpoint = null, // Not supported in Gen2
poolId = auth.userPoolId,
appClient = auth.userPoolClientId,
appClientSecret = null, // Not supported in Gen2
pinpointAppId = null // Not supported in Gen2
),
identityPool = identityPool,
oauth = oauth,
authFlowType = auth.authenticationFlowType.toConfigType(),
signUpAttributes = auth.standardRequiredAttributes,
usernameAttributes = auth.usernameAttributes.map { it.toConfigType() },
verificationMechanisms = auth.userVerificationTypes.map { it.toConfigType() },
passwordProtectionSettings = auth.passwordPolicy?.toConfigType()
)
}

private fun getAuthenticationFlowType(authType: String?): AuthFlowType {
return if (!authType.isNullOrEmpty() && AuthFlowType.values().any { it.name == authType }) {
AuthFlowType.valueOf(authType)
Expand All @@ -135,5 +181,29 @@ data class AuthConfiguration internal constructor(
private inline fun <T> JSONArray.map(func: JSONArray.(Int) -> T) = List(length()) {
func(it)
}

private fun AmplifyOutputsData.Auth.AuthenticationFlowType.toConfigType() = when (this) {
AmplifyOutputsData.Auth.AuthenticationFlowType.USER_SRP_AUTH -> AuthFlowType.USER_SRP_AUTH
AmplifyOutputsData.Auth.AuthenticationFlowType.CUSTOM_AUTH -> AuthFlowType.CUSTOM_AUTH
}

private fun AmplifyOutputsData.Auth.UsernameAttributes.toConfigType() = when (this) {
AmplifyOutputsData.Auth.UsernameAttributes.EMAIL -> UsernameAttribute.Email
AmplifyOutputsData.Auth.UsernameAttributes.PHONE -> UsernameAttribute.PhoneNumber
AmplifyOutputsData.Auth.UsernameAttributes.USERNAME -> UsernameAttribute.Username
}

private fun AmplifyOutputsData.Auth.UserVerificationTypes.toConfigType() = when (this) {
AmplifyOutputsData.Auth.UserVerificationTypes.EMAIL -> VerificationMechanism.Email
AmplifyOutputsData.Auth.UserVerificationTypes.PHONE -> VerificationMechanism.PhoneNumber
}

private fun AmplifyOutputsData.Auth.PasswordPolicy.toConfigType() = PasswordProtectionSettings(
length = minLength ?: 6,
requiresNumber = requireNumbers ?: false,
requiresSpecial = requireSymbols ?: false,
requiresUpper = requireUppercase ?: false,
requiresLower = requireLowercase ?: false
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright 2024 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

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.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.shouldContainExactly
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import org.junit.Test

class AuthConfigurationTest {
@Test
fun `configures with amplify outputs`() {
val data = amplifyOutputsData {
auth {
awsRegion = "test-region"
userPoolId = "userpool"
userPoolClientId = "userpool-client"
identityPoolId = "identity-pool"
authenticationFlowType = AmplifyOutputsData.Auth.AuthenticationFlowType.CUSTOM_AUTH
passwordPolicy {
requireLowercase = true
requireSymbols = true
}
oauth {
domain = "https://test.com"
identityProviders += AmplifyOutputsData.Auth.Oauth.IdentityProviders.GOOGLE
scopes += listOf("myScope", "myScope2")
redirectSignInUri += "https://test.com/signin"
redirectSignOutUri += "https://test.com/signout"
responseType = AmplifyOutputsData.Auth.Oauth.ResponseType.TOKEN
}
standardRequiredAttributes += AuthUserAttributeKey.email()
usernameAttributes += AmplifyOutputsData.Auth.UsernameAttributes.EMAIL
userVerificationTypes += AmplifyOutputsData.Auth.UserVerificationTypes.EMAIL
mfaConfiguration = AmplifyOutputsData.Auth.MfaConfiguration.REQUIRED
mfaMethods += AmplifyOutputsData.Auth.MfaMethods.SMS
}
}

val configuration = AuthConfiguration.from(data)

configuration.authFlowType shouldBe AuthFlowType.CUSTOM_AUTH
configuration.userPool.shouldNotBeNull().run {
region shouldBe "test-region"
poolId shouldBe "userpool"
appClient shouldBe "userpool-client"
appClientSecret.shouldBeNull()
endpoint.shouldBeNull()
pinpointAppId.shouldBeNull()
}
configuration.oauth.shouldNotBeNull().run {
appClient shouldBe "userpool-client"
appSecret.shouldBeNull()
domain shouldBe "https://test.com"
scopes shouldContainExactly listOf("myScope", "myScope2")
signInRedirectURI shouldBe "https://test.com/signin"
signOutRedirectURI shouldBe "https://test.com/signout"
}
configuration.identityPool.shouldNotBeNull().run {
region shouldBe "test-region"
poolId shouldBe "identity-pool"
}
configuration.passwordProtectionSettings.shouldNotBeNull().run {
length shouldBe 6
requiresLower.shouldBeTrue()
requiresSpecial.shouldBeTrue()
requiresUpper.shouldBeFalse()
requiresNumber.shouldBeFalse()
}
configuration.signUpAttributes shouldContainExactly listOf(AuthUserAttributeKey.email())
configuration.usernameAttributes shouldContainExactly listOf(UsernameAttribute.Email)
configuration.verificationMechanisms shouldContainExactly listOf(VerificationMechanism.Email)
}

@Test
fun `configures with minimal amplify outputs`() {
val data = amplifyOutputsData {
auth {
awsRegion = "test-region"
userPoolId = "userpool"
userPoolClientId = "userpool-client"
authenticationFlowType = AmplifyOutputsData.Auth.AuthenticationFlowType.CUSTOM_AUTH
}
}

val configuration = AuthConfiguration.from(data)

configuration.authFlowType shouldBe AuthFlowType.CUSTOM_AUTH
configuration.userPool.shouldNotBeNull().run {
region shouldBe "test-region"
poolId shouldBe "userpool"
appClient shouldBe "userpool-client"
}

configuration.oauth.shouldBeNull()
configuration.passwordProtectionSettings.shouldBeNull()
configuration.identityPool.shouldBeNull()
}

@Test
fun `throws exception if auth is not configured in amplify outputs`() {
val data = amplifyOutputsData {
// do not configure auth
}

shouldThrow<ConfigurationException> {
AuthConfiguration.from(data)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package com.amplifyframework.testutils.configuration

import com.amplifyframework.auth.AuthUserAttributeKey
import com.amplifyframework.core.configuration.AmplifyOutputsData
import kotlinx.serialization.json.JsonObject

Expand All @@ -35,6 +36,10 @@ class AmplifyOutputsDataBuilder : AmplifyOutputsData {
analytics = AnalyticsBuilder().apply(func)
}

fun auth(func: AuthBuilder.() -> Unit) {
auth = AuthBuilder().apply(func)
}

fun geo(func: GeoBuilder.() -> Unit) {
geo = GeoBuilder().apply(func)
}
Expand All @@ -53,6 +58,49 @@ class AnalyticsBuilder : AmplifyOutputsData.Analytics {
override var appId: String = "analytics-app-id"
}

class AuthBuilder : AmplifyOutputsData.Auth {
override var awsRegion: String = "us-east-1"
override var authenticationFlowType: AmplifyOutputsData.Auth.AuthenticationFlowType =
AmplifyOutputsData.Auth.AuthenticationFlowType.USER_SRP_AUTH
override var userPoolId: String = "user-pool-id"
override var userPoolClientId: String = "user-pool-client-id"
override var identityPoolId: String? = null
override var passwordPolicy: AmplifyOutputsData.Auth.PasswordPolicy? = null
override var oauth: AmplifyOutputsData.Auth.Oauth? = null
override val standardRequiredAttributes: MutableList<AuthUserAttributeKey> = mutableListOf()
override val usernameAttributes: MutableList<AmplifyOutputsData.Auth.UsernameAttributes> = mutableListOf()
override val userVerificationTypes: MutableList<AmplifyOutputsData.Auth.UserVerificationTypes> = mutableListOf()
override var unauthenticatedIdentitiesEnabled: Boolean = true
override var mfaConfiguration: AmplifyOutputsData.Auth.MfaConfiguration? = null
override val mfaMethods: MutableList<AmplifyOutputsData.Auth.MfaMethods> = mutableListOf()

fun passwordPolicy(func: PasswordPolicyBuilder.() -> Unit) {
passwordPolicy = PasswordPolicyBuilder().apply(func)
}

fun oauth(func: OauthBuilder.() -> Unit) {
oauth = OauthBuilder().apply(func)
}
}

class PasswordPolicyBuilder : AmplifyOutputsData.Auth.PasswordPolicy {
override var minLength: Int? = null
override var requireNumbers: Boolean? = null
override var requireLowercase: Boolean? = null
override var requireUppercase: Boolean? = null
override var requireSymbols: Boolean? = null
}

class OauthBuilder : AmplifyOutputsData.Auth.Oauth {
override val identityProviders: MutableList<AmplifyOutputsData.Auth.Oauth.IdentityProviders> = mutableListOf()
override var domain: String = "domain"
override val scopes: MutableList<String> = mutableListOf()
override val redirectSignInUri: MutableList<String> = mutableListOf()
override val redirectSignOutUri: MutableList<String> = mutableListOf()
override var responseType: AmplifyOutputsData.Auth.Oauth.ResponseType =
AmplifyOutputsData.Auth.Oauth.ResponseType.CODE
}

class GeoBuilder : AmplifyOutputsData.Geo {
override var awsRegion: String = "us-east-1"
override var maps: AmplifyOutputsData.Geo.Maps? = null
Expand Down

0 comments on commit 222986a

Please sign in to comment.