Skip to content

Commit

Permalink
feat(amplify_auth_cognito): Auth Devices API (#735)
Browse files Browse the repository at this point in the history
* Add platform code

* Clean up

* Add licenses

* Fix error handling

* Add logging and update threading logic

* Add iOS unit tests

* Clean up

* Add tests to project

* Fix unit tests
  • Loading branch information
dnys1 authored Jul 30, 2021
1 parent cfa8927 commit c351199
Show file tree
Hide file tree
Showing 26 changed files with 1,037 additions and 16 deletions.
4 changes: 3 additions & 1 deletion packages/amplify_auth_cognito/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ android {
testOptions {
unitTests {
includeAndroidResources = true
returnDefaultValues = true
}
}
buildTypes {
Expand All @@ -63,8 +64,9 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.amplifyframework:aws-auth-cognito:1.22.0'
testImplementation 'junit:junit:4.13'
testImplementation 'org.mockito:mockito-core:3.1.0'
testImplementation 'org.mockito:mockito-core:3.10.0'
testImplementation 'org.mockito:mockito-inline:3.1.0'
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'org.robolectric:robolectric:4.3.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9'
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import android.os.Handler
import android.os.Looper
import androidx.annotation.NonNull
import androidx.annotation.VisibleForTesting
import com.amazonaws.amplify.amplify_auth_cognito.device.DeviceHandler
import com.amazonaws.amplify.amplify_auth_cognito.types.FlutterSignUpResult
import com.amazonaws.amplify.amplify_auth_cognito.types.FlutterSignInResult
import com.amazonaws.amplify.amplify_auth_cognito.types.FlutterFetchCognitoAuthSessionResult
Expand Down Expand Up @@ -90,6 +91,11 @@ public class AuthCognito : FlutterPlugin, ActivityAware, MethodCallHandler, Plug
var eventMessenger: BinaryMessenger? = null
private lateinit var activityBinding: ActivityPluginBinding

/**
* Handles the Devices API.
*/
private val deviceHandler: DeviceHandler = DeviceHandler(errorHandler)

constructor() {
authCognitoHubEventStreamHandler = AuthCognitoHubEventStreamHandler()
}
Expand All @@ -102,7 +108,7 @@ public class AuthCognito : FlutterPlugin, ActivityAware, MethodCallHandler, Plug

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "com.amazonaws.amplify/auth_cognito")
channel.setMethodCallHandler(this);
channel.setMethodCallHandler(this)
context = flutterPluginBinding.applicationContext;
eventMessenger = flutterPluginBinding.getBinaryMessenger();
hubEventChannel = EventChannel(flutterPluginBinding.binaryMessenger,
Expand Down Expand Up @@ -156,6 +162,11 @@ public class AuthCognito : FlutterPlugin, ActivityAware, MethodCallHandler, Plug
return
}

if (DeviceHandler.canHandle(call.method)) {
deviceHandler.onMethodCall(call, result)
return
}

var data : HashMap<String, Any> = HashMap<String, Any> ()
try {
data = checkData(checkArguments(call.arguments));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,7 @@ import com.amazonaws.amplify.amplify_auth_cognito.types.FlutterInvalidStateExcep
import com.amazonaws.amplify.amplify_core.exception.ExceptionUtil
import com.amazonaws.amplify.amplify_core.exception.ExceptionMessages
import com.amazonaws.mobileconnectors.cognitoidentityprovider.exceptions.CognitoCodeExpiredException
import com.amazonaws.services.cognitoidentityprovider.model.InvalidLambdaResponseException
import com.amazonaws.services.cognitoidentityprovider.model.MFAMethodNotFoundException
import com.amazonaws.services.cognitoidentityprovider.model.NotAuthorizedException
import com.amazonaws.services.cognitoidentityprovider.model.SoftwareTokenMFANotFoundException
import com.amazonaws.services.cognitoidentityprovider.model.TooManyFailedAttemptsException
import com.amazonaws.services.cognitoidentityprovider.model.TooManyRequestsException
import com.amazonaws.services.cognitoidentityprovider.model.UnexpectedLambdaException
import com.amazonaws.services.cognitoidentityprovider.model.UserLambdaValidationException
import com.amazonaws.services.cognitoidentityprovider.model.LimitExceededException
import com.amazonaws.services.cognitoidentityprovider.model.InvalidParameterException
import com.amazonaws.services.cognitoidentityprovider.model.ExpiredCodeException
import com.amazonaws.services.cognitoidentityprovider.model.CodeMismatchException
import com.amazonaws.services.cognitoidentityprovider.model.CodeDeliveryFailureException
import com.amazonaws.services.cognitoidentityprovider.model.*

import com.amplifyframework.AmplifyException
import com.amplifyframework.auth.AuthException
Expand Down Expand Up @@ -89,6 +77,7 @@ class AuthErrorHandler {
is ExpiredCodeException -> errorCode = "CodeExpiredException"
is CodeMismatchException -> errorCode = "CodeMismatchException"
is CodeDeliveryFailureException -> errorCode = "CodeDeliveryFailureException"
is InvalidUserPoolConfigurationException -> errorCode = "ConfigurationException"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2021 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.amazonaws.amplify.amplify_auth_cognito.base

import io.flutter.Log
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.*
import java.util.concurrent.atomic.AtomicBoolean

/**
* Thread-safe [MethodChannel.Result] wrapper which prevents multiple replies and automatically posts
* results to the main thread.
*/
class AtomicResult(private val result: MethodChannel.Result, private val operation: String) :
MethodChannel.Result {
private companion object {
/**
* Scope for performing result handling.
* Method channel results must be sent on the main (UI) thread.
*/
val scope = MainScope()
}

/**
* Whether a response has been sent.
*/
private val isSent = AtomicBoolean(false)

override fun success(value: Any?) {
scope.launch {
if (isSent.getAndSet(true)) {
Log.w(
"AtomicResult(${operation})",
"Attempted to send success value after initial reply"
)
return@launch
}
result.success(value)
}
}

override fun error(errorCode: String?, errorMessage: String?, errorDetails: Any?) {
scope.launch {
if (isSent.getAndSet(true)) {
Log.w(
"AtomicResult(${operation})",
"""
Attempted to send error value after initial reply:
| PlatformException{code=${errorCode}, message=${errorMessage}, details=${errorDetails}}
""".trimMargin()
)
return@launch
}
result.error(errorCode, errorMessage, errorDetails)
}
}

override fun notImplemented() {
scope.launch {
if (isSent.getAndSet(true)) {
Log.w(
"AtomicResult(${operation})",
"Attempted to send notImplemented value after initial reply"
)
return@launch
}
result.notImplemented()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2021 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.amazonaws.amplify.amplify_auth_cognito.device

import com.amazonaws.mobile.client.results.Device
import java.time.Instant
import java.util.*

/**
* Attribute key for retrieving a [Device] instance's name.
*/
const val deviceNameKey = "device_name"

/**
* The device's name, if set.
*/
val Device.deviceName: String?
get() = attributes?.get(deviceNameKey)

/**
* Converts this device to a JSON-representable format.
*/
fun Device.toJson(): Map<String, Any?> = mapOf(
"id" to deviceKey,
"name" to deviceName,
"attributes" to attributes,
"createdDate" to createDate?.time,
"lastModifiedDate" to lastModifiedDate?.time,
"lastAuthenticatedDate" to lastAuthenticatedDate?.time
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2021 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.amazonaws.amplify.amplify_auth_cognito.device

import com.amazonaws.amplify.amplify_auth_cognito.AuthErrorHandler
import com.amazonaws.amplify.amplify_auth_cognito.base.AtomicResult
import com.amazonaws.mobile.client.AWSMobileClient
import com.amazonaws.mobile.client.Callback
import com.amazonaws.mobile.client.results.ListDevicesResult
import com.amplifyframework.auth.AuthDevice
import com.amplifyframework.auth.cognito.util.CognitoAuthExceptionConverter
import com.amplifyframework.core.Amplify
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.*

/**
* Handles method calls for the Devices API.
*/
class DeviceHandler(private val errorHandler: AuthErrorHandler) :
MethodChannel.MethodCallHandler {
companion object {
/**
* Methods this handler supports.
*/
private val methods = listOf("rememberDevice", "forgetDevice", "fetchDevices")

/**
* Whether this class can handle [method].
*/
fun canHandle(method: String): Boolean = methods.contains(method)
}

/**
* Scope for running asynchronous tasks.
*/
private val scope = CoroutineScope(Dispatchers.IO) + CoroutineName("DeviceHandler")

@Suppress("UNCHECKED_CAST")
override fun onMethodCall(call: MethodCall, _result: MethodChannel.Result) {
val result = AtomicResult(_result, call.method)
when (call.method) {
"fetchDevices" -> fetchDevices(result)
"rememberDevice" -> rememberDevice(result)
"forgetDevice" -> {
val deviceJson =
(call.arguments as? Map<*, *> ?: emptyMap<String, Any?>()) as Map<String, Any?>
var device: AuthDevice? = null
if (deviceJson.isNotEmpty()) {
val id by deviceJson
device = AuthDevice.fromId(id as String)
}
forgetDevice(result, device)
}
}
}

private fun fetchDevices(result: MethodChannel.Result) {
try {
val cognitoAuthPlugin = Amplify.Auth.getPlugin("awsCognitoAuthPlugin")
val awsMobileClient = cognitoAuthPlugin.escapeHatch as AWSMobileClient
scope.launch {
awsMobileClient.deviceOperations.list(object : Callback<ListDevicesResult> {
override fun onResult(listDevicesResult: ListDevicesResult) {
result.success(listDevicesResult.devices.map { it.toJson() })
}

override fun onError(exception: java.lang.Exception) {
errorHandler.handleAuthError(
result, CognitoAuthExceptionConverter.lookup(
exception, "Fetching devices failed."
)
)
}
})
}
} catch (e: Exception) {
errorHandler.handleAuthError(result, e)
}
}

private fun rememberDevice(result: MethodChannel.Result) {
scope.launch {
Amplify.Auth.rememberDevice(
{ result.success(null) },
{ errorHandler.handleAuthError(result, it) }
)
}
}

private fun forgetDevice(result: MethodChannel.Result, device: AuthDevice? = null) {
scope.launch {
if (device != null) {
Amplify.Auth.forgetDevice(
device,
{ result.success(null) },
{ errorHandler.handleAuthError(result, it) }
)
} else {
Amplify.Auth.forgetDevice(
{ result.success(null) },
{ errorHandler.handleAuthError(result, it) }
)
}
}
}
}
Loading

0 comments on commit c351199

Please sign in to comment.