diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt index 24d75c58f..5309ff75e 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt @@ -58,6 +58,7 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { listOf( LoginApiRequestHandler(), LoginWithOtpApiRequestHandler(), + LoginWithFacebookApiRequestHandler(), MultifactorChallengeApiRequestHandler(), EmailPasswordlessApiRequestHandler(), PhoneNumberPasswordlessApiRequestHandler(), diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithFacebookApiRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithFacebookApiRequestHandler.kt new file mode 100644 index 000000000..3f258cd44 --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithFacebookApiRequestHandler.kt @@ -0,0 +1,52 @@ +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.toMap +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.utils.assertHasProperties +import io.flutter.plugin.common.MethodChannel + +private const val AUTH_LOGIN_WITH_FACEBOOK_METHOD = "auth#loginWithFacebook" + +class LoginWithFacebookApiRequestHandler : ApiRequestHandler { + override val method: String = AUTH_LOGIN_WITH_FACEBOOK_METHOD + + override fun handle(api: AuthenticationAPIClient, request: MethodCallRequest, result: MethodChannel.Result) { + assertHasProperties(listOf("accessToken"), request.data) + + val accessToken = request.data["accessToken"] as String + val profile = request.data["profile"] as? Map + val builder = if (profile != null) { + api.loginWithNativeSocialToken(accessToken, "facebook", profile) + } else { + api.loginWithNativeSocialToken(accessToken, "facebook") + } + + // Add optional parameters + (request.data["scopes"] as? List<*>)?.let { scopes -> + builder.setScope((scopes as List).joinToString(" ")) + } + (request.data["audience"] as? String)?.let { builder.setAudience(it) } + + if (request.data["parameters"] is HashMap<*, *>) { + builder.addParameters(request.data["parameters"] as Map) + } + + builder.start(object : Callback { + override fun onFailure(exception: AuthenticationException) { + result.error( + exception.getCode(), + exception.getDescription(), + exception.toMap() + ) + } + + override fun onSuccess(credentials: Credentials) { + result.success(credentials.toMap()) + } + }) + } +} diff --git a/auth0_flutter/darwin/Classes/AuthAPI/AuthAPIHandler.swift b/auth0_flutter/darwin/Classes/AuthAPI/AuthAPIHandler.swift index e2380a1f4..49d7a6d59 100644 --- a/auth0_flutter/darwin/Classes/AuthAPI/AuthAPIHandler.swift +++ b/auth0_flutter/darwin/Classes/AuthAPI/AuthAPIHandler.swift @@ -17,6 +17,7 @@ public class AuthAPIHandler: NSObject, FlutterPlugin { enum Method: String, CaseIterable { case loginWithUsernameOrEmail = "auth#login" case loginWithOTP = "auth#loginOtp" + case loginWithFacebook = "auth#loginWithFacebook" case multifactorChallenge = "auth#multifactorChallenge" case signup = "auth#signUp" case userInfo = "auth#userInfo" @@ -60,6 +61,7 @@ public class AuthAPIHandler: NSObject, FlutterPlugin { switch method { case .loginWithUsernameOrEmail: return AuthAPILoginUsernameOrEmailMethodHandler(client: client) case .loginWithOTP: return AuthAPILoginWithOTPMethodHandler(client: client) + case .loginWithFacebook: return AuthAPILoginWithFacebookMethodHandler(client: client) case .multifactorChallenge: return AuthAPIMultifactorChallengeMethodHandler(client: client) case .signup: return AuthAPISignupMethodHandler(client: client) case .userInfo: return AuthAPIUserInfoMethodHandler(client: client) diff --git a/auth0_flutter/darwin/Classes/AuthAPI/AuthAPILoginWithFacebookMethodHandler.swift b/auth0_flutter/darwin/Classes/AuthAPI/AuthAPILoginWithFacebookMethodHandler.swift new file mode 100644 index 000000000..8aa632f78 --- /dev/null +++ b/auth0_flutter/darwin/Classes/AuthAPI/AuthAPILoginWithFacebookMethodHandler.swift @@ -0,0 +1,47 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct AuthAPILoginWithFacebookMethodHandler: MethodHandler { + enum Argument: String { + case accessToken + case scopes + case parameters + case audience + case profile + } + + let client: Authentication + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + guard let accessToken = arguments[Argument.accessToken] as? String else { + return callback(FlutterError(from: .requiredArgumentMissing(Argument.accessToken.rawValue))) + } + guard let scopes = arguments[Argument.scopes] as? [String] else { + return callback(FlutterError(from: .requiredArgumentMissing(Argument.scopes.rawValue))) + } + guard let parameters = arguments[Argument.parameters] as? [String: Any] else { + return callback(FlutterError(from: .requiredArgumentMissing(Argument.parameters.rawValue))) + } + + let audience = arguments[Argument.audience] as? String + let profile = arguments[Argument.profile] as? [String: Any] ?? [:] + + client + .login(facebookSessionAccessToken: accessToken, + profile: profile, + audience: audience, + scope: scopes.asSpaceSeparatedString) + .parameters(parameters) + .start { + switch $0 { + case let .success(credentials): callback(result(from: credentials)) + case let .failure(error): callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/example/ios/.symlinks/plugins/auth0_flutter/darwin/Classes/AuthAPI/AuthAPILoginWithFacebookMethodHandler.swift b/auth0_flutter/example/ios/.symlinks/plugins/auth0_flutter/darwin/Classes/AuthAPI/AuthAPILoginWithFacebookMethodHandler.swift new file mode 100644 index 000000000..8aa632f78 --- /dev/null +++ b/auth0_flutter/example/ios/.symlinks/plugins/auth0_flutter/darwin/Classes/AuthAPI/AuthAPILoginWithFacebookMethodHandler.swift @@ -0,0 +1,47 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct AuthAPILoginWithFacebookMethodHandler: MethodHandler { + enum Argument: String { + case accessToken + case scopes + case parameters + case audience + case profile + } + + let client: Authentication + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + guard let accessToken = arguments[Argument.accessToken] as? String else { + return callback(FlutterError(from: .requiredArgumentMissing(Argument.accessToken.rawValue))) + } + guard let scopes = arguments[Argument.scopes] as? [String] else { + return callback(FlutterError(from: .requiredArgumentMissing(Argument.scopes.rawValue))) + } + guard let parameters = arguments[Argument.parameters] as? [String: Any] else { + return callback(FlutterError(from: .requiredArgumentMissing(Argument.parameters.rawValue))) + } + + let audience = arguments[Argument.audience] as? String + let profile = arguments[Argument.profile] as? [String: Any] ?? [:] + + client + .login(facebookSessionAccessToken: accessToken, + profile: profile, + audience: audience, + scope: scopes.asSpaceSeparatedString) + .parameters(parameters) + .start { + switch $0 { + case let .success(credentials): callback(result(from: credentials)) + case let .failure(error): callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/ios/Classes/AuthAPI/AuthAPILoginWithFacebookMethodHandler.swift b/auth0_flutter/ios/Classes/AuthAPI/AuthAPILoginWithFacebookMethodHandler.swift new file mode 100644 index 000000000..a4a3d571a --- /dev/null +++ b/auth0_flutter/ios/Classes/AuthAPI/AuthAPILoginWithFacebookMethodHandler.swift @@ -0,0 +1,44 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct AuthAPILoginWithFacebookMethodHandler: MethodHandler { + enum Argument: String { + case accessToken + case scopes + case parameters + case audience + } + + let client: Authentication + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + guard let accessToken = arguments[Argument.accessToken] as? String else { + return callback(FlutterError(from: .requiredArgumentMissing(Argument.accessToken.rawValue))) + } + guard let scopes = arguments[Argument.scopes] as? [String] else { + return callback(FlutterError(from: .requiredArgumentMissing(Argument.scopes.rawValue))) + } + guard let parameters = arguments[Argument.parameters] as? [String: Any] else { + return callback(FlutterError(from: .requiredArgumentMissing(Argument.parameters.rawValue))) + } + + let audience = arguments[Argument.audience] as? String + + client + .login(facebookSessionAccessToken: accessToken, + audience: audience, + scope: scopes.asSpaceSeparatedString) + .parameters(parameters) + .start { + switch $0 { + case let .success(credentials): callback(result(from: credentials)) + case let .failure(error): callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/lib/src/mobile/authentication_api.dart b/auth0_flutter/lib/src/mobile/authentication_api.dart index 9e2a639fe..1a8e45546 100644 --- a/auth0_flutter/lib/src/mobile/authentication_api.dart +++ b/auth0_flutter/lib/src/mobile/authentication_api.dart @@ -112,6 +112,67 @@ class AuthenticationApi { mfaToken: mfaToken, ))); + /// Authenticates the user with a Facebook access token. + /// If successful, it returns a set of tokens, as well as the user's profile + /// (constructed from ID token claims). + /// + /// This method requires that you've already obtained a valid Facebook access + /// token from the Facebook SDK. The token will be exchanged for Auth0 tokens. + /// + /// ## Endpoint + /// https://auth0.com/docs/api/authentication#social-login-with-provider-s-access-token + /// + /// ## Notes + /// + /// * [audience] relates to the API Identifier you want to reference in your + /// access tokens. See [API settings](https://auth0.com/docs/get-started/apis/api-settings) + /// to learn more. + /// * [scopes] defaults to `openid profile email offline_access`. You can + /// override these scopes, but `openid` and `profile` are always requested + /// regardless of this setting. + /// * [profile] contains user profile data from Facebook. This should be the + /// profile object obtained from the Facebook SDK. + /// * [parameters] can be used to send through custom parameters to the + /// endpoint to be picked up in a Rule or Action. + /// + /// ## Usage example + /// + /// ```dart + /// // First, get Facebook access token from Facebook SDK + /// final facebookToken = await getFacebookAccessToken(); + /// final facebookProfile = await getFacebookProfile(); + /// + /// // Then authenticate with Auth0 + /// final result = await auth0.api.loginWithFacebook( + /// accessToken: facebookToken, + /// profile: facebookProfile + /// ); + /// ``` + Future loginWithFacebook({ + required final String accessToken, + final String? audience, + final Set scopes = const { + 'openid', + 'profile', + 'email', + 'offline_access' + }, + final Map parameters = const {}, + final Map? profile, + }) { + // Ensure openid and profile are always included + final effectiveScopes = {...scopes, 'openid', 'profile'}; + + return Auth0FlutterAuthPlatform.instance + .loginWithFacebook(_createApiRequest(AuthLoginWithSocialTokenOptions( + accessToken: accessToken, + audience: audience, + scopes: effectiveScopes, + parameters: parameters, + profile: profile, + ))); + } + /// Requests a challenge for multi-factor authentication (MFA) based on the /// challenge types supported by the app and user. /// diff --git a/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart b/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart index a65820a78..6f55a9e76 100644 --- a/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart +++ b/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart @@ -4,6 +4,7 @@ export 'src/auth/auth_dpop_headers_options.dart'; export 'src/auth/auth_login_code_options.dart'; export 'src/auth/auth_login_options.dart'; export 'src/auth/auth_login_with_otp_options.dart'; +export 'src/auth/auth_login_with_social_token_options.dart'; export 'src/auth/auth_multifactor_challenge_options.dart'; export 'src/auth/auth_passwordless_login_options.dart'; export 'src/auth/auth_passwordless_type.dart'; diff --git a/auth0_flutter_platform_interface/lib/src/auth/auth_login_with_social_token_options.dart b/auth0_flutter_platform_interface/lib/src/auth/auth_login_with_social_token_options.dart new file mode 100644 index 000000000..ee65d92d2 --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/auth/auth_login_with_social_token_options.dart @@ -0,0 +1,26 @@ +import '../request/request_options.dart'; + +class AuthLoginWithSocialTokenOptions implements RequestOptions { + final String accessToken; + final String? audience; + final Set scopes; + final Map parameters; + final Map? profile; + + AuthLoginWithSocialTokenOptions({ + required this.accessToken, + this.audience, + this.scopes = const {}, + this.parameters = const {}, + this.profile, + }); + + @override + Map toMap() => { + 'accessToken': accessToken, + 'audience': audience, + 'scopes': scopes.toList(), + 'parameters': parameters, + 'profile': profile + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/auth0_flutter_auth_platform.dart b/auth0_flutter_platform_interface/lib/src/auth0_flutter_auth_platform.dart index 0d0c913e2..5b465fc0c 100644 --- a/auth0_flutter_platform_interface/lib/src/auth0_flutter_auth_platform.dart +++ b/auth0_flutter_platform_interface/lib/src/auth0_flutter_auth_platform.dart @@ -4,6 +4,7 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'auth/auth_login_code_options.dart'; import 'auth/auth_login_options.dart'; import 'auth/auth_login_with_otp_options.dart'; +import 'auth/auth_login_with_social_token_options.dart'; import 'auth/auth_multifactor_challenge_options.dart'; import 'auth/auth_passwordless_login_options.dart'; import 'auth/auth_renew_access_token_options.dart'; @@ -37,6 +38,11 @@ abstract class Auth0FlutterAuthPlatform extends PlatformInterface { throw UnimplementedError('authLoginWithOtp() has not been implemented'); } + Future loginWithFacebook( + final ApiRequest request) { + throw UnimplementedError('loginWithFacebook() has not been implemented'); + } + Future multifactorChallenge( final ApiRequest request) { throw UnimplementedError('multifactorChallenge() has not been implemented'); diff --git a/auth0_flutter_platform_interface/lib/src/method_channel_auth0_flutter_auth.dart b/auth0_flutter_platform_interface/lib/src/method_channel_auth0_flutter_auth.dart index 38e335591..14a802cd6 100644 --- a/auth0_flutter_platform_interface/lib/src/method_channel_auth0_flutter_auth.dart +++ b/auth0_flutter_platform_interface/lib/src/method_channel_auth0_flutter_auth.dart @@ -4,6 +4,7 @@ import 'auth/api_exception.dart'; import 'auth/auth_login_code_options.dart'; import 'auth/auth_login_options.dart'; import 'auth/auth_login_with_otp_options.dart'; +import 'auth/auth_login_with_social_token_options.dart'; import 'auth/auth_multifactor_challenge_options.dart'; import 'auth/auth_passwordless_login_options.dart'; import 'auth/auth_renew_access_token_options.dart'; @@ -22,6 +23,7 @@ const MethodChannel _channel = MethodChannel('auth0.com/auth0_flutter/auth'); const String authLoginMethod = 'auth#login'; const String authLoginWithOtpMethod = 'auth#loginOtp'; +const String authLoginWithFacebookMethod = 'auth#loginWithFacebook'; const String authMultifactorChallengeMethod = 'auth#multifactorChallenge'; const String authStartPasswordlessWithEmailMethod = 'auth#passwordlessWithEmail'; @@ -52,6 +54,15 @@ class MethodChannelAuth0FlutterAuth extends Auth0FlutterAuthPlatform { return Credentials.fromMap(result); } + @override + Future loginWithFacebook( + final ApiRequest request) async { + final Map result = + await invokeRequest(method: authLoginWithFacebookMethod, request: request); + + return Credentials.fromMap(result); + } + @override Future multifactorChallenge( final ApiRequest request) async {