Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added support for passwordless authentication #503

Merged
merged 23 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
41f573a
Added passwordless login to android
pmathew92 Jan 28, 2025
a333817
Added passworless login for ios platform
pmathew92 Jan 28, 2025
daf0e1a
Added API doc comments
pmathew92 Jan 28, 2025
61a506f
Code refactoring and fixed build error
pmathew92 Jan 29, 2025
79a8abe
Merge branch 'main' into SDK-5524
Widcket Jan 29, 2025
8e84622
Merge branch 'main' into SDK-5524
Widcket Jan 29, 2025
0771127
Merge branch 'main' of https://github.com/auth0/auth0-flutter into SD…
pmathew92 Jan 30, 2025
240ac76
Addressed review comments
pmathew92 Jan 30, 2025
d14a5b8
Addressed comments with respect to api name changes
pmathew92 Jan 30, 2025
9c57b26
Merge branch 'main' into SDK-5524
Widcket Jan 30, 2025
2dfd95e
Added parameters option in the api
pmathew92 Jan 30, 2025
965c581
Nesting the parameters in the request for ios
pmathew92 Jan 30, 2025
da6b53c
Fixed the methid channel null error
pmathew92 Jan 30, 2025
4a26de4
Generated system link files
pmathew92 Jan 30, 2025
04ad909
Added scopes as list with default value
pmathew92 Jan 30, 2025
3a9bb14
Refactored code for analize pipeline job
pmathew92 Jan 30, 2025
235c933
Updated the examples documentation
pmathew92 Jan 30, 2025
f366b4b
Merge branch 'main' into SDK-5524
pmathew92 Jan 30, 2025
b213e5e
Minor change in doc
pmathew92 Jan 30, 2025
7c1584a
Minor formatting issues
pmathew92 Jan 31, 2025
74e91cd
Fixed flutter ananlyze error
pmathew92 Jan 31, 2025
9073a89
Added value type to passwordless type enum
pmathew92 Jan 31, 2025
31a8049
Merge branch 'main' of https://github.com/auth0/auth0-flutter into SD…
pmathew92 Jan 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: "[email protected]", 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: "[email protected]", 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
Loading