Skip to content

Commit

Permalink
afpi: Added support for loginWithOtp (auth0#172)
Browse files Browse the repository at this point in the history
* implement login with otp for android

* implement login with otp for ios

* put mfaToken in details map instead of in _errorFlags of APIException

* naming improvements, remove validateClaims

* add dependency overrides

* fix android tests

* import MethodChannel.Result

* fix android tests

* fix parameter order

* add extra tests for ApiExceptionExtensions and AuthAPIHAndler.swift

* Update auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithOtpApiRequestHandlerTest.kt

Co-authored-by: Poovamraj T T <[email protected]>

* Update auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithOtpApiRequestHandlerTest.kt

Co-authored-by: Poovamraj T T <[email protected]>

* Update auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/AuthenticationExceptionExtensions.kt

Co-authored-by: Poovamraj T T <[email protected]>

* Update auth0_flutter/lib/src/authentication_api.dart

* Update auth0_flutter_platform_interface/lib/src/auth0_flutter_auth_platform.dart

Co-authored-by: Rita Zerrizuela <[email protected]>
Co-authored-by: Poovamraj T T <[email protected]>
  • Loading branch information
3 people authored Jan 18, 2023
1 parent 1b90c89 commit db8423d
Show file tree
Hide file tree
Showing 26 changed files with 695 additions and 30 deletions.
13 changes: 13 additions & 0 deletions .idea/auth0-flutter.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
))
private val authCallHandler = Auth0FlutterAuthMethodCallHandler(listOf(
LoginApiRequestHandler(),
LoginWithOtpApiRequestHandler(),
SignupApiRequestHandler(),
UserInfoApiRequestHandler(),
RenewApiRequestHandler(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,33 @@ package com.auth0.auth0_flutter
import com.auth0.android.authentication.AuthenticationException

fun AuthenticationException.toMap(): Map<String, Any> {
return mapOf(
"_statusCode" to this.statusCode,
"_errorFlags" to mapOf(
"isMultifactorRequired" to this.isMultifactorRequired,
"isMultifactorEnrollRequired" to this.isMultifactorEnrollRequired,
"isMultifactorCodeInvalid" to this.isMultifactorCodeInvalid,
"isMultifactorTokenInvalid" to this.isMultifactorTokenInvalid,
"isPasswordNotStrongEnough" to this.isPasswordNotStrongEnough,
"isPasswordAlreadyUsed" to this.isPasswordAlreadyUsed,
"isRuleError" to this.isRuleError,
"isInvalidCredentials" to this.isInvalidCredentials,
"isRefreshTokenDeleted" to this.isRefreshTokenDeleted,
"isAccessDenied" to this.isAccessDenied,
"isTooManyAttempts" to this.isTooManyAttempts,
"isVerificationRequired" to this.isVerificationRequired,
"isNetworkError" to this.isNetworkError,
"isBrowserAppNotAvailable" to this.isBrowserAppNotAvailable,
"isPKCENotAvailable" to this.isPKCENotAvailable,
"isInvalidAuthorizeURL" to this.isInvalidAuthorizeURL,
"isInvalidConfiguration" to this.isInvalidConfiguration,
"isCanceled" to this.isCanceled,
"isPasswordLeaked" to this.isPasswordLeaked,
"isLoginRequired" to this.isLoginRequired,
)
)
val exception = this
return buildMap {
put("_statusCode", exception.statusCode)
put("_errorFlags", mapOf(
"isMultifactorRequired" to exception.isMultifactorRequired,
"isMultifactorEnrollRequired" to exception.isMultifactorEnrollRequired,
"isMultifactorCodeInvalid" to exception.isMultifactorCodeInvalid,
"isMultifactorTokenInvalid" to exception.isMultifactorTokenInvalid,
"isPasswordNotStrongEnough" to exception.isPasswordNotStrongEnough,
"isPasswordAlreadyUsed" to exception.isPasswordAlreadyUsed,
"isRuleError" to exception.isRuleError,
"isInvalidCredentials" to exception.isInvalidCredentials,
"isRefreshTokenDeleted" to exception.isRefreshTokenDeleted,
"isAccessDenied" to exception.isAccessDenied,
"isTooManyAttempts" to exception.isTooManyAttempts,
"isVerificationRequired" to exception.isVerificationRequired,
"isNetworkError" to exception.isNetworkError,
"isBrowserAppNotAvailable" to exception.isBrowserAppNotAvailable,
"isPKCENotAvailable" to exception.isPKCENotAvailable,
"isInvalidAuthorizeURL" to exception.isInvalidAuthorizeURL,
"isInvalidConfiguration" to exception.isInvalidConfiguration,
"isCanceled" to exception.isCanceled,
"isPasswordLeaked" to exception.isPasswordLeaked,
"isLoginRequired" to exception.isLoginRequired,
))
if (exception.getValue("mfa_token") != null) { put("mfa_token", exception.getValue("mfa_token")!!) }
}
}

val AuthenticationException.isTooManyAttempts: Boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.auth0.auth0_flutter.request_handlers.api

import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.callback.Callback
import com.auth0.android.jwt.JWT
import com.auth0.android.result.Credentials
import com.auth0.auth0_flutter.createUserProfileFromClaims
import com.auth0.auth0_flutter.request_handlers.MethodCallRequest
import com.auth0.auth0_flutter.toMap
import com.auth0.auth0_flutter.utils.assertHasProperties
import io.flutter.plugin.common.MethodChannel
import java.text.SimpleDateFormat
import java.util.*

private const val AUTH_LOGIN_OTP_METHOD = "auth#loginOtp"

class LoginWithOtpApiRequestHandler: ApiRequestHandler {
override val method: String = AUTH_LOGIN_OTP_METHOD

override fun handle(
api: AuthenticationAPIClient,
request: MethodCallRequest,
result: MethodChannel.Result
) {
val args = request.data

assertHasProperties(listOf("mfaToken", "otp"), args)

val loginBuilder = api
.loginWithOTP(
args["mfaToken"] as String,
args["otp"] as String,
)

loginBuilder.start(object : Callback<Credentials, AuthenticationException> {
override fun onFailure(exception: AuthenticationException) {
result.error(
exception.getCode(),
exception.getDescription(),
exception.toMap()
)
}

override fun onSuccess(credentials: Credentials) {
val scope = credentials.scope?.split(" ") ?: listOf()
val jwt = JWT(credentials.idToken)
val userProfile = createUserProfileFromClaims(jwt.claims)
val sdf =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())

val formattedDate = sdf.format(credentials.expiresAt)
result.success(
mapOf(
"accessToken" to credentials.accessToken,
"idToken" to credentials.idToken,
"refreshToken" to credentials.refreshToken,
"userProfile" to userProfile.toMap(),
"expiresAt" to formattedDate,
"scopes" to scope,
"tokenType" to credentials.type
)
)
}
})
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,28 @@ class AuthenticationExceptionExtensionsTest {

assertThat(statusCode, equalTo(50))
}

@Test
fun `should set mfa_token when calling toMap()`() {
val exception = AuthenticationException(mapOf(
"mfa_token" to "test-mfaToken"
))

val map = exception.toMap()
val mfaToken = map["mfa_token"] as String

assertThat(mfaToken, equalTo("test-mfaToken"))
}

@Test
fun `should not set mfa_token when calling toMap()`() {
val exception = AuthenticationException(mapOf(
"code" to "some-code"
))

val map = exception.toMap()
val mfaToken = map["mfa_token"] as String?

assertThat(mfaToken, equalTo(null))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package com.auth0.auth0_flutter.request_handlers.api

import com.auth0.android.Auth0
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.callback.Callback
import com.auth0.android.request.AuthenticationRequest
import com.auth0.android.result.Credentials
import com.auth0.auth0_flutter.JwtTestUtils

import com.auth0.auth0_flutter.request_handlers.MethodCallRequest
import io.flutter.plugin.common.MethodChannel.Result
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.*
import org.robolectric.RobolectricTestRunner
import java.text.SimpleDateFormat
import java.util.*

@RunWith(RobolectricTestRunner::class)
class LoginWithOtpApiRequestHandlerTest {
@Test
fun `should throw when missing otp`() {
val options = hashMapOf("mfaToken" to "test-mfa-token")
val handler = LoginWithOtpApiRequestHandler()
val mockApi = mock<AuthenticationAPIClient>()
val mockAccount = mock<Auth0>()
val mockResult = mock<Result>()
val request = MethodCallRequest(account = mockAccount, options)

val exception = Assert.assertThrows(IllegalArgumentException::class.java) {
handler.handle(
mockApi,
request,
mockResult
)
}

assertThat(
exception.message,
equalTo("Required property 'otp' is not provided.")
)
}

@Test
fun `should throw when missing mfaToken`() {
val options = hashMapOf("otp" to "test-otp")
val handler = LoginWithOtpApiRequestHandler()
val mockApi = mock<AuthenticationAPIClient>()
val mockAccount = mock<Auth0>()
val mockResult = mock<Result>()
val request = MethodCallRequest(account = mockAccount, options)

val exception = Assert.assertThrows(IllegalArgumentException::class.java) {
handler.handle(
mockApi,
request,
mockResult
)
}

assertThat(
exception.message,
equalTo("Required property 'mfaToken' is not provided.")
)
}

@Test
fun `should call loginWithOTp with the correct parameters`() {
val options = hashMapOf(
"otp" to "test-otp",
"mfaToken" to "test-mfaToken"
)
val handler = LoginWithOtpApiRequestHandler()
val mockLoginBuilder = mock<AuthenticationRequest>()
val mockApi = mock<AuthenticationAPIClient>()
val mockAccount = mock<Auth0>()
val mockResult = mock<Result>()
val request = MethodCallRequest(account = mockAccount, options)

doReturn(mockLoginBuilder).`when`(mockApi).loginWithOTP(any(), any())

handler.handle(
mockApi,
request,
mockResult
)

verify(mockApi).loginWithOTP("test-mfaToken", "test-otp")
verify(mockLoginBuilder).start(any())
}

@Test
fun `should call result error on failure`() {
val options = hashMapOf(
"otp" to "test-otp",
"mfaToken" to "test-mfaToken"
)
val handler = LoginWithOtpApiRequestHandler()
val mockLoginBuilder = mock<AuthenticationRequest>()
val mockApi = mock<AuthenticationAPIClient>()
val mockAccount = mock<Auth0>()
val mockResult = mock<Result>()
val request = MethodCallRequest(account = mockAccount, options)
val exception =
AuthenticationException(code = "test-code", description = "test-description")

doReturn(mockLoginBuilder).`when`(mockApi).loginWithOTP(any(), any())
doAnswer {
val ob = it.getArgument<Callback<Credentials, AuthenticationException>>(0)
ob.onFailure(exception)
}.`when`(mockLoginBuilder).start(any())

handler.handle(
mockApi,
request,
mockResult
)

verify(mockResult).error(eq("test-code"), eq("test-description"), any())
}

@Test
fun `should call result success on success`() {
val options = hashMapOf(
"otp" to "test-otp",
"mfaToken" to "test-mfaToken"
)
val handler = LoginWithOtpApiRequestHandler()
val mockLoginBuilder = mock<AuthenticationRequest>()
val mockApi = mock<AuthenticationAPIClient>()
val mockAccount = mock<Auth0>()
val mockResult = mock<Result>()
val request = MethodCallRequest(account = mockAccount, options)
val idToken = JwtTestUtils.createJwt(claims = mapOf("name" to "John Doe"))
val credentials = Credentials(idToken, "test", "", null, Date(), "scope1 scope2")

doReturn(mockLoginBuilder).`when`(mockApi).loginWithOTP(any(), any())
doAnswer {
val ob = it.getArgument<Callback<Credentials, AuthenticationException>>(0)
ob.onSuccess(credentials)
}.`when`(mockLoginBuilder).start(any())

handler.handle(
mockApi,
request,
mockResult
)

val captor = argumentCaptor<() -> Map<String, *>>()
verify(mockResult).success(captor.capture())

val sdf =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())

val formattedDate = sdf.format(credentials.expiresAt)

assertThat((captor.firstValue as Map<*, *>)["accessToken"], equalTo(credentials.accessToken))
assertThat((captor.firstValue as Map<*, *>)["idToken"], equalTo(credentials.idToken))
assertThat((captor.firstValue as Map<*, *>)["refreshToken"], equalTo(credentials.refreshToken))
assertThat((captor.firstValue as Map<*, *>)["expiresAt"] as String, equalTo(formattedDate))
assertThat((captor.firstValue as Map<*, *>)["scopes"], equalTo(listOf("scope1", "scope2")))
assertThat(((captor.firstValue as Map<*, *>)["userProfile"] as Map<*, *>)["name"], equalTo("John Doe"))
}
}
4 changes: 4 additions & 0 deletions auth0_flutter/example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
2D82163828F9A89300467FD1 /* AuthApiLoginWithOtpMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82163728F9A89300467FD1 /* AuthApiLoginWithOtpMethodHandlerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3F1B396BCCAD9DA04D4998A2 /* Pods_Runner_RunnerUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52FDC1B88C21EFE2421C98FA /* Pods_Runner_RunnerUITests.framework */; };
5C08DBC8288A7646000D2F37 /* CredentialsManagerExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C08DBC7288A7646000D2F37 /* CredentialsManagerExtensionsTests.swift */; };
Expand Down Expand Up @@ -80,6 +81,7 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
29F5DF109AAC0995DAF67FD3 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2D82163728F9A89300467FD1 /* AuthApiLoginWithOtpMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthApiLoginWithOtpMethodHandlerTests.swift; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3DA3F79738DB2CDFBC131B5E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
52FDC1B88C21EFE2421C98FA /* Pods_Runner_RunnerUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner_RunnerUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -201,6 +203,7 @@
5C335E4627FE68A500EDDE3A /* AuthAPI */ = {
isa = PBXGroup;
children = (
2D82163728F9A89300467FD1 /* AuthApiLoginWithOtpMethodHandlerTests.swift */,
5C335E4727FE68BC00EDDE3A /* AuthAPISpies.swift */,
5C59DA6427FFCF0600365CDB /* AuthAPIHandlerTests.swift */,
5C59DA6627FFE75600365CDB /* AuthAPILoginUsernameOrEmailMethodHandlerTests.swift */,
Expand Down Expand Up @@ -581,6 +584,7 @@
5C59DA8B2809386B00365CDB /* AuthAPIExtensionsTests.swift in Sources */,
5C59DA8D280938BE00365CDB /* WebAuthExtensionsTests.swift in Sources */,
5C335E3F27FBCD1D00EDDE3A /* SwiftAuth0FlutterPluginTests.swift in Sources */,
2D82163828F9A89300467FD1 /* AuthApiLoginWithOtpMethodHandlerTests.swift in Sources */,
5C4E65C5286D1CFB00141449 /* CredentialsManagerHasValidMethodHandlerTests.swift in Sources */,
5C328B4827F7822600451E70 /* WebAuthHandlerTests.swift in Sources */,
5C328B5327F7B12F00451E70 /* WebAuthSpies.swift in Sources */,
Expand Down
Loading

0 comments on commit db8423d

Please sign in to comment.