Skip to content

Commit c351199

Browse files
authored
feat(amplify_auth_cognito): Auth Devices API (#735)
* 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
1 parent cfa8927 commit c351199

File tree

26 files changed

+1037
-16
lines changed

26 files changed

+1037
-16
lines changed

packages/amplify_auth_cognito/android/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ android {
4949
testOptions {
5050
unitTests {
5151
includeAndroidResources = true
52+
returnDefaultValues = true
5253
}
5354
}
5455
buildTypes {
@@ -63,8 +64,9 @@ dependencies {
6364
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
6465
implementation 'com.amplifyframework:aws-auth-cognito:1.22.0'
6566
testImplementation 'junit:junit:4.13'
66-
testImplementation 'org.mockito:mockito-core:3.1.0'
67+
testImplementation 'org.mockito:mockito-core:3.10.0'
6768
testImplementation 'org.mockito:mockito-inline:3.1.0'
6869
testImplementation 'androidx.test:core:1.2.0'
6970
testImplementation 'org.robolectric:robolectric:4.3.1'
71+
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9'
7072
}

packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/AuthCognito.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import android.os.Handler
2222
import android.os.Looper
2323
import androidx.annotation.NonNull
2424
import androidx.annotation.VisibleForTesting
25+
import com.amazonaws.amplify.amplify_auth_cognito.device.DeviceHandler
2526
import com.amazonaws.amplify.amplify_auth_cognito.types.FlutterSignUpResult
2627
import com.amazonaws.amplify.amplify_auth_cognito.types.FlutterSignInResult
2728
import com.amazonaws.amplify.amplify_auth_cognito.types.FlutterFetchCognitoAuthSessionResult
@@ -90,6 +91,11 @@ public class AuthCognito : FlutterPlugin, ActivityAware, MethodCallHandler, Plug
9091
var eventMessenger: BinaryMessenger? = null
9192
private lateinit var activityBinding: ActivityPluginBinding
9293

94+
/**
95+
* Handles the Devices API.
96+
*/
97+
private val deviceHandler: DeviceHandler = DeviceHandler(errorHandler)
98+
9399
constructor() {
94100
authCognitoHubEventStreamHandler = AuthCognitoHubEventStreamHandler()
95101
}
@@ -102,7 +108,7 @@ public class AuthCognito : FlutterPlugin, ActivityAware, MethodCallHandler, Plug
102108

103109
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
104110
channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "com.amazonaws.amplify/auth_cognito")
105-
channel.setMethodCallHandler(this);
111+
channel.setMethodCallHandler(this)
106112
context = flutterPluginBinding.applicationContext;
107113
eventMessenger = flutterPluginBinding.getBinaryMessenger();
108114
hubEventChannel = EventChannel(flutterPluginBinding.binaryMessenger,
@@ -156,6 +162,11 @@ public class AuthCognito : FlutterPlugin, ActivityAware, MethodCallHandler, Plug
156162
return
157163
}
158164

165+
if (DeviceHandler.canHandle(call.method)) {
166+
deviceHandler.onMethodCall(call, result)
167+
return
168+
}
169+
159170
var data : HashMap<String, Any> = HashMap<String, Any> ()
160171
try {
161172
data = checkData(checkArguments(call.arguments));

packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/AuthErrorHandler.kt

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,7 @@ import com.amazonaws.amplify.amplify_auth_cognito.types.FlutterInvalidStateExcep
2323
import com.amazonaws.amplify.amplify_core.exception.ExceptionUtil
2424
import com.amazonaws.amplify.amplify_core.exception.ExceptionMessages
2525
import com.amazonaws.mobileconnectors.cognitoidentityprovider.exceptions.CognitoCodeExpiredException
26-
import com.amazonaws.services.cognitoidentityprovider.model.InvalidLambdaResponseException
27-
import com.amazonaws.services.cognitoidentityprovider.model.MFAMethodNotFoundException
28-
import com.amazonaws.services.cognitoidentityprovider.model.NotAuthorizedException
29-
import com.amazonaws.services.cognitoidentityprovider.model.SoftwareTokenMFANotFoundException
30-
import com.amazonaws.services.cognitoidentityprovider.model.TooManyFailedAttemptsException
31-
import com.amazonaws.services.cognitoidentityprovider.model.TooManyRequestsException
32-
import com.amazonaws.services.cognitoidentityprovider.model.UnexpectedLambdaException
33-
import com.amazonaws.services.cognitoidentityprovider.model.UserLambdaValidationException
34-
import com.amazonaws.services.cognitoidentityprovider.model.LimitExceededException
35-
import com.amazonaws.services.cognitoidentityprovider.model.InvalidParameterException
36-
import com.amazonaws.services.cognitoidentityprovider.model.ExpiredCodeException
37-
import com.amazonaws.services.cognitoidentityprovider.model.CodeMismatchException
38-
import com.amazonaws.services.cognitoidentityprovider.model.CodeDeliveryFailureException
26+
import com.amazonaws.services.cognitoidentityprovider.model.*
3927

4028
import com.amplifyframework.AmplifyException
4129
import com.amplifyframework.auth.AuthException
@@ -89,6 +77,7 @@ class AuthErrorHandler {
8977
is ExpiredCodeException -> errorCode = "CodeExpiredException"
9078
is CodeMismatchException -> errorCode = "CodeMismatchException"
9179
is CodeDeliveryFailureException -> errorCode = "CodeDeliveryFailureException"
80+
is InvalidUserPoolConfigurationException -> errorCode = "ConfigurationException"
9281
}
9382
}
9483
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package com.amazonaws.amplify.amplify_auth_cognito.base
16+
17+
import io.flutter.Log
18+
import io.flutter.plugin.common.MethodChannel
19+
import kotlinx.coroutines.*
20+
import java.util.concurrent.atomic.AtomicBoolean
21+
22+
/**
23+
* Thread-safe [MethodChannel.Result] wrapper which prevents multiple replies and automatically posts
24+
* results to the main thread.
25+
*/
26+
class AtomicResult(private val result: MethodChannel.Result, private val operation: String) :
27+
MethodChannel.Result {
28+
private companion object {
29+
/**
30+
* Scope for performing result handling.
31+
* Method channel results must be sent on the main (UI) thread.
32+
*/
33+
val scope = MainScope()
34+
}
35+
36+
/**
37+
* Whether a response has been sent.
38+
*/
39+
private val isSent = AtomicBoolean(false)
40+
41+
override fun success(value: Any?) {
42+
scope.launch {
43+
if (isSent.getAndSet(true)) {
44+
Log.w(
45+
"AtomicResult(${operation})",
46+
"Attempted to send success value after initial reply"
47+
)
48+
return@launch
49+
}
50+
result.success(value)
51+
}
52+
}
53+
54+
override fun error(errorCode: String?, errorMessage: String?, errorDetails: Any?) {
55+
scope.launch {
56+
if (isSent.getAndSet(true)) {
57+
Log.w(
58+
"AtomicResult(${operation})",
59+
"""
60+
Attempted to send error value after initial reply:
61+
| PlatformException{code=${errorCode}, message=${errorMessage}, details=${errorDetails}}
62+
""".trimMargin()
63+
)
64+
return@launch
65+
}
66+
result.error(errorCode, errorMessage, errorDetails)
67+
}
68+
}
69+
70+
override fun notImplemented() {
71+
scope.launch {
72+
if (isSent.getAndSet(true)) {
73+
Log.w(
74+
"AtomicResult(${operation})",
75+
"Attempted to send notImplemented value after initial reply"
76+
)
77+
return@launch
78+
}
79+
result.notImplemented()
80+
}
81+
}
82+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package com.amazonaws.amplify.amplify_auth_cognito.device
16+
17+
import com.amazonaws.mobile.client.results.Device
18+
import java.time.Instant
19+
import java.util.*
20+
21+
/**
22+
* Attribute key for retrieving a [Device] instance's name.
23+
*/
24+
const val deviceNameKey = "device_name"
25+
26+
/**
27+
* The device's name, if set.
28+
*/
29+
val Device.deviceName: String?
30+
get() = attributes?.get(deviceNameKey)
31+
32+
/**
33+
* Converts this device to a JSON-representable format.
34+
*/
35+
fun Device.toJson(): Map<String, Any?> = mapOf(
36+
"id" to deviceKey,
37+
"name" to deviceName,
38+
"attributes" to attributes,
39+
"createdDate" to createDate?.time,
40+
"lastModifiedDate" to lastModifiedDate?.time,
41+
"lastAuthenticatedDate" to lastAuthenticatedDate?.time
42+
)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package com.amazonaws.amplify.amplify_auth_cognito.device
16+
17+
import com.amazonaws.amplify.amplify_auth_cognito.AuthErrorHandler
18+
import com.amazonaws.amplify.amplify_auth_cognito.base.AtomicResult
19+
import com.amazonaws.mobile.client.AWSMobileClient
20+
import com.amazonaws.mobile.client.Callback
21+
import com.amazonaws.mobile.client.results.ListDevicesResult
22+
import com.amplifyframework.auth.AuthDevice
23+
import com.amplifyframework.auth.cognito.util.CognitoAuthExceptionConverter
24+
import com.amplifyframework.core.Amplify
25+
import io.flutter.plugin.common.MethodCall
26+
import io.flutter.plugin.common.MethodChannel
27+
import kotlinx.coroutines.*
28+
29+
/**
30+
* Handles method calls for the Devices API.
31+
*/
32+
class DeviceHandler(private val errorHandler: AuthErrorHandler) :
33+
MethodChannel.MethodCallHandler {
34+
companion object {
35+
/**
36+
* Methods this handler supports.
37+
*/
38+
private val methods = listOf("rememberDevice", "forgetDevice", "fetchDevices")
39+
40+
/**
41+
* Whether this class can handle [method].
42+
*/
43+
fun canHandle(method: String): Boolean = methods.contains(method)
44+
}
45+
46+
/**
47+
* Scope for running asynchronous tasks.
48+
*/
49+
private val scope = CoroutineScope(Dispatchers.IO) + CoroutineName("DeviceHandler")
50+
51+
@Suppress("UNCHECKED_CAST")
52+
override fun onMethodCall(call: MethodCall, _result: MethodChannel.Result) {
53+
val result = AtomicResult(_result, call.method)
54+
when (call.method) {
55+
"fetchDevices" -> fetchDevices(result)
56+
"rememberDevice" -> rememberDevice(result)
57+
"forgetDevice" -> {
58+
val deviceJson =
59+
(call.arguments as? Map<*, *> ?: emptyMap<String, Any?>()) as Map<String, Any?>
60+
var device: AuthDevice? = null
61+
if (deviceJson.isNotEmpty()) {
62+
val id by deviceJson
63+
device = AuthDevice.fromId(id as String)
64+
}
65+
forgetDevice(result, device)
66+
}
67+
}
68+
}
69+
70+
private fun fetchDevices(result: MethodChannel.Result) {
71+
try {
72+
val cognitoAuthPlugin = Amplify.Auth.getPlugin("awsCognitoAuthPlugin")
73+
val awsMobileClient = cognitoAuthPlugin.escapeHatch as AWSMobileClient
74+
scope.launch {
75+
awsMobileClient.deviceOperations.list(object : Callback<ListDevicesResult> {
76+
override fun onResult(listDevicesResult: ListDevicesResult) {
77+
result.success(listDevicesResult.devices.map { it.toJson() })
78+
}
79+
80+
override fun onError(exception: java.lang.Exception) {
81+
errorHandler.handleAuthError(
82+
result, CognitoAuthExceptionConverter.lookup(
83+
exception, "Fetching devices failed."
84+
)
85+
)
86+
}
87+
})
88+
}
89+
} catch (e: Exception) {
90+
errorHandler.handleAuthError(result, e)
91+
}
92+
}
93+
94+
private fun rememberDevice(result: MethodChannel.Result) {
95+
scope.launch {
96+
Amplify.Auth.rememberDevice(
97+
{ result.success(null) },
98+
{ errorHandler.handleAuthError(result, it) }
99+
)
100+
}
101+
}
102+
103+
private fun forgetDevice(result: MethodChannel.Result, device: AuthDevice? = null) {
104+
scope.launch {
105+
if (device != null) {
106+
Amplify.Auth.forgetDevice(
107+
device,
108+
{ result.success(null) },
109+
{ errorHandler.handleAuthError(result, it) }
110+
)
111+
} else {
112+
Amplify.Auth.forgetDevice(
113+
{ result.success(null) },
114+
{ errorHandler.handleAuthError(result, it) }
115+
)
116+
}
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)