Skip to content

Commit

Permalink
feat: Added support for passwordless authentication (#503)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmathew92 authored Jan 31, 2025
2 parents 77a0dbf + 31a8049 commit 3f4ae64
Show file tree
Hide file tree
Showing 27 changed files with 759 additions and 0 deletions.
42 changes: 42 additions & 0 deletions auth0_flutter/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- [📱 Authentication API](#-authentication-api)
- [Login with database connection](#login-with-database-connection)
- [Sign up with database connection](#sign-up-with-database-connection)
- [Passwordless Login](#passwordless-login)
- [Retrieve user information](#retrieve-user-information)
- [Renew credentials](#renew-credentials)
- [Errors](#errors-2)
Expand Down Expand Up @@ -586,6 +587,47 @@ final databaseUser = await auth0.api.signup(

> 💡 You might want to log the user in after signup. See [Login with database connection](#login-with-database-connection) above for an example.
### Passwordless Login
Passwordless is a two-step authentication flow that requires the **Passwordless OTP** grant to be enabled for your Auth0 application. Check [our documentation](https://auth0.com/docs/get-started/applications/application-grant-types) for more information.

#### 1. Start the passwordless flow

Request a code to be sent to the user's email or phone number. For email scenarios, a link can be sent in place of the code.

```dart
await auth0.api.startPasswordlessWithEmail(
email: "support@auth0.com", passwordlessType: PasswordlessType.code);
```
<details>
<summary>Using PhoneNumber</summary>

```dart
await auth0.api.startPasswordlessWithPhoneNumber(
phoneNumber: "123456789", passwordlessType: PasswordlessType.code);
```
</details>

#### 2. Login with the received code

To complete the authentication, you must send back that code the user received along with the email or phone number used to start the flow.

```dart
final credentials = await auth0.api.loginWithEmailCode(
email: "support@auth0.com", verificationCode: "000000");
```

<details>
<summary>Using SMS</summary>

```dart
final credentials = await auth0.api.loginWithSmsCode(
phoneNumber: "123456789", verificationCode: "000000");
```
</details>

> [!NOTE]
> Sending additional parameters is supported only on iOS at the moment.
### Retrieve user information

Fetch the latest user information from the `/userinfo` endpoint.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
LoginApiRequestHandler(),
LoginWithOtpApiRequestHandler(),
MultifactorChallengeApiRequestHandler(),
EmailPasswordlessApiRequestHandler(),
PhoneNumberPasswordlessApiRequestHandler(),
LoginWithEmailCodeApiRequestHandler(),
LoginWithSMSCodeApiRequestHandler(),
SignupApiRequestHandler(),
UserInfoApiRequestHandler(),
RenewApiRequestHandler(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.auth0.auth0_flutter.request_handlers.api

import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.authentication.PasswordlessType
import com.auth0.android.callback.Callback
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.util.HashMap

private const val PASSWORDLESS_EMAIL_LOGIN_METHOD = "auth#passwordlessWithEmail"

class EmailPasswordlessApiRequestHandler : ApiRequestHandler {
override val method: String = PASSWORDLESS_EMAIL_LOGIN_METHOD

override fun handle(
api: AuthenticationAPIClient,
request: MethodCallRequest,
result: MethodChannel.Result
) {
val args = request.data
assertHasProperties(listOf("email", "passwordlessType"), args)
val passwordlessType = getPasswordlessType(args["passwordlessType"] as String)

val builder = api.passwordlessWithEmail(
args["email"] as String,
passwordlessType
)

if (args["parameters"] is HashMap<*, *>) {
builder.addParameters(args["parameters"] as Map<String, String>)
}

builder.start(object : Callback<Void?, AuthenticationException> {
override fun onFailure(error: AuthenticationException) {
result.error(
error.getCode(),
error.getDescription(),
error.toMap()
)
}

override fun onSuccess(void: Void?) {
result.success(null)
}
})
}

private fun getPasswordlessType(passwordlessType: String): PasswordlessType {
return when (passwordlessType) {
"code" -> PasswordlessType.CODE
"link" -> PasswordlessType.WEB_LINK
"link_android" -> PasswordlessType.ANDROID_LINK
else -> PasswordlessType.CODE
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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.result.Credentials
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.util.ArrayList
import java.util.HashMap

private const val EMAIL_LOGIN_METHOD = "auth#loginWithEmail"

class LoginWithEmailCodeApiRequestHandler : ApiRequestHandler {
override val method: String = EMAIL_LOGIN_METHOD

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

val builder = api.loginWithEmail(
args["email"] as String,
args["verificationCode"] as String
).apply {
val scopes = (args["scopes"] ?: arrayListOf<String>()) as ArrayList<*>
setScope(scopes.joinToString(separator = " "))
if (args["audience"] is String) {
setAudience(args["audience"] as String)
}
if (args["parameters"] is HashMap<*, *>) {
addParameters(args["parameters"] as Map<String, String>)
}
}

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

override fun onSuccess(credentials: Credentials) {
val scope = credentials.scope?.split(" ") ?: listOf()
val formattedDate = credentials.expiresAt.toInstant().toString()
result.success(
mapOf(
"accessToken" to credentials.accessToken,
"idToken" to credentials.idToken,
"refreshToken" to credentials.refreshToken,
"userProfile" to credentials.user.toMap(),
"expiresAt" to formattedDate,
"scopes" to scope,
"tokenType" to credentials.type
)
)
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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.result.Credentials
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.util.ArrayList
import java.util.HashMap

private const val SMS_LOGIN_METHOD = "auth#loginWithPhoneNumber"

class LoginWithSMSCodeApiRequestHandler : ApiRequestHandler {
override val method: String = SMS_LOGIN_METHOD

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

val builder = api.loginWithPhoneNumber(
args["email"] as String,
args["verificationCode"] as String
).apply {
val scopes = (args["scopes"] ?: arrayListOf<String>()) as ArrayList<*>
setScope(scopes.joinToString(separator = " "))
if (args["audience"] is String) {
setAudience(args["audience"] as String)
}
if (args["parameters"] is HashMap<*, *>) {
addParameters(args["parameters"] as Map<String, String>)
}
}

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

override fun onSuccess(credentials: Credentials) {
val scope = credentials.scope?.split(" ") ?: listOf()
val formattedDate = credentials.expiresAt.toInstant().toString()
result.success(
mapOf(
"accessToken" to credentials.accessToken,
"idToken" to credentials.idToken,
"refreshToken" to credentials.refreshToken,
"userProfile" to credentials.user.toMap(),
"expiresAt" to formattedDate,
"scopes" to scope,
"tokenType" to credentials.type
)
)
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.auth0.auth0_flutter.request_handlers.api

import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.authentication.PasswordlessType
import com.auth0.android.callback.Callback
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.util.HashMap

private const val PASSWORDLESS_PHONE_NUMBER_LOGIN_METHOD = "auth#passwordlessWithPhoneNumber"

class PhoneNumberPasswordlessApiRequestHandler : ApiRequestHandler {
override val method: String = PASSWORDLESS_PHONE_NUMBER_LOGIN_METHOD

override fun handle(
api: AuthenticationAPIClient,
request: MethodCallRequest,
result: MethodChannel.Result
) {
val args = request.data
assertHasProperties(listOf("phoneNumber", "passwordlessType"), args)
val passwordlessType = getPasswordlessType(args["passwordlessType"] as String)

val builder = api.passwordlessWithSMS(
args["phoneNumber"] as String,
passwordlessType
)

if (args["parameters"] is HashMap<*, *>) {
builder.addParameters(args["parameters"] as Map<String, String>)
}

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

override fun onSuccess(void: Void?) {
result.success(void)
}
})
}

private fun getPasswordlessType(passwordlessType: String): PasswordlessType {
return when (passwordlessType) {
"code" -> PasswordlessType.CODE
"link" -> PasswordlessType.WEB_LINK
"link_android" -> PasswordlessType.ANDROID_LINK
else -> PasswordlessType.CODE
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Auth0

#if os(iOS)
import Flutter
#else
import FlutterMacOS
#endif

struct AuthAPIPasswordlessEmailMethodHandler: MethodHandler {
enum Argument: String {
case email
case passwordlessType
case parameters
}

let client: Authentication

func handle(with arguments: [String: Any], callback: @escaping FlutterResult) {
guard let email = arguments[Argument.email] as? String else {
return callback(FlutterError(from: .requiredArgumentMissing(Argument.email.rawValue)))
}

guard let passwordlessTypeString = arguments[Argument.passwordlessType] as? String,
let passwordlessType = PasswordlessType(rawValue: passwordlessTypeString) else {
return callback(FlutterError(from: .requiredArgumentMissing(Argument.passwordlessType.rawValue)))
}

guard let parameters = arguments[Argument.parameters] as? [String: Any] else {
return callback(FlutterError(from: .requiredArgumentMissing(Argument.parameters.rawValue)))
}

client
.startPasswordless(email: email,
type: passwordlessType,
connection: "email"
)
.parameters(["authParams":parameters])
.start {
switch $0 {
case let .success:
callback(nil)
case let .failure(error):
callback(FlutterError(from: error))
}
}
}
}
8 changes: 8 additions & 0 deletions auth0_flutter/darwin/Classes/AuthAPI/AuthAPIHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public class AuthAPIHandler: NSObject, FlutterPlugin {
case userInfo = "auth#userInfo"
case renew = "auth#renew"
case resetPassword = "auth#resetPassword"
case passwordlessWithEmail = "auth#passwordlessWithEmail"
case passwordlessWithPhoneNumber = "auth#passwordlessWithPhoneNumber"
case loginWithEmailCode = "auth#loginWithEmail"
case loginWithSMSCode = "auth#loginWithPhoneNumber"
}

private static let channelName = "auth0.com/auth0_flutter/auth"
Expand Down Expand Up @@ -55,6 +59,10 @@ public class AuthAPIHandler: NSObject, FlutterPlugin {
case .userInfo: return AuthAPIUserInfoMethodHandler(client: client)
case .renew: return AuthAPIRenewMethodHandler(client: client)
case .resetPassword: return AuthAPIResetPasswordMethodHandler(client: client)
case .passwordlessWithEmail: return AuthAPIPasswordlessEmailMethodHandler(client: client)
case .passwordlessWithPhoneNumber: return AuthAPIPasswordlessPhoneNumberMethodHandler(client: client)
case .loginWithEmailCode: return AuthAPILoginWithEmailMethodHandler(client: client)
case .loginWithSMSCode: return AuthAPILoginWithPhoneNumberMethodHandler(client: client)
}
}

Expand Down
Loading

0 comments on commit 3f4ae64

Please sign in to comment.